背景介绍
在iOS开发中,也不知道是谁先起头的,喜欢用Object-C的动态特性。一个比较普遍的应用是JSON解析之后的Dictionary转自定义的Model,将类class转换为struct,取出其中的成员属性数组,然后用一个循环,跟网络收到的dictionary进行对照,省去了model.property = dictionary[key]这些据说是没有技术含量的体力活。感觉上是有点高大上了。
为此,gitHub上还出现了像Mantle这样比较重的第三方库,当然功能会很多,比如property和key的名字不必相同,可以复杂一点array套dictionary再套array,同时还实现NSCoding和NSCopying协议,方便使用序列化,和直接=号copy
这样真的好吗?其实这些都是Object-C可以和C和C++混编这种方便性给惯出来的毛病。在Object-C中混上C的语句,直接调用iOS底层的API就是技术高的体现?可以说是也可以说不是。
在Object-C中混入C,直接调用最底层的runtime,运用了Object-C语言的本质,貌似应该是“高深”的技术,同时也体现了技术人员为实现产品人那些奇葩要求而进行的不懈努力,从这个角度讲,应该是积极正面的。
从另一个角度讲,这是非常坏的习惯。程序在实现功能之后,最重要的一条是“可读性”,这一条怎么强调都不过分。仅仅为了程序员“偷懒”,或者炫耀所谓的“技术性”,将Object-C和C混编,搞得不伦不类,是非常愚蠢可笑的做法,这跟“郭美美炫富”的效果没什么两样。从语言的美感,灵活性,效率等角度来说,至今还没有哪一个语言能超越C和C++。那么为什么其他的语言,比如Object-C能很好的发展呢?就是为了限制灵活性,获得良好的“可读性”。在舒服地用着Object-C的同时,嵌入底层的c代码,这跟程序语言发展的大趋势是相反的,这个除了“偷懒”和“炫技”以外,基本没什么其他好处。这类人,在普通开发者来看,是“高手”;但是在真正的高手看来,只能是“半桶水”而已。
说了这么多,只是为了表明自己的观点,iOS开发只是应用开发,专心用UIKit,WebKit,HeathKit,HomeKit....各种上层API实现具体业务就好了,runtime什么的就让它在底层默默发挥作用好了。如果是为了体现所谓的“技术含量”,那么去做C和C++相关的开发,或者iOS中的“越狱”开发,将会是更好的选择。
当前现状
所谓形势比人强,现在不用点runtime都不好意思说自己是iOS开发的,从“软件工程”的角度讲,将这些恶心的代码限制在一定范围之内,比如在某个framework中,也算是一个既能坚持自己想法,又不显得过于落伍的折中方案。
当前的工程选择的是AutoCoding这个第三方库,以源文件方式放在shared_library文件夹中。这个库只是实现了NSCoding和NSCopying协议,字典转模型功能是通过NSObject的category来实现的,相当于手写。这个库在github上star也只有800多,跟Mantle(9581)、JSONModel(5361)、MJExtention(5519)、YYModel(1846)、FastEasyMapping(419)等相比感觉没什么优势,不知道当初选择的理由是什么。
YYModel的作者写一篇测试的软文来讲这个问题,感觉还是不错的。基本上我也认同的他的观点。Mantle、JSONModel用基类的方式,侵入性太强,MJExtention源文件稍微多了一点。YYModel文件少,各方面表现都不错,跟手写也差不了多少。所以最终要用的话,就选YYModel,以源文件的方式集成在自己的工程中。
考虑的方面
功能角度
字典转模型
实现NSCoding协议,方便做序列化(缓存)
实现NSCopying协议,能用=,用copy方法
兼容性角度
类型嵌套
组数嵌套
映射,也就是模型的属性名和字典的key名称不一致
容错,属性的类型和字典value的类型不一致
速度
侵入性,是否需要继承指定的基类
其他考虑
framework是一个隔离带,这个功能在framework外面实现还是在framework里面实现
这部分工作放framework外面由业务人员自己实现还是放在framework内部有框架人员实现?
如果在framework内部实现,如何处理内外部类型传递的问题。在Object-C中,class类型可以传递,但是class的实现无法在framework内部替外部代劳。
是否提供基类?
讨论后的方案
将网络返回的response或者error原样传递出去,让framework外部的调用者有机会自己处理
提供类型参数,能字典转模型的就转了吧
提供自定义的基类作为接口,实现NSCoding和NSCopying协议,方便外部使用
手写实现这些功能比较麻烦,暂时不采用
第三方库采用YYModel,这样就可以不对framework外部暴露第三方库头文件。Mantle虽然很强,对于速度的劣势也可以忍受,但是必须继承基类的使用方式在framework的场景下实在不合适。
现有代码
现有代码基本上是手写实现了这些功能,跟第三方库相比还有差距,但是也有足够的学习意义。
NSCoding, NSCopying协议实现
#import <Foundation/Foundation.h>
@interface BaseDataModel : NSObject<NSCoding, NSCopying>
@end
#import "BaseDataModel.h"
#import <objc/runtime.h>
@implementation BaseDataModel
- (void)encodeWithCoder:(NSCoder *)aCoder
{
unsigned int outCount, i;
objc_property_t *properties = class_copyPropertyList([self class], &outCount);
for (i = 0; i < outCount; i++) {
objc_property_t property = properties[i];
NSString *key = [NSString stringWithFormat:@"%s",property_getName(property)];
id value = [self valueForKey:key];
if(!value)continue;
[aCoder encodeObject:value forKey:[NSString stringWithFormat:@"%@",key]];
}
free (properties);
}
- (id)initWithCoder:(NSCoder *)aDecoder
{
self = [super init];
if(self)
{
unsigned int outCount, i;
objc_property_t *properties = class_copyPropertyList([self class], &outCount);
for (i = 0; i < outCount; i++) {
objc_property_t property = properties[i];
NSString *key = [NSString stringWithFormat:@"%s",property_getName(property)];
id value = [aDecoder decodeObjectForKey:key];
if (!value)continue;
[self setValue:value forKey:key];
}
free (properties);
}
return self;
}
- (id)copyWithZone:(NSZone *)zone
{
id copyObject = [[[self class] allocWithZone:zone] init];
unsigned int outCount, i;
objc_property_t *properties = class_copyPropertyList([self class], &outCount);
for (i = 0; i < outCount; i++) {
objc_property_t property = properties[i];
NSString *key = [NSString stringWithFormat:@"%s",property_getName(property)];
id value = [self valueForKey:key];
if (!value)continue;
[copyObject setValue:value forKey:key];
}
free (properties);
return copyObject;
}
@end
字典转模型
这部分相对比较复杂,不一定能完全看懂,能了解核心原理就可以了。
+ (id)ac_objectsWithArray:(id)array objectClass:(__unsafe_unretained Class)clazz
{
NSMutableArray * objects = [NSMutableArray array];
for ( NSDictionary * obj in array )
{
if ( [obj isKindOfClass:[NSDictionary class]] )
{
id convertedObj = [clazz ac_objectWithDictionary:obj];
if ( convertedObj ) {
[objects addObject:convertedObj];
}
}
else
{
[objects addObject:obj];
}
}
return [objects copy];
}
+ (instancetype)ac_objectWithDictionary:(NSDictionary *)dictionary
{
id object = [[self alloc] init];
NSDictionary * properties = [object codableProperties];
for ( __unsafe_unretained NSString *property in properties )
{
id value = dictionary[property];
Class clazz = properties[property][@"class"];
Class subClazz = properties[property][@"subclass"];
if ( value )
{
id convertedValue = value;
if ( [value isKindOfClass:[NSArray class]] )
{
if ( subClazz != NSNull.null ) {
convertedValue = [NSObject ac_objectsWithArray:value objectClass:subClazz];
}
// TODO: handle else
}
else if ( [value isKindOfClass:[NSDictionary class]] )
{
convertedValue = [clazz ac_objectWithDictionary:value];
}
if ( convertedValue && ![convertedValue isKindOfClass:[NSNull class]] )
{
if ( [self conformsToProtocol:@protocol(AutoModelCoding)] )
{
convertedValue = [(id<AutoModelCoding>)self processedValueForKey:property originValue:value convertedValue:convertedValue class:clazz subClass:subClazz];
}
[object setValue:convertedValue forKey:property];
if ( ![convertedValue isKindOfClass:clazz] )
{
// @"Expected '%@' to be a %@, but was actually a %@"
NSLog( @"The type of '%@' in <%@> is <%@>, but not compatible with expected <%@>, please see detail in the <AutoModelCoding> protocol.", property, [self class], [value class], clazz );
}
}
}
}
return object;
}
- (NSDictionary *)codableProperties
{
__autoreleasing NSDictionary *codableProperties = objc_getAssociatedObject([self class], _cmd);
if (!codableProperties)
{
codableProperties = [NSMutableDictionary dictionary];
Class subclass = [self class];
while (subclass != [NSObject class])
{
[(NSMutableDictionary *)codableProperties addEntriesFromDictionary:[subclass codableProperties]];
subclass = [subclass superclass];
}
codableProperties = [NSDictionary dictionaryWithDictionary:codableProperties];
//make the association atomically so that we don't need to bother with an @synchronize
objc_setAssociatedObject([self class], _cmd, codableProperties, OBJC_ASSOCIATION_RETAIN);
}
return codableProperties;
}
@protocol AutoModelCoding <NSObject>
+ (id)processedValueForKey:(NSString *)key
originValue:(id)originValue
convertedValue:(id)convertedValue
class:(__unsafe_unretained Class)clazz
subClass:(__unsafe_unretained Class)subClazz;
@end
参考文档
Mantle–国外程序员最常用的iOS模型&字典转换框架
iOS JSON 模型转换库评测
Mantle
YYModel
MJExtension
AutoCoding