背景介紹
在iOS開發(fā)中,也不知道是誰(shuí)先起頭的,喜歡用Object-C的動(dòng)態(tài)特性。一個(gè)比較普遍的應(yīng)用是JSON解析之后的Dictionary轉(zhuǎn)自定義的Model,將類class轉(zhuǎn)換為struct,取出其中的成員屬性數(shù)組,然后用一個(gè)循環(huán),跟網(wǎng)絡(luò)收到的dictionary進(jìn)行對(duì)照,省去了model.property = dictionary[key]這些據(jù)說是沒有技術(shù)含量的體力活。感覺上是有點(diǎn)高大上了。
為此,gitHub上還出現(xiàn)了像Mantle這樣比較重的第三方庫(kù),當(dāng)然功能會(huì)很多,比如property和key的名字不必相同,可以復(fù)雜一點(diǎn)array套dictionary再套array,同時(shí)還實(shí)現(xiàn)NSCoding和NSCopying協(xié)議,方便使用序列化,和直接=號(hào)copy
這樣真的好嗎?其實(shí)這些都是Object-C可以和C和C++混編這種方便性給慣出來的毛病。在Object-C中混上C的語(yǔ)句,直接調(diào)用iOS底層的API就是技術(shù)高的體現(xiàn)?可以說是也可以說不是。
在Object-C中混入C,直接調(diào)用最底層的runtime,運(yùn)用了Object-C語(yǔ)言的本質(zhì),貌似應(yīng)該是“高深”的技術(shù),同時(shí)也體現(xiàn)了技術(shù)人員為實(shí)現(xiàn)產(chǎn)品人那些奇葩要求而進(jìn)行的不懈努力,從這個(gè)角度講,應(yīng)該是積極正面的。
從另一個(gè)角度講,這是非常壞的習(xí)慣。程序在實(shí)現(xiàn)功能之后,最重要的一條是“可讀性”,這一條怎么強(qiáng)調(diào)都不過分。僅僅為了程序員“偷懶”,或者炫耀所謂的“技術(shù)性”,將Object-C和C混編,搞得不倫不類,是非常愚蠢可笑的做法,這跟“郭美美炫富”的效果沒什么兩樣。從語(yǔ)言的美感,靈活性,效率等角度來說,至今還沒有哪一個(gè)語(yǔ)言能超越C和C++。那么為什么其他的語(yǔ)言,比如Object-C能很好的發(fā)展呢?就是為了限制靈活性,獲得良好的“可讀性”。在舒服地用著Object-C的同時(shí),嵌入底層的c代碼,這跟程序語(yǔ)言發(fā)展的大趨勢(shì)是相反的,這個(gè)除了“偷懶”和“炫技”以外,基本沒什么其他好處。這類人,在普通開發(fā)者來看,是“高手”;但是在真正的高手看來,只能是“半桶水”而已。
說了這么多,只是為了表明自己的觀點(diǎn),iOS開發(fā)只是應(yīng)用開發(fā),專心用UIKit,WebKit,HeathKit,HomeKit....各種上層API實(shí)現(xiàn)具體業(yè)務(wù)就好了,runtime什么的就讓它在底層默默發(fā)揮作用好了。如果是為了體現(xiàn)所謂的“技術(shù)含量”,那么去做C和C++相關(guān)的開發(fā),或者iOS中的“越獄”開發(fā),將會(huì)是更好的選擇。
當(dāng)前現(xiàn)狀
所謂形勢(shì)比人強(qiáng),現(xiàn)在不用點(diǎn)runtime都不好意思說自己是iOS開發(fā)的,從“軟件工程”的角度講,將這些惡心的代碼限制在一定范圍之內(nèi),比如在某個(gè)framework中,也算是一個(gè)既能堅(jiān)持自己想法,又不顯得過于落伍的折中方案。
當(dāng)前的工程選擇的是AutoCoding這個(gè)第三方庫(kù),以源文件方式放在shared_library文件夾中。這個(gè)庫(kù)只是實(shí)現(xiàn)了NSCoding和NSCopying協(xié)議,字典轉(zhuǎn)模型功能是通過NSObject的category來實(shí)現(xiàn)的,相當(dāng)于手寫。這個(gè)庫(kù)在github上star也只有800多,跟Mantle(9581)、JSONModel(5361)、MJExtention(5519)、YYModel(1846)、FastEasyMapping(419)等相比感覺沒什么優(yōu)勢(shì),不知道當(dāng)初選擇的理由是什么。
YYModel的作者寫一篇測(cè)試的軟文來講這個(gè)問題,感覺還是不錯(cuò)的。基本上我也認(rèn)同的他的觀點(diǎn)。Mantle、JSONModel用基類的方式,侵入性太強(qiáng),MJExtention源文件稍微多了一點(diǎn)。YYModel文件少,各方面表現(xiàn)都不錯(cuò),跟手寫也差不了多少。所以最終要用的話,就選YYModel,以源文件的方式集成在自己的工程中。
考慮的方面
功能角度
字典轉(zhuǎn)模型
實(shí)現(xiàn)NSCoding協(xié)議,方便做序列化(緩存)
實(shí)現(xiàn)NSCopying協(xié)議,能用=,用copy方法
兼容性角度
類型嵌套
組數(shù)嵌套
映射,也就是模型的屬性名和字典的key名稱不一致
容錯(cuò),屬性的類型和字典value的類型不一致
速度
侵入性,是否需要繼承指定的基類
其他考慮
framework是一個(gè)隔離帶,這個(gè)功能在framework外面實(shí)現(xiàn)還是在framework里面實(shí)現(xiàn)
這部分工作放framework外面由業(yè)務(wù)人員自己實(shí)現(xiàn)還是放在framework內(nèi)部有框架人員實(shí)現(xiàn)?
如果在framework內(nèi)部實(shí)現(xiàn),如何處理內(nèi)外部類型傳遞的問題。在Object-C中,class類型可以傳遞,但是class的實(shí)現(xiàn)無法在framework內(nèi)部替外部代勞。
是否提供基類?
討論后的方案
將網(wǎng)絡(luò)返回的response或者error原樣傳遞出去,讓framework外部的調(diào)用者有機(jī)會(huì)自己處理
提供類型參數(shù),能字典轉(zhuǎn)模型的就轉(zhuǎn)了吧
提供自定義的基類作為接口,實(shí)現(xiàn)NSCoding和NSCopying協(xié)議,方便外部使用
手寫實(shí)現(xiàn)這些功能比較麻煩,暫時(shí)不采用
第三方庫(kù)采用YYModel,這樣就可以不對(duì)framework外部暴露第三方庫(kù)頭文件。Mantle雖然很強(qiáng),對(duì)于速度的劣勢(shì)也可以忍受,但是必須繼承基類的使用方式在framework的場(chǎng)景下實(shí)在不合適。
現(xiàn)有代碼
現(xiàn)有代碼基本上是手寫實(shí)現(xiàn)了這些功能,跟第三方庫(kù)相比還有差距,但是也有足夠的學(xué)習(xí)意義。
NSCoding, NSCopying協(xié)議實(shí)現(xiàn)
#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
字典轉(zhuǎn)模型
這部分相對(duì)比較復(fù)雜,不一定能完全看懂,能了解核心原理就可以了。
+ (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–國(guó)外程序員最常用的iOS模型&字典轉(zhuǎn)換框架
iOS JSON 模型轉(zhuǎn)換庫(kù)評(píng)測(cè)
Mantle
YYModel
MJExtension
AutoCoding