【疯狂造轮子-iOS】JSON转Model系列之二
【疯狂造轮子-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<br/> {<br/> NSDictionary *dict = [self pjx_dictionaryWithJSON:json];<br/> return [self initWithAttributes:dict];<br/> } /**<br/> * @brief 将NSString和NSData格式的json数据转化为NSDictionary类型<br/> */<br/> - (NSDictionary *)pjx_dictionaryWithJSON:(id)json<br/> {<br/> if (!json) {<br/> return nil;<br/> }<br/> // 若是NSDictionary类型,直接返回<br/> if ([json isKindOfClass:[NSDictionary class]]) {<br/> return json;<br/> } NSDictionary *dict = nil;<br/> NSData *jsonData = nil; if ([json isKindOfClass:[NSString class]]) {<br/> // 如果是NSString,就先转化为NSData<br/> jsonData = [(NSString*)json dataUsingEncoding:NSUTF8StringEncoding];<br/> } else if ([json isKindOfClass:[NSData class]]) {<br/> jsonData = json;<br/> } if (jsonData && [jsonData isKindOfClass:[NSData class]]) {<br/> // 如果时NSData类型,使用NSJSONSerialization<br/> NSError *error = nil;<br/> dict = [NSJSONSerialization JSONObjectWithData:jsonData options: error:&error];<br/> if (error) {<br/> NSLog(@"pjx_dictionaryWithJSON error:%@", error);<br/> return nil;<br/> }<br/> if (![dict isKindOfClass:[NSDictionary class]]) {<br/> return nil;<br/> }<br/> } return dict;<br/> }
为此,我在ViewController添加了两个sample。分别用来解析NSString类型的JSON数据和NSData类型的JSON数据。
// NSString类型的JSON数据<br/> - (void)runSimpleSample2<br/> {<br/> NSString *userStr = @" \<br/> { \<br/> \"username\" : \"shuaige\", \<br/> \"password\" : \"123456\", \<br/> \"avatarImageURL\" : \"http://www.example.com/shuaige.png\" \<br/> }"; PJXUser *user = [[PJXUser alloc] initWithJSONData:userStr]; NSLog(@"runSimpleSample2\n");<br/> NSLog(@"----------------------------------------");<br/> NSLog(@"username:%@\n",user.username);<br/> NSLog(@"password:%@\n",user.password);<br/> NSLog(@"avatarImageURL:%@\n",user.avatarImageURL);<br/> } // NSData类型的JSON数据<br/> - (void)runSimpleSample3<br/> {<br/> NSString *userInfoFilePath = [[NSBundle mainBundle] pathForResource:@"UserInfo" ofType:@"txt"];<br/> NSData *data = [NSData dataWithContentsOfFile:userInfoFilePath];<br/> PJXUser *user = [[PJXUser alloc] initWithJSONData:data]; NSLog(@"runSimpleSample3\n");<br/> NSLog(@"----------------------------------------");<br/> NSLog(@"username:%@\n",user.username);<br/> NSLog(@"password:%@\n",user.password);<br/> NSLog(@"avatarImageURL:%@\n",user.avatarImageURL);<br/> }
输出结果也是正确的:
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.<br/> @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协议函数<br/> @interface PJXUserPropertyMapper : NSObject <JSONProtocol> @property (nonatomic, copy) NSString* username; // 用户名<br/> @property (nonatomic, copy) NSString* password; // 密码<br/> @property (nonatomic, copy) NSString* avatarImageURL; // 头像的URL地址 @end @implementation PJXUserPropertyMapper<br/> // 实现propertyMapper这个协议方法<br/> + (NSDictionary *)propertyMapper<br/> {<br/> return @{@"Username" : @"username",<br/> @"Password" : @"password",<br/> @"AvatarImageURL" : @"avatarImageURL"};<br/> } @end
随后我定义了一个example。
#pragma mark - PropertyMapper Sample<br/> - (void)runPropertyMapperSample<br/> {<br/> NSDictionary *userDict = @{@"Username" : @"shuaige",<br/> @"Password" : @"",<br/> @"AvatarImageURL" : @"http://www.example.com/shuaige.png"};<br/> PJXUserPropertyMapper *user = [[PJXUserPropertyMapper alloc] initWithJSONData:userDict]; NSLog(@"runPropertyMapperSample\n");<br/> NSLog(@"----------------------------------------");<br/> NSLog(@"username:%@\n",user.username);<br/> NSLog(@"password:%@\n",user.password);<br/> NSLog(@"avatarImageURL:%@\n",user.avatarImageURL);<br/> }
是不是感觉调用上和之前的非property映射没什么区别?那是因为我们需要在initWithJSONData中增加一些东西。
具体的做法是在PropertyWithDictionary函数增加了一个查表操作。
// 注意我传入的dictionary就是用户提供的JSON数据<br/> // 比如此处传入的key==@"username",value==@"shuaige"<br/> static void PropertyWithDictionaryFunction(const void *key, const void *value, void *context)<br/> {<br/> NSString *keyStr = (__bridge NSString *)(key); ...... // 如果使用了JSONProtocol,并且自定义了propertyMapper,那么还需要将keyStr转化下<br/> if ([modelSelf conformsToProtocol:@protocol(JSONProtocol)] && [[modelSelf class] respondsToSelector:@selector(propertyMapper)]) {<br/> keyStr = [[[modelSelf class] propertyMapper] objectForKey:keyStr];<br/> } ......<br/> }
这样就可以啦.我们看看效果:
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; // 博客标题<br/> @property (nonatomic, strong) NSURL *blogURL; // 博客网址<br/> @property (nonatomic, assign) NSInteger blogIndex; // 博客索引值<br/> @property (nonatomic, strong) NSDate *postDate; // 博客发布时间<br/> @property (nonatomic, strong) NSArray *friends; // 我的好友名称<br/> @property (nonatomic, strong) NSSet *collections; // 我的收藏 @end @implementation PJXUserVariousType @end #pragma mark - VariousType Sample<br/> - (void)runVariousTypeSample<br/> {<br/> NSDictionary *userDict = @{@"blogTitle" : @"iOS developer",<br/> @"blogURL" : @"http://www.example.com/blog.html",<br/> @"blogIndex" : @,<br/> @"postDate" : [NSDate date],<br/> @"friends" : @[@"meinv1", @"meinv2", @"meinv3"],<br/> @"collections" : @[@"shuaige1", @"shuaige2", @"shuaige3"]};<br/> PJXUserVariousType *user = [[PJXUserVariousType alloc] initWithJSONData:userDict]; NSLog(@"runVariousTypeSample\n");<br/> NSLog(@"----------------------------------------");<br/> NSLog(@"blogTitle:%@\n",user.blogTitle);<br/> NSLog(@"blogURL:%@\n",user.blogURL);<br/> NSLog(@"blogIndex:%ld\n",user.blogIndex);<br/> NSLog(@"postDate:%@\n",user.postDate);<br/> NSLog(@"friends:%@\n",user.friends);<br/> NSLog(@"collections:%@\n",user.collections);<br/> }
你猜输出啥?
其他都正确,唯独我们的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类型<br/> typedef NS_ENUM(NSUInteger, PJXEncodingType) {<br/> PJXEncodingTypeUnknown = , ///< unknown<br/> PJXEncodingTypeBool = 1, ///< bool<br/> PJXEncodingTypeInt8 = 2, ///< char / BOOL<br/> PJXEncodingTypeUInt8 = 3, ///< unsigned char<br/> PJXEncodingTypeInt16 = 4, ///< short<br/> PJXEncodingTypeUInt16 = 5, ///< unsigned short<br/> PJXEncodingTypeInt32 = 6, ///< int<br/> PJXEncodingTypeUInt32 = 7, ///< unsigned int<br/> PJXEncodingTypeInt64 = 8, ///< long long<br/> PJXEncodingTypeUInt64 = 9, ///< unsigned long long<br/> PJXEncodingTypeFloat = 10, ///< float<br/> PJXEncodingTypeDouble = 11, ///< double<br/> PJXEncodingTypeLongDouble = 12, ///< long double<br/> }; // 根据objc_property_attribute_t可以获取到property的类型PJXEncodingType<br/> // 参考YYModel<br/> PJXGetEncodingType(const char *encodingType) {<br/> char *type = (char *)encodingType;<br/> if (!type) return PJXEncodingTypeUnknown;<br/> size_t len = strlen(type);<br/> if (len == ) return PJXEncodingTypeUnknown; switch (*type) {<br/> case 'B': return PJXEncodingTypeBool;<br/> case 'c': return PJXEncodingTypeInt8;<br/> case 'C': return PJXEncodingTypeUInt8;<br/> case 's': return PJXEncodingTypeInt16;<br/> case 'S': return PJXEncodingTypeUInt16;<br/> case 'i': return PJXEncodingTypeInt32;<br/> case 'I': return PJXEncodingTypeUInt32;<br/> case 'l': return PJXEncodingTypeInt32;<br/> case 'L': return PJXEncodingTypeUInt32;<br/> case 'q': return PJXEncodingTypeInt64;<br/> case 'Q': return PJXEncodingTypeUInt64;<br/> case 'f': return PJXEncodingTypeFloat;<br/> case 'd': return PJXEncodingTypeDouble;<br/> case 'D': return PJXEncodingTypeLongDouble; default: return PJXEncodingTypeUnknown;<br/> }<br/> } /**<br/> * @brief 存储Model中每个property的信息<br/> * ......<br/> * @param type 是一个PJXEncodingType类型变量,为了存储该属性是哪种Number(int?double?BOOL?)<br/> */<br/> @interface PJXPropertyInfo : NSObject<br/> ......<br/> @property (nonatomic, assign) PJXEncodingType type;<br/> @end @implementation PJXPropertyInfo - (instancetype)initWithPropertyInfo:(objc_property_t)property<br/> {<br/> self = [self init]; if (self) {<br/> ...... // 判断属性类型<br/> unsigned int attrCount;<br/> // 关于objc_property_attribute_t,这里有一篇文章介绍的很好<br/> // http://www.henishuo.com/runtime-property-ivar/<br/> objc_property_attribute_t *attrs = property_copyAttributeList(property, &attrCount);<br/> for (unsigned int i = ; i < attrCount; i++) {<br/> switch (attrs[i].name[]) {<br/> case 'T': {// EncodingType<br/> if (attrs[i].value) {<br/> //NSLog(@"attrs[%d].value = %s", i, attrs[i].value);<br/> // 可以根据value获取到property类型<br/> _type = PJXGetEncodingType(attrs[i].value);<br/> }<br/> break;<br/> }<br/> default:<br/> break;<br/> }<br/> }<br/> ......<br/> } return self;<br/> } // 根据propertyInfo中存储的type判断其是否为Number<br/> - (BOOL)isNumber<br/> {<br/> switch (self.type) {<br/> case PJXEncodingTypeBool:<br/> case PJXEncodingTypeInt8:<br/> case PJXEncodingTypeUInt8:<br/> case PJXEncodingTypeInt16:<br/> case PJXEncodingTypeUInt16:<br/> case PJXEncodingTypeInt32:<br/> case PJXEncodingTypeUInt32:<br/> case PJXEncodingTypeInt64:<br/> case PJXEncodingTypeUInt64:<br/> case PJXEncodingTypeFloat:<br/> case PJXEncodingTypeDouble:<br/> case PJXEncodingTypeLongDouble:<br/> return YES;<br/> default:<br/> return NO;<br/> break;<br/> }<br/> } // 使用objc_msgSend调用modelSelf中该属性对应的setter方法<br/> - (void)setNumberValue:(NSNumber *)number withModelSelf:(id)modelSelf<br/> {<br/> switch (self.type) {<br/> case PJXEncodingTypeBool:<br/> ((void (*)(id, SEL, BOOL))(void *) objc_msgSend)(modelSelf, self.setter, number.boolValue);<br/> break;<br/> case PJXEncodingTypeInt8:<br/> ((void (*)(id, SEL, BOOL))(void *) objc_msgSend)(modelSelf, self.setter, number.charValue);<br/> break;<br/> case PJXEncodingTypeUInt8:<br/> ((void (*)(id, SEL, BOOL))(void *) objc_msgSend)(modelSelf, self.setter, number.unsignedCharValue);<br/> break;<br/> case PJXEncodingTypeInt16:<br/> ((void (*)(id, SEL, BOOL))(void *) objc_msgSend)(modelSelf, self.setter, number.shortValue);<br/> break;<br/> case PJXEncodingTypeUInt16:<br/> ((void (*)(id, SEL, BOOL))(void *) objc_msgSend)(modelSelf, self.setter, number.unsignedShortValue);<br/> break;<br/> case PJXEncodingTypeInt32:<br/> ((void (*)(id, SEL, BOOL))(void *) objc_msgSend)(modelSelf, self.setter, number.intValue);<br/> break;<br/> case PJXEncodingTypeUInt32:<br/> ((void (*)(id, SEL, BOOL))(void *) objc_msgSend)(modelSelf, self.setter, number.unsignedIntValue);<br/> break;<br/> case PJXEncodingTypeInt64:<br/> ((void (*)(id, SEL, uint64_t))(void *) objc_msgSend)(modelSelf, self.setter, number.longLongValue);<br/> break;<br/> case PJXEncodingTypeUInt64:<br/> ((void (*)(id, SEL, uint64_t))(void *) objc_msgSend)(modelSelf, self.setter, number.unsignedLongLongValue);<br/> break;<br/> case PJXEncodingTypeFloat:<br/> ((void (*)(id, SEL, float))(void *) objc_msgSend)(modelSelf, self.setter, number.floatValue);<br/> break;<br/> case PJXEncodingTypeDouble:<br/> ((void (*)(id, SEL, double))(void *) objc_msgSend)(modelSelf, self.setter, number.doubleValue);<br/> break;<br/> case PJXEncodingTypeLongDouble:<br/> ((void (*)(id, SEL, long double))(void *) objc_msgSend)(modelSelf, self.setter, number.doubleValue);<br/> break;<br/> default:<br/> break;<br/> }<br/> } @end
有了上述的几个方法,后面就好办了,只需在PropertyWithDictionaryFunction函数中添加一个Number的判断就行:
static void PropertyWithDictionaryFunction(const void *key, const void *value, void *context)<br/> {<br/> ...... // 如果该属性是Number,那么就用Number赋值方法给其赋值<br/> if ([info isNumber]) {<br/> [info setNumberValue:setValue withModelSelf:modelSelf];<br/> } else {<br/> ((void (*)(id, SEL, id))(void *) objc_msgSend)(modelSelf, info.setter, setValue);<br/> }<br/> }
这下终于成功了:
2.4 没有考虑用户自定义了Model属性的setter方法
这个其实比较简单,只需要对property的attribute(objc_property_attribute_t)进行判断即可:
- (instancetype)initWithPropertyInfo:(objc_property_t)property<br/> {<br/> ...... BOOL isCustomSetter = NO;<br/> // 判断属性类型<br/> unsigned int attrCount;<br/> // 关于objc_property_attribute_t,这里有一篇文章介绍的很好<br/> // http://www.henishuo.com/runtime-property-ivar/<br/> objc_property_attribute_t *attrs = property_copyAttributeList(property, &attrCount);<br/> for (unsigned int i = ; i < attrCount; i++) {<br/> switch (attrs[i].name[]) {<br/> case 'T': { // EncodingType<br/> if (attrs[i].value) {<br/> //NSLog(@"attrs[%d].value = %s", i, attrs[i].value);<br/> // 可以根据value获取到property类型<br/> _type = PJXGetEncodingType(attrs[i].value);<br/> }<br/> break;<br/> }<br/> <strong>case 'S': { // 自定义setter方法<br/> if</strong><strong> (attrs[i].value) {<br/> isCustomSetter =</strong><strong> YES;<br/> _setter =</strong><strong> NSSelectorFromString([NSString stringWithUTF8String:attrs[i].value]);<br/> }<br/> } break</strong><strong>;</strong><br/> default:<br/> break;<br/> }<br/> } <strong>if (!</strong><strong>isCustomSetter) {</strong><br/> // 如果没有自定义setter方法,只考虑系统默认生成setter方法<br/> // 也就是说属性username的setter方法为setUsername:<br/> NSString *setter = [NSString stringWithFormat:@"%@%@", [_name substringToIndex:].uppercaseString, [_name substringFromIndex:]];<br/> _setter = NSSelectorFromString([NSString stringWithFormat:@"set%@:", setter]);<br/> <strong>}</strong><br/> } return self;<br/> }
使用下面这个例子测试:
@interface PJXUserCustomSetter : NSObject @property (nonatomic, copy, setter=setCustomUserName:) NSString* username; // 用户名<br/> @property (nonatomic, copy, setter=setCustomBirthday:) NSDate* birthday; // 生日 @end @implementation PJXUserCustomSetter - (void)setCustomUserName:(NSString *)username<br/> {<br/> _username = [NSString stringWithFormat:@"My name is %@", username];<br/> } - (void)setCustomBirthday:(NSDate *)birthday<br/> {<br/> NSTimeInterval timeInterval = **; // 过一天<br/> _birthday = [NSDate dateWithTimeInterval:timeInterval sinceDate:birthday];<br/> } @end #pragma mark - Custom Setter Sample<br/> - (void)runCustomSetterSample<br/> {<br/> NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];<br/> [dateFormatter setDateFormat:@"yyyy-MM-dd HH:mm:ss"];<br/> NSDate *birthday = [dateFormatter dateFromString:@"2016-04-07 00:20:03"];<br/> NSDictionary *userDict = @{@"username" : @"shuaige",<br/> @"birthday" : birthday};<br/> PJXUserCustomSetter *user = [[PJXUserCustomSetter alloc] initWithJSONData:userDict]; NSLog(@"runCustomSetterSample\n");<br/> NSLog(@"----------------------------------------");<br/> NSLog(@"username:%@\n",user.username);<br/> NSLog(@"birthday:%@\n",user.birthday);<br/> }
得到的结果为:
成功了.
2.5 没有考虑用户传入的JSON数据有嵌套
我个人感觉这个应该没什么问题,为什么这么说呢?因为我嵌套的无非也是一个NSObject类型,那么就调用其自身的setter方法就OK啊.不过还是以防万一,我构造了一下案例:
@interface PJXBlog : NSObject @property (nonatomic, copy) NSString *title; // 博客名称<br/> @property (nonatomic, strong) NSDate *postDate; // 博客发表日期<br/> @property (nonatomic, copy) PJXUser *author; // 博客作者 @end @implementation PJXBlog @end #pragma mark - Nest Sample<br/> - (void)runNestSample<br/> {<br/> NSDictionary *blogDict = @{@"title" : @"how to convert JSON to Model?",<br/> @"postDate" : [NSDate date],<br/> @"author" : @{@"username" : @"shuaige",<br/> @"password" : @"",<br/> @"avatarImageURL":@"http://www.example.com/shuaige.png"}};<br/> PJXBlog *blog = [[PJXBlog alloc] initWithJSONData:blogDict]; NSLog(@"runNestSample\n");<br/> NSLog(@"----------------------------------------");<br/> NSLog(@"title:%@\n",blog.title);<br/> NSLog(@"postDate:%@\n",blog.postDate);<br/> NSLog(@"author:%@\n",blog.author);<br/> }
输出结果如下:
结果没什么问题.不过这样说可能不是很负责任,但是目前我也想不到反例.暂时先当做成功了.
3. 总结
以我的能力,目前只能将JSON转化Model实现到这个地步了.总体来说,实现的难度不是很大(因为我考虑的情况还是比较少的,另外还有些功能没添加),不过涉及的知识点还是挺多的,挺不错的一个练手项目:).