【疯狂造轮子-iOS】JSON转Model系列之一
【疯狂造轮子-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" :@"",
@"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:].uppercaseString, [_name substringFromIndex:]];
_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 = ;
objc_property_t *properties = class_copyPropertyList(cls, &propertyCount); _propertyInfos = [NSMutableDictionary dictionary]; // 遍历properties数组
// 根据对应的objc_property_t信息构建出PJXPropertyInfo对象,并给propertyInfos赋值
if (properties) {
for (unsigned int i = ; 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 = {};
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地址。
【疯狂造轮子-iOS】JSON转Model系列之一的更多相关文章
- 【疯狂造轮子-iOS】JSON转Model系列之二
[疯狂造轮子-iOS]JSON转Model系列之二 本文转载请注明出处 —— polobymulberry-博客园 1. 前言 上一篇<[疯狂造轮子-iOS]JSON转Model系列之一> ...
- Mac iOS Json 操作Model to JSON
在移动网络时代,json成为了主流的数据交换格式.如何能够方便快捷的创建.转化.传递json文件称为了开发者必备的技能.幸好,我们生活在开源时代,很多功能不需要我们重现造轮子.今天我推荐一款开源jso ...
- ios json转model的简单现实
在android开发中,可用第三方的转换库如gson等.当然在ios也有一些库如MJExtensiond等.在这里,我简单实现一下. 一.先建一个model并且继承NSObject,代码如下: cla ...
- 重复造轮子系列——基于Ocelot实现类似支付宝接口模式的网关
重复造轮子系列——基于Ocelot实现类似支付宝接口模式的网关 引言 重复造轮子系列是自己平时的一些总结.有的轮子依赖社区提供的轮子为基础,这里把使用过程的一些觉得有意思的做个分享.有些思路或者方法在 ...
- 造轮子系列之RPC 1:如何从零开始开发RPC框架
前言 RPC 框架是后端攻城狮永远都绕不开的知识点,目前业界比较知名有 Dubbo.Spring Cloud 等.很多人都停留在了只会用的阶段,作为程序猿,拥有好奇心深入学习,才能有效提高自己的竞争力 ...
- GitHub Android 最火开源项目Top20 GitHub 上的开源项目不胜枚举,越来越多的开源项目正在迁移到GitHub平台上。基于不要重复造轮子的原则,了解当下比较流行的Android与iOS开源项目很是必要。利用这些项目,有时能够让你达到事半功倍的效果。
1. ActionBarSherlock(推荐) ActionBarSherlock应该算得上是GitHub上最火的Android开源项目了,它是一个独立的库,通过一个API和主题,开发者就可以很方便 ...
- 自己造轮子系列之OOM框架AutoMapper
[前言] OOM框架想必大家在Web开发中是使用频率非常之高的,如果还不甚了解OOM框架,那么我们对OOM框架稍作讲解. OOM顾名思义,Object-Object-Mapping实体间相互转换.常见 ...
- 重复造轮子系列——基于FastReport设计打印模板实现桌面端WPF套打和商超POS高度自适应小票打印
重复造轮子系列——基于FastReport设计打印模板实现桌面端WPF套打和商超POS高度自适应小票打印 一.引言 桌面端系统经常需要对接各种硬件设备,比如扫描器.读卡器.打印机等. 这里介绍下桌面端 ...
- 「iOS造轮子」之UIButton 用Block响应事件
俗语说 一个不懒的程序员不是好程序员 造轮子,也只是为了以后更好的coding. coding,简易明了的代码更是所有程序员都希望看到的 无论是看自己的代码,还是接手别人的代码 都希望一看都知道这代码 ...
随机推荐
- 前端网络、JavaScript优化以及开发小技巧
一.网络优化 YSlow有23条规则,中文可以参考这里.这几十条规则最主要是在做消除或减少不必要的网络延迟,将需要传输的数据压缩至最少. 1)合并压缩CSS.JavaScript.图片,静态资源CDN ...
- Tcp/ip 报文解析
在编写网络程序时,常使用TCP协议.那么一个tcp包到底由哪些东西构成的呢?其实一个TCP包,首先需要通过IP协议承载,而IP报文,又需要通过以太网传送.下面我们来看看几种协议头的构成 一 .Ethe ...
- C# - 值类型、引用类型&走出误区,容易错误的说法
1. 值类型与引用类型小总结 1)对于引用类型的表达式(如一个变量),它的值是一个引用,而非对象. 2)引用就像URL,是允许你访问真实信息的一小片数据. 3)对于值类型的表达式,它的值是实际的数据. ...
- 执行 $Gulp 时发生了什么 —— 基于 Gulp 的前端集成解决方案(二)
前言 文章 在windows下安装gulp —— 基于 Gulp 的前端集成解决方案(一) 中,已经完成对 gulp 的安装,由于是window环境,文中特意提到了可以通过安装 gitbash 来代替 ...
- JavaScript权威指南 - 数组
JavaScript数组是一种特殊类型的对象. JavaScript数组元素可以为任意类型,最大容纳232-1个元素. JavaScript数组是动态的,有新元素添加时,自动更新length属性. J ...
- ZKWeb网页框架1.3正式发布
本次更新的内容有 更新引用包版本 Microsoft.AspNetCore.Hosting.Abstractions 1.1.0 Microsoft.AspNetCore.Http.Abstracti ...
- php批量删除
php批量删除可以实现多条或者全部数据一起删除 新建php文件 显示数据库中内容: <table width="100%" border="1" cell ...
- 从啥也不会到可以胜任最基本的JavaWeb工作,推荐给新人的学习路线(二)
在上一节中,主要阐述了JavaScript方面的学习路线.先列举一下我朋友的经历,他去过培训机构,说是4个月后月薪过万,虽然他现在还未达到这个指标. 培训机构一般的套路是这样:先教JavaSE,什么都 ...
- 小兔Java教程 - 三分钟学会Java文件上传
今天群里正好有人问起了Java文件上传的事情,本来这是Java里面的知识点,而我目前最主要的精力还是放在了JS的部分.不过反正也不麻烦,我就专门开一贴来聊聊Java文件上传的基本实现方法吧. 话不多说 ...
- 漫谈TCP
不得不承认,tcp是一个非常复杂的协议.它包含了RFC793及之后的一些协议.能把tcp的所有方面面面具到地说清楚,本身就是个很复杂的事情.如果再讲得枯燥,那么就会更让人昏昏欲睡了.本文希望能尽量用稍 ...