iOS端JSON轉(zhuǎn)Model鏈?zhǔn)骄幊炭蚣躍uperKVC用法與原理

背景

在客戶端編程中愉烙,字典轉(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)如下屯碴。

框架結(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é)請參考源碼劣坊,源碼地址在文章開頭。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末屈留,一起剝皮案震驚了整個(gè)濱河市局冰,隨后出現(xiàn)的幾起案子测蘑,更是在濱河造成了極大的恐慌,老刑警劉巖康二,帶你破解...
    沈念sama閱讀 221,635評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件碳胳,死亡現(xiàn)場離奇詭異,居然都是意外死亡沫勿,警方通過查閱死者的電腦和手機(jī)挨约,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,543評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來产雹,“玉大人诫惭,你說我怎么就攤上這事÷冢” “怎么了夕土?”我有些...
    開封第一講書人閱讀 168,083評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長瘟判。 經(jīng)常有香客問我隘弊,道長,這世上最難降的妖魔是什么荒适? 我笑而不...
    開封第一講書人閱讀 59,640評(píng)論 1 296
  • 正文 為了忘掉前任,我火速辦了婚禮开镣,結(jié)果婚禮上刀诬,老公的妹妹穿的比我還像新娘。我一直安慰自己邪财,他們只是感情好陕壹,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,640評(píng)論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著树埠,像睡著了一般糠馆。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上怎憋,一...
    開封第一講書人閱讀 52,262評(píng)論 1 308
  • 那天又碌,我揣著相機(jī)與錄音,去河邊找鬼绊袋。 笑死毕匀,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的癌别。 我是一名探鬼主播皂岔,決...
    沈念sama閱讀 40,833評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼展姐!你這毒婦竟也來了躁垛?” 一聲冷哼從身側(cè)響起剖毯,我...
    開封第一講書人閱讀 39,736評(píng)論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎教馆,沒想到半個(gè)月后逊谋,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,280評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡活玲,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,369評(píng)論 3 340
  • 正文 我和宋清朗相戀三年涣狗,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片舒憾。...
    茶點(diǎn)故事閱讀 40,503評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡镀钓,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出镀迂,到底是詐尸還是另有隱情丁溅,我是刑警寧澤,帶...
    沈念sama閱讀 36,185評(píng)論 5 350
  • 正文 年R本政府宣布探遵,位于F島的核電站窟赏,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏箱季。R本人自食惡果不足惜涯穷,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,870評(píng)論 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望藏雏。 院中可真熱鬧拷况,春花似錦、人聲如沸掘殴。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,340評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽奏寨。三九已至起意,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間病瞳,已是汗流浹背揽咕。 一陣腳步聲響...
    開封第一講書人閱讀 33,460評(píng)論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留仍源,地道東北人心褐。 一個(gè)月前我還...
    沈念sama閱讀 48,909評(píng)論 3 376
  • 正文 我出身青樓,卻偏偏與公主長得像笼踩,于是被迫代替她去往敵國和親逗爹。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,512評(píng)論 2 359

推薦閱讀更多精彩內(nèi)容

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn)掘而,斷路器挟冠,智...
    卡卡羅2017閱讀 134,699評(píng)論 18 139
  • 1. 簡介 1.1 什么是 MyBatis ? MyBatis 是支持定制化 SQL袍睡、存儲(chǔ)過程以及高級(jí)映射的優(yōu)秀的...
    笨鳥慢飛閱讀 5,527評(píng)論 0 4
  • 國家電網(wǎng)公司企業(yè)標(biāo)準(zhǔn)(Q/GDW)- 面向?qū)ο蟮挠秒娦畔?shù)據(jù)交換協(xié)議 - 報(bào)批稿:20170802 前言: 排版 ...
    庭說閱讀 11,004評(píng)論 6 13
  • Spring Boot 參考指南 介紹 轉(zhuǎn)載自:https://www.gitbook.com/book/qbgb...
    毛宇鵬閱讀 46,848評(píng)論 6 342
  • 有家長反映到自己的孩子平常學(xué)習(xí)挺努力認(rèn)真的斑胜,寫的作業(yè)和練習(xí)題以及課堂表現(xiàn)都很漂亮控淡,可是一到考試就打退堂鼓,緊張的手...
    孫老師說閱讀 1,520評(píng)論 0 0