读完这篇文章你可以自己写一个 YYModel 这样的神器,这篇文章类似一个源码解析,但不同的是,它不光光是解析,更是实战,因为我觉得学习一个东西必须要自己写一遍才算是真的学了一遍,否则即便是读完了源码印象还是不会太深刻,so,开始吧。
注:为了简单起见,我的例子只是实现了一个精简的版本,YYModel 有很多功能,我这里就实现了一个核心的功能,JSON -> Model。
注:文章的最后有完整的代码
从JSON映射到Model的原理
想一下平时我们是怎么使用类似这样子的库的,当我们有一个JSON的时候,我们把所有JSON的字段(比如name、page)全部写成对应的类中的属性。然后库会自动把你JSON对应字段的值赋值到这些对应的属性里去。属性我们用 @property 来定义,就意味着编译器会帮你生成对应的get``set方法,我们使用的 . 其实也是在调用get``set方法来实现赋值的。在 Objective-C 中有一个著名的函数 objc_msgSend(...) 我们所有的类似 [obj method] 的方法调用(发送消息)都会被转换成 objc_msgSend(...) 的方式来调用。(具体这个函数怎么用后面再说)
所以对于一个库来说,要调用你这个 Model 的 set 方法,用 objc_msgSend(...) 会容易的多,所以JSON映射到Model的原理其实就是调用这个函数而已。
所以整个流程就是,你给我一个 Model 类,我会用 runtime 提供的各种函数来拿到你所有的属性和对应的get``set,判断完相应的类型以后,调用objc_msgSend(…)。说起来真的非常简单,做起来就有点麻烦…
前期的准备工作
为了后面的方便,我们需要把一些关键的东西封装起来,我们需要单独封装 ivar property method,也就是实例变量、属性、方法,但事实上我们的这个精简版的YYModel并不需要 method ivar 的封装,为了保证完整性,我还是打算写下来。
封装 ivar
先来封装 ivar,看一下头文件 CPClassIvarInfo.h(YYModel只有4个文件,两个 .h 两个 .m 我为了让代码看起来更清楚,所以我自己在重写 YYModel 的时候把所有可以拆出来的类都分别拆成了一对.h .m)并把前缀改成了 CP 意思是 copy。
1 | #import <Foundation/Foundation.h> |
Ivar 代表一个实例变量,你所有和实例变量有关的操作都必须要把 Ivar 传进去,等一下就能看到。
name 是这个实例变量的变量名
typeEncoding 是对类型的编码,具体可以看这里 对于不同的类型就会有对应的编码,比如 int 就会变编码成 i,可以用 @encode(int)这样的操作来看一个类型的编码。
type 是一个自定义的枚举,它描述了 YYMode 规定的类型。
一个强大的枚举
然后重新再创建一个文件(CPCommon),作为一个公共的文件 CPEncodingType 这个枚举就写在这里。
我们要创建的这个枚举需要一口气表示三种不同的类型,一种用于普通的类型上(int double object),一种用来表示关键词(const),一种表示 Property 的属性(Nonatomic weak retain)。
我们可以用位运算符来搞定这三种类型,用8位的枚举值来表示第一种,16位的表示第二种,24位的表示第三种,然后为了区别这三种类型都属于多少位的,我们可以分别搞三个 mask ,做一个该类型和某一个 mask 的与(&)的操作就可以知道这个类型是具体是哪一个类型了,例子在后面。
这个枚举我们可以这样定义:
1 | typedef NS_OPTIONS(NSUInteger, CPEncodingType) { |
比如有一个类型是这样的
1 | CPEncodingType type = CPEncodingTypeDouble; |
假设我们并不知道它是 CPEncodingTypeDouble 类型,那我们要怎么样才能知道它是什么类型呢?只要这样:
1 | NSLog(@"%lu",type & CPEncodingTypeMask); |
输出: 12
在枚举的定义中
1 | CPEncodingTypeDouble = 12, |
假设这个枚举值有很多种混在一起
1 | CPEncodingType type = CPEncodingTypeDouble | CPEncodingTypePropertyRetain; |
可能有人知道这种神奇的用法,但在我读YYModel之前我没用过这种方法(技术比较菜)。
然后还有一个函数,这个函数可以把类型编码(Type Encoding)转换成刚才的枚举值,很简单却很长的一个函数:
1 | CPEncodingType CPEncodingGetType(const char *typeEncoding) { |
很简单,不用多讲了。
回到 CPClassIvarInfo 刚才我们只给出了头文件,现在看一下实现。
1 | - (instancetype)initWithIvar:(Ivar)ivar { |
只有一个方法,这里就用到了两个 runtime 函数 ivar_getName(ivar) 和 ivar_getTypeEncoding(ivar) 传入 ivar 就行。
封装Method
然后看一下对于 Method 的封装,看一下头文件(CPClassMethodInfo.h)
1 | #import <Foundation/Foundation.h> |
Objective-C 的 Optional
NS_ASSUME_NONNULL_BEGIN 和 NS_ASSUME_NONNULL_END 是成对出现的,因为 Swift 可以和 Objective-C 混用,但是 Swift 有 Optional 类型,而 Objective-C 没有这样的概念,为了和 Swift 保持一致,现在 Objective-C 有了 _Nullable 可空 _Nonnull不可空这样的关键字,这两个关键字可以在变量、方法返回值、方法参数上使用,比如:
1 | @property (nonatomic, strong) NSString * _Nonnull string; |
还有另外一对 nullable nonnull,它们可以这样用
1 | @property (nullable, nonatomic, strong) NSString *string; |
对了,这些关键词只能用在指针上,其他类型是不能用的。
当你一旦在某个地方写上关键词 nullable的时候,编译器就会提出警告,Pointer is missing a nullability type specifier (_Nonnull, _Nullable, or _Null_unspecified) 然后你就可以加上NS_ASSUME_NONNULL_BEGIN 和 NS_ASSUME_NONNULL_END来表示只有我标记为 nullable 的地方才可空,其余地方都是 nonnull。
回到刚才的头文件代码,method 表示一个方法
name 很明显就是方法名了
sel 和 imp 是一个对应关系,一个对象的所有方法都会保存在一张表里,通过 sel 就能找到这个方法的 imp,我讲的可能有点简单,如果想要深入的了解可以查一下文档或者博客。
typeEncoding 又是一个编码,这里是参数和返回值的编码
returnTypeEncoding 返回值的编码
argumentTypeEncodings 所有参数的编码
实现还是很简单
1 | - (instancetype)initWithMethod:(Method)method { |
和前面套路一样。
封装 Property
老样子,看头
1 | #import <Foundation/Foundation.h> |
这是在精简版的YYModel中会用到的一个类,这里尤其要注意的是type和typdEncoding两个属性,希望读者能够仔细调试一下,看一下主要的一段代码:
1 | CPEncodingType type = 0; |
我们通过property_copyAttributeList这个函数得到一个指向一个结构体objc_property_attribute_t的指针,这个结构体的结构如下:
1 | typedef struct { |
说是一个指针,其实它是一个结构体数组,指针指向的其实是这个数组第一个元素。
这个结构体表示的是一个 Property 的属性,关于 Property 的类型编码可以看这里
要说清这个数组里每一个结构体元素的name和value都存了什么,我们可以看一下下面这段代码:
1 | Class cls = objc_getClass("CPBook"); |
这里比如有一个类是 CPBook ,我们通过这个类的 Class 来拿到一个叫做 name 的 Property,然后在拿到这个 Property 所有属性,输出的结果是 T@"NSString",&,N,V_name
其实,我们用和上面一样返回一个结构体数组的方式来获取这个 Property 的属性的话,那么这个结构体应该会有4个元素。
第一个元素 name = T,value = @"NSString",第二个元素 name = &,value 没有值,第三个元素 name = N,value 仍然没有值,第四个元素 name = V,value = _name。不信可以运行一下下面的代码来看看。
1 | Class cls = objc_getClass("CPBook"); |
至于 V N & 这样的符号是什么意思,可以打开上面给出的链接自己看一下文档,一看便知。
这样一来在 switch 分支中,只要匹配到 T 就能得到这个 Property 的类型是什么,这样就可以得到这个类型的 Type Encoding,并且能够得到该类的 Class。只要匹配到 V 就能得到这个 Property 实例变量名。
该类全部代码如下:
1 | - (instancetype)initWithProperty:(objc_property_t)property { |
这样一来,我们就有了 ivar Method Property 的封装类。接下来,我们需要一个叫做CPClassInfo的类,来封装一些类的信息,并且把以上三个类也封装进去,用来描述整个类。
封装 Class
继续看头:
1 | #import <Foundation/Foundation.h> |
Class 类型用来描述一个类,你可以使用
1 | model.class |
等方法来取到这个 Class。·注意object_getClass()和其他方式 有些不同具体看这里
其余的 Property 不用多介绍了,看到它们的名字就大概能猜到干嘛的了。
最后的几个 NSDictionary 用来存所有的 ivar Method Property。
有些时候,一个类有可能被更改,可能改掉了方法或者是 Property,那么这时候应该通知CPClassInfo来重新获取到更改过后的类的信息。所以我们有两个相关的方法来实现这个目的。
1 | - (void)setNeedUpadte; |
先来看一下初始化方法
1 | - (instancetype)initWithClass:(Class)cls{ |
你没看错,这和头文件定义的classInfoWithClass:不是一个方法,头文件定义的那个方法用来缓存,因为实例化这个方法还是有点开销的,所以没有必要每一次都去实例化。
这里有一个 _update 方法,刚才说过,如果这个类会在某一个时刻发生变化,应该通知,收到通知后,我们去执行一些更新的操作,所以把会发生变化的一部分代码单独拿出来更好,现在看一下 _update 方法。
1 | - (void)_update{ |
其实这个方法就是拿到一个类所有的 ivar Method Property ,一个类发生变化是不是主要就是这三个玩意的变化?
最后一行的 _needUpdate 是一个全局变量,用来标识是否发生的变化,它被定义在这里,以免暴露给外面。
1 | @implementation CPClassInfo{ |
当外界需要通知自己已经发生变化或者查一下是否发生变化时就调用这两个相关方法
1 | - (BOOL)needUpdate { |
现在来看一下classInfoWithClass:
1 | + (instancetype)classInfoWithClass:(Class)cls{ |
两个 NSMutableDictionary 都是用来缓存的,并声明在了静态区,并且使用dispatch_once()来确保只会被初始化一次,然后我们需要保证线程安全,因为有可能会在多线程的场景里被用到,所以使用信号量dispatch_semaphore_t来搞定,信号量就像停车这样的场景一样,如果发现车满了,就等待,一有空位就放行,也就是说,当一个线程要进入临界区的时候,必须获取一个信号量,如果没有问题就进入临界区,这时另一个线程进来了,也要获取,发现信号量并没有释放,就继续等待,直到前面一个信号量被释放后,该线程才准许进入。我们可以使用dispatch_semaphore_wait()来获取信号量,通过dispatch_semaphore_signal()来释放信号量。
在这段代码里,我们首先确保要实例化的这个对象有没有被缓存,用传进来的 cls 作为 key,如果缓存命中,那直接取出缓存,然后判断一下,有没有更新,如果有更新,调用_update刷新一遍,返回,否则直接返回。缓存没有命中的话,还是乖乖的调用实例化方法,然后缓存起来。
继续封装
CPModelPropertyMeta
先建一个文件,叫做 CPMeta.h 和 CPMeta.m,我们要在这里写两个类,一个是对 Property 的再次封装,一个是对 Class 的再次封装。
我直接把头文件代码全拿出来了:
1 | #import <Foundation/Foundation.h> |
可以看到这里有两个类,姑且叫做 CPModelPropertyMeta 和 CPModelMeta 以及一个枚举,这个枚举表示一个NS的类型,因为在上一个枚举当中,我们对于对象只定义了 CPEncodingTypeObject 这一个类型,没法区分它到底是 NSString 还是别的,所以这里要细化一下,类型判断清楚很重要,如果不把这部分做好,那么在JSON转换的时候,类型上出错就直接蹦了。
先来看一下 CPModelPropertyMeta 。(在 YYModel 中,这两个类其实是和一个叫做NSObject+CPModel的扩展放在一起的,但是我强制把它们拆出来了,为了看起来清楚,所以我把 @package 的成员变量都写到了 interface 里面,这么做是不合理的,但这里为了清晰和学习起见,所以我乱来了。)这个类中多了几个成员变量,我就说几个看起来不那么清楚的成员变量。
_isCNumber 这里变量表示是不是一个C语言的类型,比如int这样的。
_genericCls这个变量在精简版里没用到,我只是放在这里,YYModel 可以给容器型的属性转换,具体可以看YY大神的文档。
_isKVCCompatible 能不能支持 KVC
_mappedToKey 要映射的 key,把 JSON 转成 Model 的时会根据这个 key 把相同字段的 JSON 值赋值给这个 Property。
为了判断 NS 的类型和是否是 C 类型,在 .m 里有两个函数
1 | #define force_inline __inline__ __attribute__((always_inline)) |
这两个函数不用多说了,很简单,要说明一下宏定义 force_inline 所有标记了 force_inline 的函数叫做内联函数,在调用的时候都不是一般的调用,而是在编译的时候就已经整个丢进了调用这个函数的方法或函数里去了,这和平时定义一个宏一样,你在哪里使用到了这个宏,那么在编译的时候编译器就会把你使用这个宏的地方替换成宏的值。为什么要这么做呢?因为效率,调用一个函数也是有开销的,调用一个函数有压栈弹栈等操作。如果你的函数很小,你这么一弄就免去了这些操作。
然后看一下CPModelPropertyMeta的初始化方法
1 | + (instancetype)modelWithClassInfo:(CPClassInfo *)clsInfo propretyInfo:(CPClassPropertyInfo *)propertyInfo generic:(Class)generic{ |
判断一下是否是 object 的类型,然后拿到具体的 NS 类型,或者判断一下是不是 C 类型,然后拿到 getter setter 最后判断一下能不能 KVC。
CPModelPropertyMeta
这个类主要是生成一个映射表,这个映射表就是 _mapper 这个变量,这个类也需要被缓存起来,套路和上面讲到的缓存套路一样
1 | + (instancetype)metaWithClass:(Class)cls { |
缓存没命中就调用 initWithClass: 来进行初始化
1 | - (instancetype)initWithClass:(Class)cls{ |
把 CPClassInfo 里所有的 propertyInfo 遍历出来,实例化成一个 CPModelPropertyMeta ,还顺便把 CPClassInfo 父类的所有 propertyInfo 也拿出来,这样一来,你的 Model 即便有一个父类也能把父类的 Property 赋值。
然后生成一个映射表,就基本完成了初始化工作了,这张映射表是关键,等一下所有的 JSON 的转换都依赖这一张表。
从 JSON 到 Model 的转换
现在进入正餐环节,我们刚才已经把所有的准备工作完成了,现在要开始正式的完成从 JSON 到 Model 的转换了。
首先,先建一个 Category,取名 CPModel,因为我们只完成整个 YYMode 的一个主要功能,所以我们只给出一个接口就行了,所以头文件很简单。
1 | #import <Foundation/Foundation.h> |
使用者只需要调用 + modelWithJSON: 即可完成转换的操作。
现在看看这个方法要怎么实现:
1 | + (instancetype)modelWithJSON:(id)json { |
首先先把 JSON 转换成 NSDictionary ,然后得到该 Model 的 Class 去实例化这个 Model,接着调用一个叫做- modelSetWithDictionary: 的方法。
把 JSON 转换成 NSDictionary 的方法很简单
1 | + (NSDictionary *)_cp_dictionaryWithJSON:(id)json{ |
然后看一下 - modelSetWithDictionary:
1 | - (BOOL)modelSetWithDictionary:(NSDictionary *)dic{ |
这里有一个结构体,这个结构体用来存储 model(因为是给这个Model 里的 Property 赋值)、modelMeta(刚才也看到了,这里存放了映射表)、dictionary(这是由 JSON 转换过来的),这个结构体的定义如下:
1 | typedef struct { |
然后在- modelSetWithDictionary:有这么一行代码
1 | CFDictionaryApplyFunction((CFDictionaryRef)dic, ModelSetWithDictionaryFunction, &context); |
这个代码的作用是,把一对 Key - Value 拿出来,然后调用你传进去的函数ModelSetWithDictionaryFunction(),你有多少对Key - Value,它就会调用多少次这个函数,相当于便利所有的Key - Value,为什么要这样做,而不用一个循环呢?在YY大神的博客里有这么一段
遍历容器类时,选择更高效的方法
相对于 Foundation 的方法来说,CoreFoundation 的方法有更高的性能,用 CFArrayApplyFunction() 和 CFDictionaryApplyFunction() 方法来遍历容器类能带来不少性能提升,但代码写起来会非常麻烦。
然后我们来看一下ModelSetWithDictionaryFunction()的实现
1 | static void ModelSetWithDictionaryFunction(const void *key, const void *value, void *context) { |
为什么在变量前都加了__unsafe_unretained,YY大神也说了
避免多余的内存管理方法
在 ARC 条件下,默认声明的对象是 strong 类型的,赋值时有可能会产生 retain/release 调用,如果一个变量在其生命周期内不会被释放,则使用 unsafe_unretained 会节省很大的开销。
访问具有 weak 属性的变量时,实际上会调用 objc_loadWeak() 和 objc_storeWeak() 来完成,这也会带来很大的开销,所以要避免使用 weak 属性。
继续,根据 key(这个 key 就是 JSON 里的字段,应该和你 Model 定义的 Property 名相同,否则就匹配不了,在 YYMode 中有一个自定义映射表的支持,我把它去掉了,有兴趣的可以下载 YYMode 的源码看一下) 取出映射表里的 propertyMeta。现在我们有了要转换的 model 对象,和一个和 JSON 里字段对应的 propertyMeta 对象,已经该 JSON 字段的值,现在要赋值的条件全部具备了,我们只需要调用propertyMeta中的setter方法,然后把值传进去就完成了,这部分的工作由 ModelSetValueForProperty()函数完成,这个函数里有大量的类型判断,为了简单起见,我就判断了NSString NSNumber 和普通C语言类型,代码如下:
1 | static void ModelSetValueForProperty(__unsafe_unretained id model, __unsafe_unretained id value, __unsafe_unretained CPModelPropertyMeta *meta) { |
关于 objc_msgSend() 我们随便拿一行例子来举例,比如这个:
1 | ((void (*)(id, SEL, id))(void *) objc_msgSend)((id)model, meta->_setter, value); |
这是一个可以调用者决定返回值和参数的函数,一般的函数是做不到的,默认情况下这个函数是长这样
1 | objc_msgSend(id, SEL) |
id 是指调用某一个方法的对象,在这里这个对象就是你的 Model
SEL 是指你这个对象要调用的方法是什么,在这里这个方法就是 setter方法
然而,setter 方法是有参数的,这个参数怎么传进去?这就需要强制类型转换了,我们把这个函数强制转换成这个模样:
1 | ((void (*)(id, SEL, id))(void *) objc_msgSend) |
这样代表这个函数是一个没有返回值,并且有3个参数的函数,分别是 id SEL id,前面两个参数之前讲过了,第三个参数就是你要调用的这个 setter 方法需要的参数,所以经过强制类型转换之后的变异版就成了一开始的那种样子。
其余的都没有什么好讲的了,都很简单,都是一些烦人的类型判断,只要仔细一点一行行看就能看懂了。
全部搞定以后,和原版的 YYModel 一样,你可以这么来测试
1 | CPTestModel *model = [CPTestModel modelWithJSON:@"{\"name\": \"Harry Potter\",\"index\": 512,\"number\": 10,\"num\": 100}"]; |
结尾
如果你自己亲自动手写完了这个精简版的 YYMode ,你再去看完整版的会容易很多,我写的这篇文章是把我从读 YYModel 源码中学到的一些有用的东西分享给大家,如有什么写错的地方,欢迎指正。
完整代码
点击这里