【瘋狂造輪子-iOS】JSON轉Model系列之一 本文轉載請註明出處 —— polobymulberry-博客園 1. 前言 之前一直看別人的源碼,雖然對自己提升比較大,但畢竟不是自己寫的,很容易遺忘。這段時間準備自己造一些輪子,主要目的還是為了提升自身實力,總不能一遇到問題就Google。 之前 ...
【瘋狂造輪子-iOS】JSON轉Model系列之一
本文轉載請註明出處 —— polobymulberry-博客園
1. 前言
之前一直看別人的源碼,雖然對自己提升比較大,但畢竟不是自己寫的,很容易遺忘。這段時間準備自己造一些輪子,主要目的還是為了提升自身實力,總不能一遇到問題就Google。
之前寫i博客園客戶端的時候,經常會遇到JSON數據轉Model的功能。一般遇到這種問題我都是自己在對應Model類中定義一個+ (instance)initWithAttributes:(NSDictionary *)attributes函數來將NSDictionary*數據轉化為對應Model。
下麵是i博客園中ICUser的部分代碼,其中就使用了initWithAttributes。
// ICUser.h #import <Foundation/Foundation.h> extern NSString *const kUserId; extern NSString *const kUserBlogId; extern NSString *const kUserDisplayName; extern NSString *const kUserAvatarURL; @interface ICUser : NSObject @property (nonatomic, copy) NSString *userId; @property (nonatomic, assign) NSInteger blogId; @property (nonatomic, copy) NSString *displayName; @property (nonatomic, strong) NSURL *avatarURL;+ (instancetype)initWithAttributes:(NSDictionary *
)attributes; @end // ICUser.m #import "ICUser.h" NSString *const kUserId = @"UserId"; NSString *const kUserBlogId = @"BlogId"; NSString *const kUserDisplayName = @"DisplayName"; NSString *const kUserAvatarURL = @"Avatar"; @implementation ICUser+ (instancetype)initWithAttributes:(NSDictionary *
)attributes { ICUser *user = [[ICUser alloc] init];
user.userId = attributes[kUserId]; user.blogId = [attributes[kUserBlogId] integerValue]; user.displayName = attributes[kUserDisplayName]; user.avatarURL = [NSURL URLWithString:attributes[kUserAvatarURL]]; return user; } @end
如果我們需要處理的情況符合下麵兩個要求:
- Model類的個數比較少
- 每個Model的成員不是很複雜
這種情況下使用上述方法還可以接受。但是一旦Model這一層急劇膨脹,這時候就會讓人苦不堪言:
- initWithAttributes函數容易寫錯,而且出錯後不方便排查。
- 機械性的代碼會比較多,不利於提高效率。
考慮到手動轉JSON為Model的種種不便,我決定自己寫一個JSON轉Model的庫。雖然網上已經有很多這方面的第三方庫,但是我還是想自己造輪子,目的是為了更深入地學習iOS。
2. 設計思路
1.首先要考慮到輸入輸出是什麼?
輸入:NSDictionary類型的數據
這裡我們先從簡,一般我們使用到解析JSON的場合是在網路請求。伺服器端返回JSON格式的數據,我們需要轉化成本地的Model(此處不討論直接使用NSDictionary好還是轉化為Model好)。並且本篇文章只假設我們網路請求獲取到的JSON數據已經在客戶端處理成了NSDictionary類型的數據(比較常見)。
輸出:Model類型的數據
Model類型的數據。
舉例:
目前我實現的一個簡單的例子:
#pragma mark - PJXUser @interface PJXUser : NSObject @property (nonatomic, copy) NSString* username; // 用戶名 @property (nonatomic, copy) NSString* password; // 密碼 @property (nonatomic, copy) NSString* avatarImageURL; // 頭像的URL地址 @end - (void)runSimpleSample { NSDictionary *userDict = @{@"username" :@"shuaige", @"password" :@"123456", @"avatarImageURL":@"http://www.example.com/shuaige.png"}; PJXUser *user = [[PJXUser alloc] initWithAttributes:userDict];; NSLog(@"username:%@\n",user.username); NSLog(@"password:%@\n",user.password); NSLog(@"avatarImageURL:%@\n",user.avatarImageURL); }
這個例子的輸入就是userDict這個NSDictionary數據,輸出則是一個PJXUser類的對象user。不知道大家有沒有註意到,attributes中的key必須和Model中的property的名稱一致,比如上例中PJXUser的username、password等屬性(當然,你可以使用一個映射表解決這個問題,不過我們先暫時不想那麼多)。
2. 核心演算法怎麼做(輸入轉輸出)?
核心演算法部分其實就是調用initWithAttributes:這個函數。那這個函數該如何設計呢?
既然我們需要所有的Model類都可以調用這個initWithAttributes:來完成JSON轉Model的工作。那麼我們首先想到的就是將這個函數添加到NSObject的category中,並要求所有Model類都繼承自NSObject。
所以我首先新建了一個NSObject+Extension的category。併在其中添加了- (instancetype)initWithAttributes:(NSDictionary *)attributes方法。下麵我簡單闡述下該函數的實現。
其實我的實現思路基本參照的YYKit(傳送門)中的YYModel(傳送門)部分。其最核心的部分就是調用Model中每個屬性的setter方法,並且將傳入的attributes中每個元素的value作為setter的參數。
好的,到此為止最核心的部分已經講完了。可能大家會有很多疑問,比如說如何獲取到屬性的setter方法,獲取後又如何調用setter方法,畢竟此時的操作是在NSObject這個父類中進行的,並沒有具體子類的信息。這裡我簡單提一下,既然編譯期我們無法解決上述問題,那麼我們就需要藉助於OC的runtime機制了。當然,下麵會具體講解如何實現。
3. 具體實現
根據上面的核心思路,我覺得實現起來還存在一些問題:
如何獲取每個屬性的setter方法?如果現在獲取到了每個屬性的setter方法(註意是SEL類型),怎麼給每個屬性調用此方法?
現在是在NSObject中操作,所以不指望使用obj.username = attributes[@"username"]。所以需要使用runtime中的objc_msgSend,使用方法舉例如下:
((void (*)(id, SEL, id))(void *) objc_msgSend)((id)self, NSSelectorFromString(@"setUsername:"), @"shuaige");
可以看到我們只需要把其中的@"setUsername"和@"shuaige"替換成我們自己的變數就行。具體怎麼替換呢?這時候我們就需要創建一些數據結構來處理和保存相關的屬性信息。當然,這些數據結構也是我在實現過程中不斷修正的結果。至於中間如何修正,就不細說了,直接上結果。
數據結構的構建其實也很符合我們的思考習慣。既然我們需要對某個類進行處理,不可避免的,我們需要新建一個類來存儲Class信息(PJXClassInfo),而每個Class是由property、ivar和method組成的,所以針對不同組成,我們需要定義三個類來存儲property、ivar、method。但是此處我們只需要property信息,所以只建立了property相關的類(PJXPropertyInfo)。
我首先創建了一個PJXClassInfo的類。這個類目前只存放了一個NSMutableDictionary類型的propertyInfos屬性,該屬性是用來存儲這個Class的property信息。而propertyInfos中每個元素其實就是一個個PJXPropertyInfo對象。而每個PJXPropertyInfo保存的就是property的name,setter方法等等,當然,後期會根據需求為PJXPropertyInfo添加新的屬性。
這兩個類的關係如下:
下麵我們看看具體代碼如何實現,如下:
PJXPropertyInfo代碼
/** * @brief 存儲Model中每個property的信息 * @param property 是一個objc_property_t類型變數 * @param name 表示該property的名稱 * @param setter 是一個SEL類型變數,表示該property的setter方法 */ @interface PJXPropertyInfo : NSObject @property (nonatomic, assign) objc_property_t property; @property (nonatomic, strong) NSString *name; @property (nonatomic, assign) SEL setter; @end @implementation PJXPropertyInfo - (instancetype)initWithPropertyInfo:(objc_property_t)property { self = [self init]; if (self) { // 以備不時之需 _property = property; // 使用property_getName獲取到該property的名稱 const char *name = property_getName(property); if (name) { _name = [NSString stringWithUTF8String:name]; } // 目前不考慮自定義setter方法,只考慮系統預設生成setter方法 // 也就是說屬性username的setter方法為setUsername: NSString *setter = [NSString stringWithFormat:@"%@%@", [_name substringToIndex:1].uppercaseString, [_name substringFromIndex:1]]; _setter = NSSelectorFromString([NSString stringWithFormat:@"set%@:", setter]); } return self; } @end
PJXClassInfo代碼
/** * @brief 存儲Model的Class信息,不過目前只存儲Class的property信息 * @param propertyInfos 是一個NSMutableDictionary類型的變數,key存儲property的名稱,value存儲對應的PJXPropertyInfo對象 */ @interface PJXClassInfo : NSObject @property (nonatomic, strong) NSMutableDictionary *propertyInfos; @end @implementation PJXClassInfo - (instancetype)initWithClassInfo:(Class)cls { self = [self init]; // 使用class_copyPropertyList獲取到Class的所有property(objc_property_t類型) unsigned int propertyCount = 0; objc_property_t *properties = class_copyPropertyList(cls, &propertyCount); _propertyInfos = [NSMutableDictionary dictionary]; // 遍歷properties數組 // 根據對應的objc_property_t信息構建出PJXPropertyInfo對象,並給propertyInfos賦值 if (properties) { for (unsigned int i = 0; i < propertyCount; i++) { PJXPropertyInfo *propertyInfo = [[PJXPropertyInfo alloc] initWithPropertyInfo:properties[i]]; _propertyInfos[propertyInfo.name] = propertyInfo; } // 註意釋放空間 free(properties); } return self; } @end
現在我們回到之前的問題,即如何獲取setter並應用?可以看到有了這兩個數據結構,我們就已經解決瞭如何獲取到每個property的setter的問題(使用PJXClassInfo的propertyInfos的屬性)。剩下的事情就簡單了,調用setter方法進行賦值。這裡參考YYModel中的方式,使用了一個Core Foundation函數CFDictionaryApplyFunction。
void CFDictionaryApplyFunction(CFDictionaryRef theDict, CFDictionaryApplierFunction applier, void *context);
該函數的作用是對於theDict每個key-value元素都應用applier函數。
所以我們來看看這個applier函數應該怎麼設計。
註意這種C語言的applier回調函數不能設計為成員函數,因為成員函數隱藏了一個self參數。此處我們將該回調函數設計成static,並且命名為PropertyWithDictionaryFunction。
// 註意我傳入的dictionary就是用戶提供的JSON數據 // 比如此處傳入的key==@"username",value==@"shuaige" static void PropertyWithDictionaryFunction(const void *key, const void *value, void *context) { // 先將key和value轉化到Cocoa框架下 NSString *keyStr = (__bridge NSString *)(key); id setValue = (__bridge id)(value); // modelSelf其實就是self,不過我這裡用的是static函數,所以沒有預設參數self // 此時我們需要藉助context參數來獲取到這個self // 所以我設計了一個PJXModelContext,用來存儲self信息 // 另外,此函數的參數中也沒有保存每個property信息,也得靠context這個參數來傳遞 // 所以PJXModelContext還需要存儲PJXClassInfo對象信息 PJXModelContext *modelContext = context; id modelSelf = (__bridge id)(modelContext->modelSelf); PJXClassInfo *classInfo = (__bridge PJXClassInfo *)(modelContext->modelClassInfo); PJXPropertyInfo *info = classInfo.propertyInfos[keyStr]; ((void (*)(id, SEL, id))(void *) objc_msgSend)(modelSelf, info.setter, setValue); }
最後一步就是在我們的initWithAttributes:函數中構建PJXModelContext並應用到上述函數。
typedef struct { void *modelSelf; void *modelClassInfo; }PJXModelContext; - (instancetype)initWithAttributes:(NSDictionary *)attributes { self = [self init]; if (self) { // 初始化PJXClassInfo對象,並給modelContext賦值 PJXModelContext modelContext = {0}; modelContext.modelSelf = (__bridge void *)(self); PJXClassInfo *classInfo = [[PJXClassInfo alloc] initWithClassInfo:[self class]]; modelContext.modelClassInfo = (__bridge void *)classInfo; // 應用該函數,將得到JSON->Model後的Model數據 CFDictionaryApplyFunction((CFDictionaryRef)attributes, PropertyWithDictionaryFunction, &modelContext); } return self; }
4. 測試結果
在2.設計思路這一部分,我們舉了一個案例。現在我們運行下,看看NSLog的結果:
成功了!
5. 存在問題
目前的函數整體才100來行,還是存在很多問題沒有考慮到。
比如:
- 沒有考慮用戶傳入的JSON數據的key值和property的名稱不一致
- 沒有考慮用戶傳入的JSON數據有嵌套
- 沒有考慮JSON數據的value值不一定是NSString類型
- 沒有考慮JSON數據並不一定是NSDictionary類型
- 沒有考慮用戶自定義了Model屬性的setter方法
- ……
不過一口吃不了一個胖子,在後面我會一一修複這些Bug,敬請期待。附上該代碼的GitHub地址。