背景介紹
在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