作为App端开发,iOS和Android都不可避免要将服务器返回的数据转换为Model以便使用,而服务器返回的数据中又以JSON格式为最为常见。从最初的手动转换到各种JSON模型转换第三方库的出现和使用,我们既节省了大量时间,又提高了工作效率。先来看下常用的有哪几种,Mantle,JSONModel,YYModel等等,我们先来看下三者的使用方法、转换性能和各自的特点。
常用JSON模型转换库评测
JSON模型转换库的核心功能一般有JSON-->Model,Model->JSON,归档等,由于Model-->JSON实际应用情况不多,本次评测只针对了JSON-->Model和归档这两种情况。
常用库测试
begin = CACurrentMediaTime();
for (NSInteger i = 0; i < count; i++) {
// 模型转换
}
end = CACurrentMediaTime();
NSLog(@"JSON模型转换:%.2f", (end - begin) * 1000);
通过上面代码我们得出下表:
性能评测(毫秒)
模型转换 | JSON-->Modle | Archive |
---|---|---|
手动转换 | 145.94 | |
YYModel | 152.08 | 1396.63 |
JSONModel | 979.96 | 2006.00 |
Mantle | 2674.07 | 4737.81 |
在模型库的对比使用过程中,对于兼容性得出了下表 :
兼容性评测
JSON-->Model | YYModel | JSONModle | Mantle |
---|---|---|---|
number-->NSString | 正常 | 正常 | nil |
string-->NSInteger | 正常 | nil | nil |
string-->NSDate | 正常 | 正常 | nil |
属性名映射 | 支持 | 支持 | 支持(需要全写出) |
Archive | 支持 | 支持 | 支持 |
通过上面的表格我们大致可以得出下面的结论:
手动转换:
不用引入任何第三方库,性能最高,但是代码量大而繁琐,key一旦写错,这个key对应的属性转换出错。如果类型不符,需要我们手动去判断来容错,否则极易造成崩溃。比如:把number赋给了model的NSString,如果调用NSString的方法,就会造成崩溃。还有如果传回来的是空对象,在转换时会转成NSNull对象,调用时也会造成程序崩溃。
YYModel
作者郭耀源,国内iOS开发者,代表作品YYKit,非常赞的一个开源库,YYModel只是YYKit众多功能组件之一。可以自定义详细的JSON模型转换过程,性能极高,容错率较高。YYModel在转换时回做类型检测,如果类型不符,会对其进行尝试转换(比如string赋给了NSintger),尝试失败时才会将其置为nil。
JSONModel
早期广泛应用的一个JSON模型转换库,在性能上要略劣于YYModel,但是非常好用,使用简单,易于上手。缺点也比较明显,所有的model都需要继承于JSONModel这个基类。对于代码来说,这属于侵入式使用,对于使用者来说有许多不便,灵活性差。比如对瓜子C端的代码,我们有一个model的基类GJECNetworkResponseBaseEntity,我们会在这个model里边来转换一些返回数据的状态信息,比如code和对应的message,一般我们知道返回数据是否正常以及对应的信息。如果我们开发很久了再使用JSONModel,由于它的侵入性会使我们在使用上有很大局限性,代码更改量也是相当的大
Mantle
功能最丰富的一个JSON模型转换库,文档完善,使用也是相当广泛的,也是非常稳定的(转换的时候会做类型检测,类型不服的都会转换成nil,避免将错误的数据类型赋值给对应的属性,以避免潜在的crash问题)。我之前实习的公司中,遨游浏览器的APP开发就是使用的这个作为数据解析库。功能丰富了带来的缺点也是很明显,性能是这几个库中最差的,并且相当明显。转换过程中如果有类型不符的,会终止整个转换过程。Mantle也需要所有的model都要继承于MTLModel,并且实现MTLJSONSerializing协议,也具有侵入性,和JSONModel在灵活性上都是较差的。
总结一下:
JSON模型转换 | YYModel | JSONModle | Mantle |
---|---|---|---|
性能 | 高 | 较高 | 一般(较差) |
容错性 | 高 | 一般 | 较差 |
功能 | 简单,方便 | 简单,方便 | 丰富,可定制 |
侵入性 | 无,灵活性高 | 有,灵活性差 | 有,灵活性差 |
上手难易度 | 低 | 较低 | 较高 |
源码分析及项目使用
逻辑流程图
核心代码
YYModel虽然只有5个文件,但是逻辑严谨,功能高效,使用方便灵活。核心文件如下:
#import "NSObject+YYModel.h"
#import "NSObject+YYModel.m"
#import "YYClassInfo.h"
#import "YYClassInfo.m"
其中YYClassInfo中包含了YYClassIvarInfo,YYClassMethodInfo,YYClassPropertyInfo这3个内部类,YYClassInfo以字典集合的方式整合了以上三者的信息,达到了进一步封装的目的。
// 获取变量列表
class_copyIvarList(Class _Nullable cls, unsigned int * _Nullable outCount)
// 获取方法列表
class_copyMethodList(Class _Nullable cls, unsigned int * _Nullable outCount)
// 获取属性列表
class_copyPropertyList(Class _Nullable cls, unsigned int * _Nullable outCount)
通过上面三个方法获得模型类的变量列表,方法列表和属性列表,并把这些信息封装到YYClassInfo和对应的YYClass***Info。并且模型类的这些变量,方法和属性一旦有变化,就要做出相应的更新,提高了缓存信息的准确性。
+ (instancetype)classInfoWithClass:(Class)cls {
// 从容器中获取缓存信息
...
if (info && info->_needUpdate) {
[info _update];
}
dispatch_semaphore_signal(lock);
if (!info) {
info = [[YYClassInfo alloc] initWithClass:cls];
// 缓存信息到对应容器
...
}
return info;
}
在_update
方法中会使用class_copyIvarList,class_copyMethodList和class_copyPropertyList来更新模型类。
在NSObject+YYModel.m中有两个私有类:_YYModelMeta和_YYModelPropertyMeta:
- _YYModelMeta 用来存储模型类的信息(_classInfo),模型类所有属性对应的映射关系集合和_YYModelPropertyMeta信息等
@interface _YYModelMeta : NSObject {
@package
YYClassInfo *_classInfo;
/// Key:mapped key and key path, Value:_YYModelPropertyMeta.
NSDictionary *_mapper;
/// Array<_YYModelPropertyMeta>, all property meta of this model.
NSArray *_allPropertyMetas;
/// Array<_YYModelPropertyMeta>, property meta which is mapped to a key path.
NSArray *_keyPathPropertyMetas;
/// Array<_YYModelPropertyMeta>, property meta which is mapped to multi keys.
NSArray *_multiKeysPropertyMetas;
/// The number of mapped key (and key path), same to _mapper.count.
NSUInteger _keyMappedCount;
/// Model class type.
YYEncodingNSType _nsType;
BOOL _hasCustomWillTransformFromDictionary;
BOOL _hasCustomTransformFromDictionary;
BOOL _hasCustomTransformToDictionary;
BOOL _hasCustomClassFromDictionary;
}
@end
- _YYModelPropertyMeta 用来存储模型类的property的数据信息,包括属性name,type,class,getter和setter方法等信息。
@interface _YYModelPropertyMeta : NSObject {
@package
NSString *_name; ///< property's name
YYEncodingType _type; ///< property's type
YYEncodingNSType _nsType; ///< property's Foundation type
BOOL _isCNumber; ///< is c number type
Class _cls; ///< property's class, or nil
Class _genericCls; ///< container's generic class, or nil if threr's no generic class
SEL _getter; ///< getter, or nil if the instances cannot respond
SEL _setter; ///< setter, or nil if the instances cannot respond
BOOL _isKVCCompatible; ///< YES if it can access with key-value coding
BOOL _isStructAvailableForKeyedArchiver; ///< YES if the struct can encoded with keyed archiver/unarchiver
BOOL _hasCustomClassFromDictionary; ///< class/generic class implements +modelCustomClassForDictionary:
/*
property->key: _mappedToKey:key _mappedToKeyPath:nil _mappedToKeyArray:nil
property->keyPath: _mappedToKey:keyPath _mappedToKeyPath:keyPath(array) _mappedToKeyArray:nil
property->keys: _mappedToKey:keys[0] _mappedToKeyPath:nil/keyPath _mappedToKeyArray:keys(array)
*/
NSString *_mappedToKey; ///< the key mapped to
NSArray *_mappedToKeyPath; ///< the key path mapped to (nil if the name is not key path)
NSArray *_mappedToKeyArray; ///< the key(NSString) or keyPath(NSArray) array (nil if not mapped to multiple keys)
YYClassPropertyInfo *_info; ///< property's info
_YYModelPropertyMeta *_next; ///< next meta if there are multiple properties mapped to the same key.
}
@end
这两个内部类缓存了模型类的元数据信息,将其放在了一个静态字典中。我们在使用的时候只需要创建一次,其余的就直接使用缓存中的元数据信息就可以,提高了访问和转换效率。
YYModel使用yy_modelWithJSON
作为JSON模型转换的入口,将传入的对象转换成字典,真正来实现JSON模型转换功能的是yy_modelSetWithDictionary
。这个方法的内部实现如下:
- (BOOL)yy_modelSetWithDictionary:(NSDictionary *)dic {
...
// 初始化YYModelMeta
_YYModelMeta *modelMeta = [_YYModelMeta metaWithClass:object_getClass(self)];
...
// 遍历解析JSON,并完成Model赋值
if (modelMeta->_keyMappedCount >= CFDictionaryGetCount((CFDictionaryRef)dic)) {
CFDictionaryApplyFunction((CFDictionaryRef)dic, ModelSetWithDictionaryFunction, &context);
if (modelMeta->_keyPathPropertyMetas) {
CFArrayApplyFunction((CFArrayRef)modelMeta->_keyPathPropertyMetas,
CFRangeMake(0, CFArrayGetCount((CFArrayRef)modelMeta->_keyPathPropertyMetas)),
ModelSetWithPropertyMetaArrayFunction,
&context);
}
if (modelMeta->_multiKeysPropertyMetas) {
CFArrayApplyFunction((CFArrayRef)modelMeta->_multiKeysPropertyMetas,
CFRangeMake(0, CFArrayGetCount((CFArrayRef)modelMeta->_multiKeysPropertyMetas)),
ModelSetWithPropertyMetaArrayFunction,
&context);
}
} else {
CFArrayApplyFunction((CFArrayRef)modelMeta->_allPropertyMetas,
CFRangeMake(0, modelMeta->_keyMappedCount),
ModelSetWithPropertyMetaArrayFunction,
&context);
}
...
return YES;
}
上面真正起到遍历赋值的方法是CFDictionaryApplyFunction
和CFArrayApplyFunction
,通过这两个CoreFoundation
的方法来遍历传入的字典来给模型类赋值。这两个方法和OC自带的遍历方法相比,会带来不少性能上的提升,缺点就是写起来相当麻烦。这两个方法都有一个回调函数(ModelSetWithDictionaryFunction
和ModelSetWithPropertyMetaArrayFunction
),在遍历字典时,将字典的每一个value赋值给对应的模型类中的对应属性。
YYModel使用比较简单,只需要实现YYModel协议中的一些方法,就可以完成JSON模型转换。
@optional
// Custom property mapper.
+ (nullable NSDictionary<NSString *, id> *)modelCustomPropertyMapper;
// The generic class mapper for container properties.
+ (nullable NSDictionary<NSString *, id> *)modelContainerPropertyGenericClass;
// json-->objec的转换过程中,自定义objec的类别
+ (nullable Class)modelCustomClassForDictionary:(NSDictionary *)dictionary;
// 黑名单
+ (nullable NSArray<NSString *> *)modelPropertyBlacklist;
// 白名单
+ (nullable NSArray<NSString *> *)modelPropertyWhitelist;
// 在json-->modle之前可以在这个方法中修改json
- (NSDictionary *)modelCustomWillTransformFromDictionary:(NSDictionary *)dic;
// 在json-->modle之后可以在这个方法中根据json修改model
- (BOOL)modelCustomTransformFromDictionary:(NSDictionary *)dic;
// 在json-->modle之后可以在这个方法中根据model修改json
- (BOOL)modelCustomTransformToDictionary:(NSMutableDictionary *)dic;