【疯狂造轮子-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

如果我们需要处理的情况符合下面两个要求:

  1. Model类的个数比较少
  2. 每个Model的成员不是很复杂

这种情况下使用上述方法还可以接受。但是一旦Model这一层急剧膨胀,这时候就会让人苦不堪言:

  1. initWithAttributes函数容易写错,而且出错后不方便排查。
  2. 机械性的代码会比较多,不利于提高效率。

考虑到手动转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来行,还是存在很多问题没有考虑到。

比如:

  1. 没有考虑用户传入的JSON数据的key值和property的名称不一致
  2. 没有考虑用户传入的JSON数据有嵌套
  3. 没有考虑JSON数据的value值不一定是NSString类型
  4. 没有考虑JSON数据并不一定是NSDictionary类型
  5. 没有考虑用户自定义了Model属性的setter方法
  6. ……

不过一口吃不了一个胖子,在后面我会一一修复这些Bug,敬请期待。附上该代码的GitHub地址

【疯狂造轮子-iOS】JSON转Model系列之一的更多相关文章

  1. 【疯狂造轮子-iOS】JSON转Model系列之二

    [疯狂造轮子-iOS]JSON转Model系列之二 本文转载请注明出处 —— polobymulberry-博客园 1. 前言 上一篇<[疯狂造轮子-iOS]JSON转Model系列之一> ...

  2. Mac iOS Json 操作Model to JSON

    在移动网络时代,json成为了主流的数据交换格式.如何能够方便快捷的创建.转化.传递json文件称为了开发者必备的技能.幸好,我们生活在开源时代,很多功能不需要我们重现造轮子.今天我推荐一款开源jso ...

  3. ios json转model的简单现实

    在android开发中,可用第三方的转换库如gson等.当然在ios也有一些库如MJExtensiond等.在这里,我简单实现一下. 一.先建一个model并且继承NSObject,代码如下: cla ...

  4. 重复造轮子系列——基于Ocelot实现类似支付宝接口模式的网关

    重复造轮子系列——基于Ocelot实现类似支付宝接口模式的网关 引言 重复造轮子系列是自己平时的一些总结.有的轮子依赖社区提供的轮子为基础,这里把使用过程的一些觉得有意思的做个分享.有些思路或者方法在 ...

  5. 造轮子系列之RPC 1:如何从零开始开发RPC框架

    前言 RPC 框架是后端攻城狮永远都绕不开的知识点,目前业界比较知名有 Dubbo.Spring Cloud 等.很多人都停留在了只会用的阶段,作为程序猿,拥有好奇心深入学习,才能有效提高自己的竞争力 ...

  6. GitHub Android 最火开源项目Top20 GitHub 上的开源项目不胜枚举,越来越多的开源项目正在迁移到GitHub平台上。基于不要重复造轮子的原则,了解当下比较流行的Android与iOS开源项目很是必要。利用这些项目,有时能够让你达到事半功倍的效果。

    1. ActionBarSherlock(推荐) ActionBarSherlock应该算得上是GitHub上最火的Android开源项目了,它是一个独立的库,通过一个API和主题,开发者就可以很方便 ...

  7. 自己造轮子系列之OOM框架AutoMapper

    [前言] OOM框架想必大家在Web开发中是使用频率非常之高的,如果还不甚了解OOM框架,那么我们对OOM框架稍作讲解. OOM顾名思义,Object-Object-Mapping实体间相互转换.常见 ...

  8. 重复造轮子系列——基于FastReport设计打印模板实现桌面端WPF套打和商超POS高度自适应小票打印

    重复造轮子系列——基于FastReport设计打印模板实现桌面端WPF套打和商超POS高度自适应小票打印 一.引言 桌面端系统经常需要对接各种硬件设备,比如扫描器.读卡器.打印机等. 这里介绍下桌面端 ...

  9. 「iOS造轮子」之UIButton 用Block响应事件

    俗语说 一个不懒的程序员不是好程序员 造轮子,也只是为了以后更好的coding. coding,简易明了的代码更是所有程序员都希望看到的 无论是看自己的代码,还是接手别人的代码 都希望一看都知道这代码 ...

随机推荐

  1. SQL Server 大数据搬迁之文件组备份还原实战

    一.本文所涉及的内容(Contents) 本文所涉及的内容(Contents) 背景(Contexts) 解决方案(Solution) 搬迁步骤(Procedure) 搬迁脚本(SQL Codes) ...

  2. NYOJ 1007

    在博客NYOJ 998 中已经写过计算欧拉函数的三种方法,这里不再赘述. 本题也是对欧拉函数的应用的考查,不过考查了另外一个数论基本定理:如何用欧拉函数求小于n且与n互质所有的正整数的和. 记eule ...

  3. 一个粗心的Bug,JSON格式不规范导致AJAX错误

    一.事件回放  今天工作时碰到了一个奇怪的问题,这个问题很早很早以前也碰到过,不过没想到过这么久了竟然又栽在这里. 当时正在联调一个项目,由于后端没有提供数据接口,于是我直接本地建立了一个 json ...

  4. Microservice架构模式简介

    在2014年,Sam Newman,Martin Fowler在ThoughtWorks的一位同事,出版了一本新书<Building Microservices>.该书描述了如何按照Mic ...

  5. 按需加载.js .css文件

    首先,理解按需加载当你需要用到某个js里面的函数什么鬼,或者某个css里的样式的时候你才开始加载这个文件. 然后是怎样实现的,简单来说就是在js中动态的createElem<script> ...

  6. Hawk 4.6 并行化

    并行化 Hawk支持单机并行化,也就是使用多线程获取数据.它可以控制目前所有任务的数量,为了不给网站造成过大的压力,仅当任务池中的任务数量小于一定值后,才会插入新的任务. 你可以在数据清洗的 执行面板 ...

  7. 算法与数据结构(十六) 快速排序(Swift 3.0版)

    上篇博客我们主要聊了比较高效的归并排序算法,本篇博客我们就来介绍另一种高效的排序算法:快速排序.快速排序的思想与归并排序类似,都是采用分而治之的方式进行排序的.快速排序的思想主要是取出无序序列中第一个 ...

  8. 玩转spring boot——结合redis

    一.准备工作 下载redis的windows版zip包:https://github.com/MSOpenTech/redis/releases 运行redis-server.exe程序 出现黑色窗口 ...

  9. C# Entity Framework并发处理

    原网站:C# Entity Framework并发处理 在软件开发过程中,并发控制是确保及时纠正由并发操作导致的错误的一种机制.从 ADO.NET 到 LINQ to SQL 再到如今的 ADO.NE ...

  10. springMVC初始化绑定器

    单日期 在处理器类中配置绑定方法  使用@InitBinder注解 在这里首先注册一个用户编辑器 参数一为目标类型   propertyEditor为属性编辑器,此处我们选用 CustomDateEd ...