【瘋狂造輪子-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位址。