《iOS进阶指南》试读之《Mantle解析》

Mantle

阅读一个库的源码,首先要知道,我们为什么需要这一类的库。

Mantle的目的

Mantle 的诞生是为了更方便的将服务端返回的数据映射为我们的 Model。
简单来说,我们在写 app 的时候,经常需要把服务端返回的数据和我们自己创建 model 关联起来,这样,在和 View 层交互的时候就可以使用 model 而不是直接使用字典。
那么,我们如果不使用 Mantle 的情况下。是如何创建一个 Model 并且把服务端返回的数据填充到这个Model里呢?我们来看看 Mantle 给的例子,一般是这样的。

然后 .m 文件里的实现一般是这样的。

想象一下,如果你的 Model 有几十种,相当于你要写几十次这样重复的代码。因此,为了减少这种重复性的工作,应运而生了 Mantle 这样的库。

Mantle的功能

了解了Mantle为何诞生。那么,我们就要看看Mantle到底为我们解决了什么样的问题?

  • 避免了写重复性的 init 方法,通过服务端返回的数据自动生成 model ,也可以利用model来反序列化生成 JSON 。
  • 当model里的某个属性名称和服务端的某个字段的名字不一样的时候,可以利用 + (NSDictionary *)JSONKeyPathsByPropertyKey 这个方法来进行字段的匹配。
  • 可以通过 +JSONTransformer 这个命名方法来声明一个转换方法。举例来说就是,一般服务端返回日期的时候是以时间戳的方式返回的,通常是一个长整形的数字,1464116217 ,但是你声明的对应的property是一个 NSDate 类型,这时候,你就需要进行长整形 -> 日期类型的转换,所以 Mantle 提供了这种方式,来进行方便的转换。
  • 自动的decode和encoding。方便 Model 的归档。

当然,Mantle 远不止这些功能。但是,我们先搞清楚主要的功能,那么一些次要的功能自然就迎刃而解了。

Mantle如何实现这些功能?

如何实现自动将Dictionary的值自动的赋给Model里对应的property?

