【瘋狂造輪子-iOS】JSON轉Model系列之二
本文轉載請注明出處 —— polobymulberry-博客園
1. 前言
上一篇《【瘋狂造輪子-iOS】JSON轉Model系列之一》實現了一個簡陋的JSON轉Model的庫,不過還存在很多問題。下面我會嘗試一個個去解決。
2. 存在問題及解決思路
2.1 沒有考慮JSON數據並不一定是NSDictionary類型
有時候JSON並不一定是NSDictionary類型,可能是一個字符串,也可能是NSData類型的數據。不過不管是哪種類型,統統先將其轉化為NSData數據,然后使用+[NSJSONSerialization JSONObjectWithData:options:error:]來轉化。所以我在initWithAttributes:上面又封裝了一層。
- (instancetype)initWithJSONData:(id)json { NSDictionary *dict = [self pjx_dictionaryWithJSON:json]; return [self initWithAttributes:dict]; } /** * @brief 將NSString和NSData格式的json數據轉化為NSDictionary類型 */ - (NSDictionary *)pjx_dictionaryWithJSON:(id)json { if (!json) { return nil; } // 若是NSDictionary類型,直接返回 if ([json isKindOfClass:[NSDictionary class]]) { return json; } NSDictionary *dict = nil; NSData *jsonData = nil; if ([json isKindOfClass:[NSString class]]) { // 如果是NSString,就先轉化為NSData jsonData = [(NSString*)json dataUsingEncoding:NSUTF8StringEncoding]; } else if ([json isKindOfClass:[NSData class]]) { jsonData = json; } if (jsonData && [jsonData isKindOfClass:[NSData class]]) { // 如果時NSData類型,使用NSJSONSerialization NSError *error = nil; dict = [NSJSONSerialization JSONObjectWithData:jsonData options:0 error:&error]; if (error) { NSLog(@"pjx_dictionaryWithJSON error:%@", error); return nil; } if (![dict isKindOfClass:[NSDictionary class]]) { return nil; } } return dict; }
為此,我在ViewController添加了兩個sample。分別用來解析NSString類型的JSON數據和NSData類型的JSON數據。
// NSString類型的JSON數據 - (void)runSimpleSample2 { NSString *userStr = @" \ { \ \"username\" : \"shuaige\", \ \"password\" : \"123456\", \ \"avatarImageURL\" : \"http://www.example.com/shuaige.png\" \ }"; PJXUser *user = [[PJXUser alloc] initWithJSONData:userStr]; NSLog(@"runSimpleSample2\n"); NSLog(@"----------------------------------------"); NSLog(@"username:%@\n",user.username); NSLog(@"password:%@\n",user.password); NSLog(@"avatarImageURL:%@\n",user.avatarImageURL); } // NSData類型的JSON數據 - (void)runSimpleSample3 { NSString *userInfoFilePath = [[NSBundle mainBundle] pathForResource:@"UserInfo" ofType:@"txt"]; NSData *data = [NSData dataWithContentsOfFile:userInfoFilePath]; PJXUser *user = [[PJXUser alloc] initWithJSONData:data]; NSLog(@"runSimpleSample3\n"); NSLog(@"----------------------------------------"); NSLog(@"username:%@\n",user.username); NSLog(@"password:%@\n",user.password); NSLog(@"avatarImageURL:%@\n",user.avatarImageURL); }
輸出結果也是正確的:
2.2 沒有考慮用戶傳入的JSON數據的key值和property的名稱不一致
我第一反應是使用一個映射表。也就是說用戶使用時需要自定義一套property和key的映射表。YYModel中使用了一個+ (NSDictionary *)modelCustomPropertyMapper函數,用戶可以自定義該函數達到映射表的效果,而這個函數是放在一個protocol中的。我挺認同這種設計的,因為modelCustomPropertyMapper這種函數和Model是一種組合關系,可有可無(optional),所以設計成協議更合適。但是作者在設計protocol又說了一句:
// There's no need to add '<YYModel>' to your class header. @protocol YYModel <NSObject>
什么意思呢,就是說你自定義一個NSObject子類(如YYBook)時,如果想實現自定義的property映射關系,只需要實現modelCustomPropertyMapper函數即可,而不需要寫成@interface YYBook : NSObject <YYModel>。作者的意思是你遵不遵循YYModel這個protocol都沒事,反正你只要在YYBook實現了modelCustomPropertyMapper即可。具體解釋,大家請參考這個issue。
這種設計我不是很贊同,我是有潔癖的人,要不然你就別定義YYModel這個protocol,說明文檔里面着重說明一下就行。所以此處我還是選擇判斷NSObject的子類是否遵循protocol,也就是說只有遵循了這個protocol,才能自定義property映射關系。
首先我們看如何使用自定義propertyMapper。我先建立一個PJXUserPropertyMapper類,遵循了JSONProtocol協議,並實現了propertyMapper協議函數。
// 遵循JSONProtocol協議,這個JSONProtocol中定義的就是我的propertyMapper協議函數 @interface PJXUserPropertyMapper : NSObject <JSONProtocol> @property (nonatomic, copy) NSString* username; // 用戶名 @property (nonatomic, copy) NSString* password; // 密碼 @property (nonatomic, copy) NSString* avatarImageURL; // 頭像的URL地址 @end @implementation PJXUserPropertyMapper // 實現propertyMapper這個協議方法 + (NSDictionary *)propertyMapper { return @{@"Username" : @"username", @"Password" : @"password", @"AvatarImageURL" : @"avatarImageURL"}; } @end
隨后我定義了一個example。
#pragma mark - PropertyMapper Sample - (void)runPropertyMapperSample { NSDictionary *userDict = @{@"Username" : @"shuaige", @"Password" : @"123456", @"AvatarImageURL" : @"http://www.example.com/shuaige.png"}; PJXUserPropertyMapper *user = [[PJXUserPropertyMapper alloc] initWithJSONData:userDict]; NSLog(@"runPropertyMapperSample\n"); NSLog(@"----------------------------------------"); NSLog(@"username:%@\n",user.username); NSLog(@"password:%@\n",user.password); NSLog(@"avatarImageURL:%@\n",user.avatarImageURL); }
是不是感覺調用上和之前的非property映射沒什么區別?那是因為我們需要在initWithJSONData中增加一些東西。
具體的做法是在PropertyWithDictionary函數增加了一個查表操作。
// 注意我傳入的dictionary就是用戶提供的JSON數據 // 比如此處傳入的key==@"username",value==@"shuaige" static void PropertyWithDictionaryFunction(const void *key, const void *value, void *context) { NSString *keyStr = (__bridge NSString *)(key); ...... // 如果使用了JSONProtocol,並且自定義了propertyMapper,那么還需要將keyStr轉化下 if ([modelSelf conformsToProtocol:@protocol(JSONProtocol)] && [[modelSelf class] respondsToSelector:@selector(propertyMapper)]) { keyStr = [[[modelSelf class] propertyMapper] objectForKey:keyStr]; } ...... }
這樣就可以啦.我們看看效果:
2.3 沒有考慮JSON數據的value值不一定是NSString類型
開始的時候,挺擔心我這種寫法會不會不兼容別的數據類型。不過我覺得應該沒什么問題,畢竟我使用的setter方法本質上沒啥問題,我的類型全用id來代替了(事實上,我的想法大錯特錯):
((void (*)(id, SEL, id))(void *) objc_msgSend)(modelSelf, info.setter, setValue);
不過本着不怕一萬,就怕萬一的心態。我還是做了一個example來試驗一下:
@interface PJXUserVariousType : NSObject @property (nonatomic, copy) NSString *blogTitle; // 博客標題 @property (nonatomic, strong) NSURL *blogURL; // 博客網址 @property (nonatomic, assign) NSInteger blogIndex; // 博客索引值 @property (nonatomic, strong) NSDate *postDate; // 博客發布時間 @property (nonatomic, strong) NSArray *friends; // 我的好友名稱 @property (nonatomic, strong) NSSet *collections; // 我的收藏 @end @implementation PJXUserVariousType @end #pragma mark - VariousType Sample - (void)runVariousTypeSample { NSDictionary *userDict = @{@"blogTitle" : @"iOS developer", @"blogURL" : @"http://www.example.com/blog.html", @"blogIndex" : @666, @"postDate" : [NSDate date], @"friends" : @[@"meinv1", @"meinv2", @"meinv3"], @"collections" : @[@"shuaige1", @"shuaige2", @"shuaige3"]}; PJXUserVariousType *user = [[PJXUserVariousType alloc] initWithJSONData:userDict]; NSLog(@"runVariousTypeSample\n"); NSLog(@"----------------------------------------"); NSLog(@"blogTitle:%@\n",user.blogTitle); NSLog(@"blogURL:%@\n",user.blogURL); NSLog(@"blogIndex:%ld\n",user.blogIndex); NSLog(@"postDate:%@\n",user.postDate); NSLog(@"friends:%@\n",user.friends); NSLog(@"collections:%@\n",user.collections); }
你猜輸出啥?
其他都正確,唯獨我們的blogIndex出錯了。這里確實是我欠考慮了,類似NSInteger,BOOL這些NSNumber類型(我暫時只考慮這些常用類型)需要單獨處理一下。這一部分看起來容易,但是為了處理這種特殊情況確實要下很大功夫。比如你得先判斷該屬性是不是double或int這種類型,只有判斷除了該屬性是double還是int,你才能正確使用setter方法,而此處的調用方式也要單獨寫一個,因為和之前調用方式有一些些區別,需要判斷Number的類型是double,是int,還是BOOl…….
對此我在PJXPropertyInfo中定義了兩個函數,一個叫isNumber,用來判斷該屬性是不是一個Number,另一個叫setNumberValue:withModelSelf:,用來給是Number類型的屬性賦值。另外,我仿照YYModel(比YYModel簡化很多了)建了一個PJXEncodingType的enum類型,用來存儲Number的類型(int?double?BOOL?……),與之配套的還有一個PJXGetEncodingType函數,來獲取當前屬性的類型(是int?double?BOOL?),具體怎么做還挺復雜的,后面會詳細說明。
代碼如下:
// Number類型 typedef NS_ENUM(NSUInteger, PJXEncodingType) { PJXEncodingTypeUnknown = 0, ///< unknown PJXEncodingTypeBool = 1, ///< bool PJXEncodingTypeInt8 = 2, ///< char / BOOL PJXEncodingTypeUInt8 = 3, ///< unsigned char PJXEncodingTypeInt16 = 4, ///< short PJXEncodingTypeUInt16 = 5, ///< unsigned short PJXEncodingTypeInt32 = 6, ///< int PJXEncodingTypeUInt32 = 7, ///< unsigned int PJXEncodingTypeInt64 = 8, ///< long long PJXEncodingTypeUInt64 = 9, ///< unsigned long long PJXEncodingTypeFloat = 10, ///< float PJXEncodingTypeDouble = 11, ///< double PJXEncodingTypeLongDouble = 12, ///< long double }; // 根據objc_property_attribute_t可以獲取到property的類型PJXEncodingType // 參考YYModel PJXGetEncodingType(const char *encodingType) { char *type = (char *)encodingType; if (!type) return PJXEncodingTypeUnknown; size_t len = strlen(type); if (len == 0) return PJXEncodingTypeUnknown; switch (*type) { case 'B': return PJXEncodingTypeBool; case 'c': return PJXEncodingTypeInt8; case 'C': return PJXEncodingTypeUInt8; case 's': return PJXEncodingTypeInt16; case 'S': return PJXEncodingTypeUInt16; case 'i': return PJXEncodingTypeInt32; case 'I': return PJXEncodingTypeUInt32; case 'l': return PJXEncodingTypeInt32; case 'L': return PJXEncodingTypeUInt32; case 'q': return PJXEncodingTypeInt64; case 'Q': return PJXEncodingTypeUInt64; case 'f': return PJXEncodingTypeFloat; case 'd': return PJXEncodingTypeDouble; case 'D': return PJXEncodingTypeLongDouble; default: return PJXEncodingTypeUnknown; } } /** * @brief 存儲Model中每個property的信息 * ...... * @param type 是一個PJXEncodingType類型變量,為了存儲該屬性是哪種Number(int?double?BOOL?) */ @interface PJXPropertyInfo : NSObject ...... @property (nonatomic, assign) PJXEncodingType type; @end @implementation PJXPropertyInfo - (instancetype)initWithPropertyInfo:(objc_property_t)property { self = [self init]; if (self) { ...... // 判斷屬性類型 unsigned int attrCount; // 關於objc_property_attribute_t,這里有一篇文章介紹的很好 // http://www.henishuo.com/runtime-property-ivar/ objc_property_attribute_t *attrs = property_copyAttributeList(property, &attrCount); for (unsigned int i = 0; i < attrCount; i++) { switch (attrs[i].name[0]) { case 'T': {// EncodingType if (attrs[i].value) { //NSLog(@"attrs[%d].value = %s", i, attrs[i].value); // 可以根據value獲取到property類型 _type = PJXGetEncodingType(attrs[i].value); } break; } default: break; } } ...... } return self; } // 根據propertyInfo中存儲的type判斷其是否為Number - (BOOL)isNumber { switch (self.type) { case PJXEncodingTypeBool: case PJXEncodingTypeInt8: case PJXEncodingTypeUInt8: case PJXEncodingTypeInt16: case PJXEncodingTypeUInt16: case PJXEncodingTypeInt32: case PJXEncodingTypeUInt32: case PJXEncodingTypeInt64: case PJXEncodingTypeUInt64: case PJXEncodingTypeFloat: case PJXEncodingTypeDouble: case PJXEncodingTypeLongDouble: return YES; default: return NO; break; } } // 使用objc_msgSend調用modelSelf中該屬性對應的setter方法 - (void)setNumberValue:(NSNumber *)number withModelSelf:(id)modelSelf { switch (self.type) { case PJXEncodingTypeBool: ((void (*)(id, SEL, BOOL))(void *) objc_msgSend)(modelSelf, self.setter, number.boolValue); break; case PJXEncodingTypeInt8: ((void (*)(id, SEL, BOOL))(void *) objc_msgSend)(modelSelf, self.setter, number.charValue); break; case PJXEncodingTypeUInt8: ((void (*)(id, SEL, BOOL))(void *) objc_msgSend)(modelSelf, self.setter, number.unsignedCharValue); break; case PJXEncodingTypeInt16: ((void (*)(id, SEL, BOOL))(void *) objc_msgSend)(modelSelf, self.setter, number.shortValue); break; case PJXEncodingTypeUInt16: ((void (*)(id, SEL, BOOL))(void *) objc_msgSend)(modelSelf, self.setter, number.unsignedShortValue); break; case PJXEncodingTypeInt32: ((void (*)(id, SEL, BOOL))(void *) objc_msgSend)(modelSelf, self.setter, number.intValue); break; case PJXEncodingTypeUInt32: ((void (*)(id, SEL, BOOL))(void *) objc_msgSend)(modelSelf, self.setter, number.unsignedIntValue); break; case PJXEncodingTypeInt64: ((void (*)(id, SEL, uint64_t))(void *) objc_msgSend)(modelSelf, self.setter, number.longLongValue); break; case PJXEncodingTypeUInt64: ((void (*)(id, SEL, uint64_t))(void *) objc_msgSend)(modelSelf, self.setter, number.unsignedLongLongValue); break; case PJXEncodingTypeFloat: ((void (*)(id, SEL, float))(void *) objc_msgSend)(modelSelf, self.setter, number.floatValue); break; case PJXEncodingTypeDouble: ((void (*)(id, SEL, double))(void *) objc_msgSend)(modelSelf, self.setter, number.doubleValue); break; case PJXEncodingTypeLongDouble: ((void (*)(id, SEL, long double))(void *) objc_msgSend)(modelSelf, self.setter, number.doubleValue); break; default: break; } } @end
有了上述的幾個方法,后面就好辦了,只需在PropertyWithDictionaryFunction函數中添加一個Number的判斷就行:
static void PropertyWithDictionaryFunction(const void *key, const void *value, void *context) { ...... // 如果該屬性是Number,那么就用Number賦值方法給其賦值 if ([info isNumber]) { [info setNumberValue:setValue withModelSelf:modelSelf]; } else { ((void (*)(id, SEL, id))(void *) objc_msgSend)(modelSelf, info.setter, setValue); } }
這下終於成功了:
2.4 沒有考慮用戶自定義了Model屬性的setter方法
這個其實比較簡單,只需要對property的attribute(objc_property_attribute_t)進行判斷即可:
- (instancetype)initWithPropertyInfo:(objc_property_t)property { ...... BOOL isCustomSetter = NO; // 判斷屬性類型 unsigned int attrCount; // 關於objc_property_attribute_t,這里有一篇文章介紹的很好 // http://www.henishuo.com/runtime-property-ivar/ objc_property_attribute_t *attrs = property_copyAttributeList(property, &attrCount); for (unsigned int i = 0; i < attrCount; i++) { switch (attrs[i].name[0]) { case 'T': { // EncodingType if (attrs[i].value) { //NSLog(@"attrs[%d].value = %s", i, attrs[i].value); // 可以根據value獲取到property類型 _type = PJXGetEncodingType(attrs[i].value); } break; } case 'S': { // 自定義setter方法 if (attrs[i].value) { isCustomSetter = YES; _setter = NSSelectorFromString([NSString stringWithUTF8String:attrs[i].value]); } } break; default: break; } } if (!isCustomSetter) { // 如果沒有自定義setter方法,只考慮系統默認生成setter方法 // 也就是說屬性username的setter方法為setUsername: NSString *setter = [NSString stringWithFormat:@"%@%@", [_name substringToIndex:1].uppercaseString, [_name substringFromIndex:1]]; _setter = NSSelectorFromString([NSString stringWithFormat:@"set%@:", setter]); } } return self; }
使用下面這個例子測試:
@interface PJXUserCustomSetter : NSObject @property (nonatomic, copy, setter=setCustomUserName:) NSString* username; // 用戶名 @property (nonatomic, copy, setter=setCustomBirthday:) NSDate* birthday; // 生日 @end @implementation PJXUserCustomSetter - (void)setCustomUserName:(NSString *)username { _username = [NSString stringWithFormat:@"My name is %@", username]; } - (void)setCustomBirthday:(NSDate *)birthday { NSTimeInterval timeInterval = 24*60*60; // 過一天 _birthday = [NSDate dateWithTimeInterval:timeInterval sinceDate:birthday]; } @end #pragma mark - Custom Setter Sample - (void)runCustomSetterSample { NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init]; [dateFormatter setDateFormat:@"yyyy-MM-dd HH:mm:ss"]; NSDate *birthday = [dateFormatter dateFromString:@"2016-04-07 00:20:03"]; NSDictionary *userDict = @{@"username" : @"shuaige", @"birthday" : birthday}; PJXUserCustomSetter *user = [[PJXUserCustomSetter alloc] initWithJSONData:userDict]; NSLog(@"runCustomSetterSample\n"); NSLog(@"----------------------------------------"); NSLog(@"username:%@\n",user.username); NSLog(@"birthday:%@\n",user.birthday); }
得到的結果為:
2.5 沒有考慮用戶傳入的JSON數據有嵌套
我個人感覺這個應該沒什么問題,為什么這么說呢?因為我嵌套的無非也是一個NSObject類型,那么就調用其自身的setter方法就OK啊.不過還是以防萬一,我構造了一下案例:
@interface PJXBlog : NSObject @property (nonatomic, copy) NSString *title; // 博客名稱 @property (nonatomic, strong) NSDate *postDate; // 博客發表日期 @property (nonatomic, copy) PJXUser *author; // 博客作者 @end @implementation PJXBlog @end #pragma mark - Nest Sample - (void)runNestSample { NSDictionary *blogDict = @{@"title" : @"how to convert JSON to Model?", @"postDate" : [NSDate date], @"author" : @{@"username" : @"shuaige", @"password" : @"123456", @"avatarImageURL":@"http://www.example.com/shuaige.png"}}; PJXBlog *blog = [[PJXBlog alloc] initWithJSONData:blogDict]; NSLog(@"runNestSample\n"); NSLog(@"----------------------------------------"); NSLog(@"title:%@\n",blog.title); NSLog(@"postDate:%@\n",blog.postDate); NSLog(@"author:%@\n",blog.author); }
輸出結果如下:
結果沒什么問題.不過這樣說可能不是很負責任,但是目前我也想不到反例.暫時先當做成功了.
3. 總結
以我的能力,目前只能將JSON轉化Model實現到這個地步了.總體來說,實現的難度不是很大(因為我考慮的情況還是比較少的,另外還有些功能沒添加),不過涉及的知識點還是挺多的,挺不錯的一個練手項目:).
附上GitHub地址。