蝇量级的JSON模型转换库(OC,Swift通用)

2,297 阅读5分钟

Github地址

看到JSON模型转换库,相信大家都没兴趣了,因为各自项目里肯定早都有了。其实也不是我想重复造轮子,主要是这个库其实早在2012年这个库就有了,而那时既没有小码哥的MJExtension,也没有牛逼闪闪的YYModel。那时的JSON模型转换库没有几个,而且都又大又难用,所以我当时自己写了一个。这个库在公司的项目中一直沿用至今,中间我陆续做了点扩展,也优化了一下性能,整体还是非常稳定可用的。

今天突然想把它拿出来,给大家分享一下。因为这个库虽然不是性能最强最全面的,但是它足够的简单,直白,只有两个文件,300行代码,可供初学者学习,也可用大家自行扩展满足自己的需求。

特性

  • 极简,没有复杂的调用,只有300行代码。
  • 市面上其他库支持的转换,这个基本上都有了。
  • Swift下也可以使用。
  • 性能优于MJExtention。

使用

和其他库基本上是差不多的。例子可以参考main.m。

Swift下使用

这个库本身是用OC写的,如果是纯Swift项目就不建议用了。但是对于OC和Swift混编项目,尤其是从OC转过来的项目,还是很好用的。因为OC下的转换通常只要一行代码,项目切换到Swift以后,如果用SwiftyJSON还是挺不习惯的。用这个库可以让OC和Swift里的开发习惯保持基本一致。

Swift下的示例可以参考SwiftJsonTest.swift。 具体有如下几点要求:

  • 模型类需要继承自NSObject,并用@objcMembers声明。
  • 枚举需要继承自Int,并用@objc声明。
  • Int等基础数据类型属性不能声明为可选,需要赋初始值。
  • 枚举属性不能声明为可选,需要赋初始值。
  • 数组属性如果包含自定义类型的元素,映射的时候元素类名前要加上模块名(App名或库名)

代码示例如下:

@objc enum OrderState: Int {
    case created = 1
    case completed = 2
    case canceled = 3
}

@objcMembers class OrderInfo: NSObject, JSONEx {
    var state: OrderState = .created
    var ID: String?
    var count: Int = 0
    var product: ProductInfo?
    
    static func customPropertyNameForKeys() -> [String : String] {
        return ["ID": "id"]
    }
}

@objcMembers class School: NSObject, JSONEx {
    var address: String?
    var students: [Student] = []
    
    static func arrayPropertyItemClasses() -> [String : String] {
        return ["students": "JSONEx.Student"]
    }
}

调用的时候一句就可以了:

let obj: PhoneInfo = PhoneInfo(dictionary: dic)

和OC下的使用是基本一致的。

实现

整体的实现都非常简单直白,只有字典NSDictionary给对象属性赋值的部分代码稍微多一点,这个赋值是基于KVC做的,如果属性有setter方法的话KVC的效率是不差的。对象的基类属性的赋值是通过递归做的。 主要的方法如下:

@implementation NSDictionary (NSDictionary_ObjectMapping)

- (void)setObject:(id)obj fromClass:(Class)cls
{
    unsigned int count = 0;
    objc_property_t *properties = class_copyPropertyList(cls, &count); //获取该类的所有属性
    for (int i = 0; i < count; i++) {
        
        objc_property_t property = properties[i];
        const char* key = property_getName(property);
        NSString* strKey = [NSString stringWithCString:key encoding:NSUTF8StringEncoding];
        id value = [self objectForKey:strKey];//从字典里按属性名获取value
        if (value == nil) {
            //对象属性名和JSON关键词key不一致的情况,获取属性名对应的key
            NSString *jsonKey = [self getObjCustomPropertyKey:obj properyName:strKey];
            if (jsonKey != nil) {
                value = [self objectForKey:jsonKey];//再次从字典里获取value
            }
        }
        
        Class propertyCls = [self getPropertyClass:property];//获取属性的类
        if (propertyCls == nil || [self isFoundationClass:propertyCls]) {
            if(value != nil && value != (id)kCFNull) {
                if (propertyCls == [NSURL class] && [value isKindOfClass:[NSString class]]) {
                    [obj setValue:[NSURL URLWithString:value] forKey:strKey];
                } else if (propertyCls == [NSArray class] && [value isKindOfClass:[NSArray class]]) {
                    //数组属性里的元素是自定义类型
                    Class cls = [self getObjArrayPropertyClass:obj properyName:strKey];
                    if (cls) {
                        [obj setValue:[cls objectArrayWithArray:value] forKey:strKey];
                    } else {
                        [obj setValue:value forKey:strKey];
                    }
                } else {
                    [obj setValue:value forKey:strKey];//通过KVC给对象的属性赋值
                }
            }
        } else {//自定义类型属性
            if(value != nil && [value isKindOfClass:[NSDictionary class]]) {
                id subObj = [[propertyCls alloc] init];
                [value setObject:subObj fromAllClass:propertyCls];
                [obj setValue:subObj forKey:strKey];
            }
        }
    }
    free(properties);
}

