背景
在客戶端編程中愉烙,字典轉(zhuǎn)模型是一個(gè)極為常見的問題,蘋果提供了KVC來實(shí)現(xiàn)NSDictionary到Model的注入,但是KVC只能進(jìn)行單層淺注入尾膊,且無法處理類型轉(zhuǎn)換、key與屬性名不對應(yīng)荞彼、深度注入等問題冈敛,筆者從Masonry得到啟發(fā),開發(fā)了一個(gè)通過鏈?zhǔn)脚渲米⑷肫鲗?shí)現(xiàn)深度注入鸣皂、類型轉(zhuǎn)換抓谴、key-屬性名映射等功能的輕量級(jí)注入框架SuperKVC暮蹂。目前已經(jīng)開源到GitHub,點(diǎn)擊這里前往齐邦,歡迎Star和Fork椎侠,歡迎和我一起完善這個(gè)框架!
本文將從應(yīng)用和原理兩個(gè)角度介紹SuperKVC
用法
該框架與Masonry類似措拇,要求用戶在相關(guān)方法參數(shù)的block中通過鏈?zhǔn)骄幊痰姆椒ㄅ渲米⑷肫鲄?shù),例如配置要?jiǎng)?chuàng)建和注入的class慎宾、類型轉(zhuǎn)換器丐吓、名稱映射等,然后方法會(huì)返回注入后的結(jié)果趟据。如果JSON的根元素為字典券犁,則返回一個(gè)模型實(shí)例;如果JSON的根元素為數(shù)組汹碱,則返回一個(gè)模型數(shù)組粘衬。
淺注入示例
假設(shè)我們有如下的JSON,要注入到UserModel中咳促。
{
"id": 100075,
"name": "Greedy",
"birthday": "1993-03-06",
"isVip": true,
"partners": [100236, 100244, 100083]
}
@interface UserModel : NSObject
@property (nonatomic, assign) int64_t userId;
@property (nonatomic, copy) NSString *name;
@property (nonatomic, strong) NSDate *birthday;
@property (nonatomic, assign) BOOL isVip;
@property (nonatomic, strong) NSArray *partners;
@end
注意到這里的幾個(gè)“坑點(diǎn)”稚新,響應(yīng)的id
對應(yīng)的是模型的userId
,響應(yīng)的birthday
類型為NSString
跪腹,而模型的birthday
類型為NSDate
褂删,使用SuperKVC進(jìn)行注入的代碼如下。
// responseObject is a JSONObject(NSDictionary).
UserModel *userModel = [responseObject sk_injectWithInjector:^(SuperKVCInjector *injector) {
// 配置要生成和注入的模型實(shí)例類型
injector.bind([UserModel class]);
// 配置key到屬性名的映射
injector.mapping(@"id").to(@"userId");
// 配置類型轉(zhuǎn)換
injector.format(@"birthday").with.converter(^NSDate* (NSString *birthdayString) {
NSDateFormatter *fmt = [NSDateFormatter new];
fmt.dateFormat = @"yyyy-MM-dd";
return [fmt dateFromString:birthdayString];
});
}];
可以看到冲茸,SuperKVC的使用方式與Masonry十分類似屯阀,通過鏈?zhǔn)骄幊痰姆绞脚渲米⑷肫鳎哂休^高的可讀性轴术,且使用方便难衰,不需要手動(dòng)創(chuàng)建模型和從字典中取值和對模型賦值。
數(shù)組注入示例
SuperKVC還能夠自動(dòng)處理字典數(shù)組到模型數(shù)組的轉(zhuǎn)換逗栽,對于下面的JSON盖袭。
[{
"id": 100075,
"name": "Greedy",
"birthday": "1993-03-06",
"isVip": true,
"partners": [100236, 100244, 100083]
},
{
"id": 100724,
"name": "Charlie",
"birthday": "1996-08-12",
"isVip": false,
"partners": [100710, 100715]
},{},]
按照上面的配置調(diào)用注入器,即可得到UserModel的模型數(shù)組祭陷。
// responseObject is a JSONObject(NSArray).
NSArray<UserModel *> *userModels = [responseObject sk_injectWithInjector:^(SuperKVCInjector *injector) {
injector.bind([UserModel class]);
injector.mapping(@"id").to(@"userId");
injector.format(@"birthday").with.converter(^NSDate* (NSString *birthdayString) {
NSDateFormatter *fmt = [NSDateFormatter new];
fmt.dateFormat = @"yyyy-MM-dd";
return [fmt dateFromString:birthdayString];
});
}];
深度注入示例
對于模型中包含模型的情況苍凛,通過converter來嵌套調(diào)用注入器,例如下面的JSON兵志。
[{
"id": 100075,
"name": "Greedy",
"birthday": "1993-03-06",
"isVip": false,
"cards": [
{
"id": 400820666,
"name": "King Card of Unity",
"expire": "2026-03-27"
},
{
"id": 622800333,
"name": "Silver Card of Glory",
"expire": "2029-02-21"
},
{
"id": 623400765,
"name": "King Card of Floyt",
"expire": "2024-08-15"
}
]
},{},]
@interface UserModel : NSObject
@property (nonatomic, assign) int64_t userId;
@property (nonatomic, copy) NSString *name;
@property (nonatomic, strong) NSDate *birthday;
@property (nonatomic, assign) BOOL isVip;
@property (nonatomic, strong) NSArray<CardModel *> *cards;
@end
@interface CardModel : NSObject
@property (nonatomic, assign) int64_t cardId;
@property (nonatomic, copy) NSString *name;
@property (nonatomic, strong) NSDate *expireDate;
@end
這里的關(guān)鍵問題是如何配置UserModel的cards屬性使得注入的Card字典數(shù)組被自動(dòng)轉(zhuǎn)換為CardModel數(shù)組醇蝴,我們需要借助于converter,對cards對應(yīng)的字典數(shù)組進(jìn)行格式轉(zhuǎn)換想罕,代碼如下悠栓。
NSArray *userArray = [responseObject sk_injectWithInjector:^(SuperKVCInjector *injector) {
injector.bind([UserModel class]);
injector.mapping(@"id").to(@"userId");
injector.format(@"birthday").with.converter(^NSDate* (NSString *birthdayString) {
return [fmt dateFromString:birthdayString];
});
injector.format(@"cards").with.converter(^CardModel* (NSDictionary *cardDictArray) {
return [cardDictArray sk_dequeInjectorForClass:[CardModel class] emptyHandler:^(SuperKVCInjector *injector) {
injector.bind([CardModel class]);
injector.mapping(@"id").to(@"cardId");
injector.mapping(@"expire").to(@"expireDate");
injector.format(@"expireDate").with.converter(^NSDate* (NSString *birthdayString) {
return [fmt dateFromString:birthdayString];
});
}];
});
}];
上面幾行都是常規(guī)配置霉涨,注意cards處理方式,借助converter惭适,嵌套的調(diào)用injector來實(shí)現(xiàn)內(nèi)層的注入笙瑟,這里同樣涉及到名稱映射和日期格式轉(zhuǎn)換,注意到內(nèi)層的injector使用了deque開頭的方法癞志,這是為了避免重復(fù)創(chuàng)建相同的注入器而采用的復(fù)用機(jī)制往枷。
原理
SuperKVC的注入基于反射和KVC實(shí)現(xiàn)的,反射是為了獲取屬性列表和填充實(shí)例變量凄杯,KVC是為了處理基本類型在運(yùn)行時(shí)對實(shí)例變量的填充错洁,鏈?zhǔn)脚渲猛ㄟ^block實(shí)現(xiàn),注入器和方法緩存通過LRU(NSCache)實(shí)現(xiàn)戒突,整個(gè)框架的結(jié)構(gòu)如下屯碴。
其中直接暴露給用戶的只有NSObject的Category、SuperKVCInjector和SKVManager膊存,Category中包含了執(zhí)行注入的兩個(gè)方法导而。
@interface NSObject (SuperKVC)
- (id)sk_injectWithInjector:(void(^)(SuperKVCInjector *injector))block;
- (id)sk_dequeInjectorForClass:(Class)clazz emptyHandler:(void(^)(SuperKVCInjector *injector))block;
@end
第一個(gè)方法用于常規(guī)注入,每次調(diào)用都會(huì)創(chuàng)建一個(gè)全新的注入器隔崎;第二個(gè)方法包含了注入器復(fù)用的邏輯今艺,通常用于嵌套調(diào)用時(shí)提高執(zhí)行效率。由于第二個(gè)方法只是對注入器的緩存邏輯仍稀,不涉及核心算法洼滚,因此這里不再贅述,我們來看第一個(gè)方法的內(nèi)部實(shí)現(xiàn)技潘。
- (id)sk_injectWithInjector:(void (^)(SuperKVCInjector *))block {
SuperKVCInjector *injector = [SuperKVCInjector new];
block(injector);
return [self parseAttributesForInjector:injector];
}
方法內(nèi)部先是創(chuàng)建了一個(gè)注入器遥巴,然后調(diào)用外部block要求用戶配置注入器,最后使用注入器來處理注入邏輯享幽,這里的關(guān)鍵是注入器的配置铲掐。根據(jù)前面的描述,我們知道要配置的內(nèi)容主要包括下列內(nèi)容值桩。
名稱 | 功能 | 使用 |
---|---|---|
bind | 配置要生成和注入的模型類 | injector.bind(Class ); |
mapping | 配置字典key到模型屬性名的映射 | injector.mapping(responseKey ).to(propertyName ); |
format | 自主格式化一個(gè)屬性的值 | injector.format(propertyName ).with.converter(^id (id oldVar) { /* your format code */ return newVar ; }); |
ignore | 忽略一些模型屬性的注入 | injector.ignore(propName1 ).and(propName2 ) ... |
synthesize | 如果通過@synthesize配置了新的存取規(guī)則摆霉,需要手動(dòng)指定屬性對應(yīng)的實(shí)例變量名稱 | injector.synthesize(propertyName ).to(ivarName ); |
這幾類屬性的實(shí)現(xiàn)比較類似,下面講解bind和mapping的實(shí)現(xiàn)奔坟。
bind的用法為傳入一個(gè)class携栋,并且要求使用小括號(hào)調(diào)用,為了實(shí)現(xiàn)這種效果咳秉,要把bind的調(diào)用轉(zhuǎn)為函數(shù)調(diào)用而不能是方法調(diào)用婉支,并且利用OC中返回值非空的無參方法可以用點(diǎn)語法調(diào)用,因此讓bind方法無參澜建,但是返回一個(gè)有參block向挖,這個(gè)block接收bind的參數(shù)即可蝌以,由于bind之后不需要連接其他操作,因此block的返回值為空何之,bind方法的實(shí)現(xiàn)如下跟畅。
- (void(^)(Class clazz))bind {
return ^(Class clazz) {
SKVBindAttribute *attr = [[SKVBindAttribute alloc] initWithBindClass:clazz];
[self.attributes addObject:attr];
};
}
可以看到,調(diào)用bind方法的實(shí)質(zhì)是返回了一個(gè)block溶推,這個(gè)block接收一個(gè)Class參數(shù)徊件,并且沒有返回值,block內(nèi)創(chuàng)建了一個(gè)SKVBindAttribute悼潭,并且存儲(chǔ)Class信息庇忌,最后將這個(gè)屬性存儲(chǔ)到注入器的屬性列表中。注入器的屬性列表用來存儲(chǔ)所有配置信息舰褪,每個(gè)信息都是SKVAttribute的一個(gè)子類,每個(gè)子類對應(yīng)一行鏈?zhǔn)讲僮魇栝希脕泶鎯?chǔ)一行的完整上下文占拍。對于bind操作,由于只接收一個(gè)Class參數(shù)捎迫,因此處理較為簡單晃酒。
下面我們看一下mapping這個(gè)接收兩個(gè)參數(shù)的操作,與bind類似窄绒,mapping也是一個(gè)無參方法贝次,但是由于有后續(xù)操作,mapping方法返回的block的返回值不能為空彰导,而是一個(gè)mapping時(shí)創(chuàng)建的SVKMappingAttribute蛔翅,這個(gè)attribute已經(jīng)存儲(chǔ)了mapping傳入的參數(shù),并沿著調(diào)用鏈繼續(xù)向后傳遞位谋,具體代碼如下山析。
- (SKVMappingAttribute *(^)(NSString *))mapping {
return ^SKVMappingAttribute* (NSString *responseKey) {
SKVMappingAttribute *attr = [SKVMappingAttribute new];
attr.responseKey = responseKey;
[self.attributes addObject:attr];
return attr;
};
}
經(jīng)過mapping操作之后,返回的是已經(jīng)存儲(chǔ)了響應(yīng)key的SKVMappingAttribute實(shí)例掏父,通過調(diào)用實(shí)例的to方法繼續(xù)處理要映射到的屬性key的名稱笋轨,SKVMappingAttribute的結(jié)構(gòu)如下。
@interface SKVMappingAttribute : SKVAttribute
@property (nonatomic, strong) NSString *responseKey;
@property (nonatomic, strong) NSString *modelKey;
- (void(^)(NSString *modelKey))to;
@end
在前面的調(diào)用中赊淑,已經(jīng)設(shè)置了responseKey爵政,并且返回了attribute實(shí)例,這時(shí)候可以繼續(xù)調(diào)用to方法陶缺,設(shè)置modelKey钾挟,從而完成mapping上下文的存儲(chǔ)。
- (void (^)(NSString *))to {
return ^ (NSString *to) {
self.modelKey = to;
};
}
其他操作都是類似的组哩,這里不再贅述等龙,如有興趣可去看源碼处渣。經(jīng)過所有配置之后,接下來要進(jìn)行的是屬性處理蛛砰,首先將屬性排序罐栈,排序的優(yōu)先級(jí)通過priority確定,其中bind的優(yōu)先級(jí)最高泥畅,經(jīng)過排序后荠诬,將這些配置依次寫入注入器,為反射之前做準(zhǔn)備位仁,注入器存儲(chǔ)這些配置的數(shù)據(jù)結(jié)構(gòu)如下柑贞。
@interface SuperKVCInjector ()
@property (nonatomic, assign) Class bindClass;
@property (nonatomic, strong) NSMutableDictionary *mappingDict;
@property (nonatomic, strong) NSMutableDictionary *synthesizeDict;
@property (nonatomic, strong) NSMutableSet *ignoreSet;
@property (nonatomic, strong) NSMutableDictionary<NSString *, SKVFormatAttribute *> *formatDict;
@end
在遍歷的屬性中會(huì)構(gòu)建這些集合結(jié)構(gòu),用于反射時(shí)查詢和處理聂抢,在經(jīng)過這些操作之后钧嘶,就可以生成模型開始注入了,這時(shí)還要區(qū)分要處理的JSONObject是數(shù)組還是字典琳疏,如果是數(shù)組有决,需要遍歷每一個(gè)元素分別處理模型注入,最后生成一個(gè)全新的模型數(shù)組空盼;如果是字典书幕,則直接處理接下來的注入。
- (id)buildAndInjectWithClass:(Class)clazz forInjector:(SuperKVCInjector *)injector {
id ret = nil;
if ([self isKindOfClass:[NSArray class]]) {
ret = @[].mutableCopy;
NSArray *dicts = (NSArray *)self;
for (NSDictionary *dict in dicts) {
id model = [self buildAndInjectModelWithClass:clazz dict:dict forInjector:injector];
[ret addObject:model];
}
} else if ([self isKindOfClass:[NSDictionary class]]) {
ret = [self buildAndInjectModelWithClass:clazz dict:(NSDictionary *)self forInjector:injector];
}
return ret;
}
處理單個(gè)模型注入的方法較為復(fù)雜揽趾,這里截取核心部分台汇,主要是對不同配置的查詢和處理,下面的代碼是其中使用的一個(gè)處理單個(gè)屬性注入的代碼塊篱瞎。
BOOL (^handlePropNamed)(id model, NSString *propName) = ^BOOL (id model, NSString *propName) {
// 一般情況下苟呐,字典的key與屬性名相同
NSString *responseName = propName;
// 查詢映射表,如果不為空奔缠,則按照映射表獲取屬性名對應(yīng)的響應(yīng)名
if (injector.mappingDict[propName] != nil) {
responseName = injector.mappingDict[propName];
}
// 根據(jù)屬性名獲取實(shí)例變量名掠抬,一般情況下為_屬性名
NSString *ivarName = [NSString stringWithFormat:@"_%@",propName];
// 如果使用了@synthesize,則需要查表獲得實(shí)例變量名
if (injector.hasSynthesize && injector.synthesizeDict[propName]) {
ivarName = injector.synthesizeDict[propName];
}
Ivar ivar = class_getInstanceVariable(clazz, ivarName.UTF8String);
if (ivar == NULL) {
return NO;
}
id value = dict[responseName];
// 對于NSNull校哎,不處理注入两波,否則對模型的操作容易引起崩潰
if ([value isKindOfClass:[NSNull class]]) {
return YES;
}
// 查詢自定義格式轉(zhuǎn)換表,如果有自定義格式轉(zhuǎn)換闷哆,則調(diào)用格式轉(zhuǎn)換的block來轉(zhuǎn)換格式
if (injector.formatDict[propName] != nil) {
SKVFormatAttribute *formatAttr = injector.formatDict[propName];
value = formatAttr.converterBlock(value);
}
// 對于基本類型腰奋,需要借助KVC來注入
if ([value isKindOfClass:[NSValue class]]) {
[model setValue:value forKey:propName];
} else {
// 對于對象類型,通過runtime直接注入
object_setIvar(model, ivar, value);
}
return YES;
};
以上就是框架的主要邏輯抱怔,具體處理細(xì)節(jié)請參考源碼劣坊,源碼地址在文章開頭。