我们先来尝试一下自己实现自动化的赋值。
熟悉OC的朋友应该知道NSObject有一个方法,叫做 - (void)setValuesForKeysWithDictionary:(NSDictionary *)keyedValues;
这个方法就是利用kvc,直接让 model 调用 - (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath; 方法,通过遍历传入的字典的key,对model进行赋值。
举个简单例子,现在我们有个Model叫做User。它的property和init方法如下。

然后我们这样给它赋值。

打印出来的结果就是

但是,这里有可能会出现几个问题。

  1. 如果 Dictionary 里的某一个 key 的名字和Model里的property的名字不匹配,就会造成NSUnknownKeyException 然后直接崩溃。
  2. 没法进行类型判断,如果你的dictionary里某个key对应的值和你的model里相同的key对应的值得类型不一致,他没有办法自动转换,而且完全不会报错。

当然,你可能会说,第一个问题,可以通过实现 - (void)setValue:(id)value forUndefinedKey:(NSString *)key 这个方法来进行检测和修复。
但问题是,如果大量的key不匹配的话,你又回到了原来的问题,需要写大量重复的代码。

这样分析下来,直接使用 setValuesForKeysWithDictionary 并不能实现我们的需求。

Mantle 是如何做的?

显然,Mantle需要解决的第一个问题就是,如何建立起 Model 里的 property 和 Dictionary 里的key 一一对应的关系。
这个显然是需要用户提供的。
因为不同的 App 服务端返回的数据五花八门,命名方式有可能是驼峰命名也有可能不是,那么我们定义的 Model 也一样。如何才能建立起这种关系呢?
Mantle 的做法是,你需要

  1. 创建的 Model 需要继承自MTLModel
  2. 必须实现 MTLJSONSerializing 协议。

先看代码。

.m 文件中是这样的

我们来思考一下,为什么这样写就可以建立起 model 和 Dictionary 一一对应的关系。

首先,服务端返回给我们的 Car 信息的JSON文件中,键值对分别是, name 对应的是汽车的名称,ownner 对应的是汽车的拥有者,但是我们在创建 Model 的时候,汽车的名字是 carName,拥有者的名字是carOwnner ,所以,我们需要告诉 Mantle,在利用 kvc 赋值的时候。

实际上走到这的时候第一步就错了,为什么?
因为dictionary里根本就没有 carName 这个key,你拿到的是nil。
所以,应该如何处理这种 model 里的 key 和 字典里 key 不一致的情况?
Mantle 的处理方法是,你需要告诉我,model 的 property 的名字和字典里的 key 存在怎样的一种对应关系。
所以,当 Mantle 在遍历Car这个类的Property列表的时候,,应当先去用户在 JSONKeyPathsByPropertyKey 方法中传回的字典里寻找,是否有Property对应的服务端的key,再利用这个对应的key去Dictionary里拿数据,再赋值给我们的property。比如上面这个例子,就应该把carName替换为name之后再从字典取值,然后再把取得的值赋给carName这个property。
需要注意的是,如果 JSONKeyPathsByPropertyKey 里没有填写任何对应关系,最新版本Mantle是不会默认 Model 里的key和字典中的key 相同的,而是直接跳过。但是早些版本的Mantle会默认这种相同的关系,直接赋值。
本着严谨的精神,我去查看了一下 Mantle release 记录,看看到底是哪个版本的 Mantle 取消了这种默认的行为。
在 2.0 版本的ChangeLog里看到了下面一段话。

1
2

所以,在 2.0 版本, Mantle 取消了这种隐式的转换关系。
在这个说明里,作者还提到了一个方法,+[NSDictionary mtl_identityPropertyMapWithModel:] ,这个方法如何使用呢?当你的 Model 里的所有属性的名字和 JSON 里的所有 key 的名字完全相同的时候,你就可以用这个方法直接生成一个 NSDictionary, 直接返回。省掉了自己写。例如.

分析源码

说了这么多。我们就来看看Mantle到底如何实现这么多好用的功能。

先看看 Mantle的目录结构。

3

主要模块有:

  • Modules 主要负责最基础的功能,包括通过 runtime 获取Class的property,encode 和 decode 功能。
  • Adapters 主要负责JSON Model转换的核心逻辑。
  • ValueTransform 主要负责某个Property需要进行自定义转换的需求。例如服务端返回的时间戳 -> Model中声明的NSDate类型这种转换。
  • libextobjc 将property_attribute这种Type Encodings过得东西转化为对应的Model。后面会细讲。

基础知识

1.如何利用Runtime获取一个类的所有Property的信息?

之前已经说过了,想要实现ORM的基本条件就是,获取一个类的所有属性的名字。拿不到属性的名字,就没法利用kvc赋值。
怎么拿?代码如下。

打印的结果如下。

2.上一个例子里打印出来的property_attribute是什么?

名字好理解,输出的和我们声明的property名字一致,这个没什么问题。但是后面那个attributes是个什么东西?
我们声明的@property (nonatomic, copy) NSString *name;通过runtime取出来之后的形式是这样的。
T@"NSString",C,N,V_name
关于这样的一段字符串到底是什么意思,苹果官方文档是这么说的

The string starts with a T followed by the @encode type and a comma, and finishes with a V followed by the name of the backing instance variable. Between these, the attributes are specified by the following descriptors, separated by commas:

然后我画了一张图来解释这一段字符串到底是什么意思。

4

至于每个符号的意思,你们可以从苹果的官方文档中找到。地址在:https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtTypeEncodings.html#//apple_ref/doc/uid/TP40008048-CH100-SW1

所以,我们可以通过property_getName获取到property的名字,也可以通过property_getAttributes获取到property的一些属性。这样,也就迈出了第一步,起码,我们能知道一个类都有那些属性,这些属性叫什么,都是怎么声明的。

3.解析EXTRuntimeExtensions这个类,先看看.h文件

Mantle中对应上述功能的就是这个类。
我们来看看Mantle是如何解析type encoding过的属性的。

首先,Mantle定义了一个枚举类型,用来记录一个property的内存管理方式,枚举中有三种类型。分别对应iOS中的assign,retain,copy.
枚举的名字叫做mtl_propertyMemoryManagementPolicy.
然后,Mantle声明了一个名为mtl_propertyAttributes的struct用来记录一个property的各种属性。
这个struct有下列成员。
readonly: 一个Bool值,用来记录这个property是否为readonly.
nonatomic: Bool。记录是否为nonatomic
weak: Bool。memory manage的方式是否为weak。
canBeCollected:Bool。是否可以被垃圾回收机制管理。(实际上iOS并没有自动垃圾回收机制。OSX以前有过,但是在mountain lion之后就被禁用了。)
dynamic: Bool.是否被声明为dynamic。
memoryManagementPolicy:即Mantle自己定义的枚举类型,用来记录property的内存管理方式,如果这个property是readonly的时候,那么这个值永远为mtl_propertyMemoryManagementPolicyAssign.
getter: SEL。即记录这个property的自定义getter方法。
setter: SEL.记录这个property的自定义setter方法
ivar: const char 类型。记录这个property对应的成员变量。例如@property (nonatomic, copy) NSString user,那么对应的成员变量应该为_user.objectClass: 如果这个property的类型是一个自定义的类。那么这个objectClass用来记录这个类。char type[]: 用来记录这个property的type encoding之后的值。例如,user这个property的类型是NSString,那么type encoding之后,NSString会被编译器转化成@这个标记,所以这个type里存储的就是@.

头文件中还定义了一个方法。
mtl_propertyAttributes *mtl_copyPropertyAttributes (objc_property_t property);
这个方法是干嘛的呢?
很简单,就是把我们的objc_property_t类型转换成我们的mtl_propertyAttributes的struct。

接下来,我们来看看.m文件中,Mantle是如何把一个runtime中的objc_property_t转换成struct的。

再来看看EXTRuntimeExtensions.m

 

1 2 收藏 评论

相关文章

可能感兴趣的话题



直接登录
跳到底部
返回顶部