@end

步骤如下:

  1. 通过class_copyPropertyList获取类的属性列表
  2. 从字典里按属性名获取value
  3. 如果未获取到,则查看该属性名是否设置了对应的Key
  4. 如果设置了则再次从字典里获取value
  5. 获取属性的类型Class
  6. 判断属性是否是自定义的类型
  7. 如果是,那么递归调用该方法给该属性的属性赋值
  8. 如果是基础数据类型,则直接通过KVC给该属性赋值
  9. 如果是数组,则查看是否指定了该数据元素的类型
  10. 指定了则调用JSON数组转对象数组的方法

性能优化

主要优化了两个地方,一是获取属性类型的地方,而是访问扩展设置方法的地方。

- (Class)getPropertyClass:(objc_property_t)property
{
    unsigned int attrCount;
    objc_property_attribute_t *attrs = property_copyAttributeList(property, &attrCount);
    for (unsigned int i = 0; i < attrCount; i++) {
        const char *name = attrs[i].name;
        const char *value = attrs[i].value;
        if (name[0] == 'T') {
            if (strlen(value) > 2 && value[0] == '@' && value[1] == '\"') {
                char *p = strrchr(value + 2, '\"');
                if (p) {
                    *p = '\0';
                    free(attrs);
                    return objc_getClass(value + 2);
                }
            }
            break;
        }
    }
    free(attrs);
    return nil;
}

这里直接对char字符串操作,效率高很多,之前是先转成NSString对象的。关于属性的信息大家可以查这里Property Attribute Description Examples。 对象是T@开头,后面接着的就是类型。

还有一个优化是关于扩展设置方法的:

@protocol JSONEx <NSObject>
@optional
+ (NSDictionary<NSString *, NSString *> *)arrayPropertyItemClasses;//数组属性里元素的类型
+ (NSDictionary<NSString *, NSString *> *)customPropertyNameForKeys;//属性名和JSON关键词的映射
@end

每次都读取这两个方法很耗时。我参考YYModel的做法把这两个方法返回的设置一次性读到一个全局的静态字典里,再来访问这个静态变量,速度就快了。代码如下:

// 用静态字典存取类的customPropertyNameForKeys方法的值,以避免函数调用,提高访问速度
static NSDictionary* customPropertyKeyForClass(Class cls) {
    if (!cls || ![cls respondsToSelector:@selector(customPropertyNameForKeys)]) return nil;
    static CFMutableDictionaryRef cache;
    static dispatch_once_t onceToken;
    static dispatch_semaphore_t lock;
    dispatch_once(&onceToken, ^{
        cache = CFDictionaryCreateMutable(CFAllocatorGetDefault(), 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
        lock = dispatch_semaphore_create(1);
    });
    dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER);
    NSDictionary *dic = CFDictionaryGetValue(cache, (__bridge const void *)(cls));
    dispatch_semaphore_signal(lock);
    if (!dic) {
        dic = [(id<JSONEx>)cls customPropertyNameForKeys];
        if (dic) {
            dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER);
            CFDictionarySetValue(cache, (__bridge const void *)(cls), (__bridge const void *)(dic));
            dispatch_semaphore_signal(lock);
        }
    }
    return dic;
}

这里用GCD的信号量保证多线程情况下的静态字典的读写互斥。

最终的结果: 用YYModel里的用例,循环解析10000遍,在iPhone 7模拟器上,耗时大幅优于MJExtention。

JSONEx: 640.83ms
YYModel: 176.11ms
MJExtension: 2196.75ms