Mantle 源代码阅读笔记 一
作者:李富强Jason
最近的项目需求需要持久化一些对象,由于只是一些比较简单的数据,使用NSUserDefaults进行存储即可。之前实现过比较简单自动archive和unarchive的操作。原理很简单,遍历NSObject的property list,然后通过valueForKey:和setValue:forKey:方法进行操作。这种实现不能满足我的新需求,我的新需求需要做到将property为其他类型的对象也做到自动archive和unarchive,再加上JSON解析方面的工作量,直接粗暴通过硬编码实现会产生一大堆verbose的代码,自己实现需要自动化archive和unarchive的代码需要的工作量较大。于是顺便看了一下Mantle的源代码,发现其中这方面的处理很不错,各方面很合理,就通过这个实现了。
Mantle解析JSON或者NSCoding操作我认为实际上都可以分成两个大步骤来阅读:Transform 和 赋值 。Mantle的源代码不是很多,但是代码很干净,注释也很完善。
我把全部文件根据我认为的步骤进行了一下分类:
1. Transform相关:
MTLJSONAdapter
MTLManagedObjectAdapter
MTLValueTransformer
NSValueTransformer+MTLInversionAdditions
NSValueTransformer+MTLPredefinedTransformerAdditions
MTLModel+NSCoding
2.赋值相关:
MTLModel
3.工具类:
MTLReflection:
NSArray+MTLManipulationAdditions
NSDictionary+MTLManipulationAdditions
NSError+MTLModelException
NSObject+MTLComparisonAdditions
4. extobjc:
MTLEXTKeyPathCoding
MTLEXTRuntimeExtension
MTLEXTScope
metamacros
从NSDictionary到Model
把JSON数据解析为Model只需要下面两行代码即可:
Transform过程
从JSON转换到model,方法入口是在 MTLJSONAdapter 的 modelOfClass:fromJSONDictionary:error: ,详细逻辑的实现方法是 - (id)initWithJSONDictionary:modelClass:error: 。这个方法在入口处进行了assert,modelClass的类型必须是MTLModel的子类,同时modelClass必须实现MTLJSONSerializing protocol。
接下来就是上面这段代码,这段代码比较有意思,它涉及到一个我们经常使用却不太在意的东西,类簇(class cluster),这个设计模式在Cocoa中使用很广泛,最明显的例子是NSNumber,关于class cluster可以参考: https://developer.apple.com/library/ios/documentation/General/Conceptual/CocoaEncyclopedia/ClassClusters/ClassClusters.html 。这里我们只需要知道,如果在使用Mantle的过程中,我们要使用class cluster,只需要实现这个方法,然后返回具体的class类型即可。
========================================================================
接下来我们需要处理 JSONKeyPathsByPropertyKey 返回的值,这个值是model的property与JSON的key之间的映射关系,例如官方示例JSONKeyPathsByPropertyKey中返回的数据如下:
在把原始JSON数据转换为model需要的Dictionary之前,对JSONKeyPathsByPropertyKey的返回的数据进行验证,在上面这段代码中就是做这个事情,主要验证两方面,一方面,JSONKeyPathsByPropertyKey中要求的model的property在modelClass中是否存在,另外一方面,JSONKeyPathsByPropertyKey要求JSON的key是否是合法的,必须是NSString或者NSNull.null。
其中,NSNull.null表示忽略model中的这个property( http://stackoverflow.com/questions/18961622/how-to-omit-null-values-in-json-dictionary-using-mantle ),不进行赋值,这个需求也是经常遇到的。
这里的 [self.modelClass propertyKeys]; 后面在runtime部分会在讲解一下,这里只需要知道,它返回了modelClass的property列表即可。
========================================================================
完成对 JSONKeyPathsByPropertyKey 返回的验证之后,下面要做的是把JSON数据transform为model的property要求的类型,比如说JSON数据中返回的url对应的数据是字符串,但是model中的URL要求的是NSURL *类型,这个转换过程就是在这个步骤中完成的。
首先需要说一下NSValueTransformer,这个东西在我没有使用Mantle之前也不太了解,看来一下Mantle中的使用,发现这个东西的确是非常方便灵活,与NSURLProtocol类似,正常来说Transformer的使用需要通过register class,Mantle中的用法则是直接获取transformer对象,关于NSValueTransformer可以参考: http://nshipster.com/nsvaluetransformer/ 。
如下面这段代码,遍历propertyKeys,这个是modelClass的属性列表,这个列表是通过MTLModel的+propertyKeys方法获取的。通过propertyKey获取它对应JSON的key(-JSONKeyPathForPropertyKey:),得到key值JSONKeyPath之后,校验JSONDictionary中这个JSONKeyPath中的value是否合法,如果校验过程中抛出异常,则catch之后,返回nil,设置error。
上面这段代码中,第一个比较有意思的地方是,valueForKeyPath: VS objectForKey:, http://stackoverflow.com/questions/4489684/what-is-the-difference-between-valueforkey-objectforkey-and-valueforkeypath ,对NSDictionary来说,这两个差异不是特别明显。第二个比较有意思的是NSLog中使用的“%1$@”这种输出格式, http://stackoverflow.com/questions/19327441/gcc-dollar-sign-in-printf-format-string 。
========================================================================
下一步要做的事情就是整个transform的核心步骤了,我们首先需要把JSON数据转换为model所需要的类型,这个需要通过获取NSValueTransformer来进行转换,所以第一步是获取一个NSValueTransformer对象。
使用官方的demo作为示例,看看这个NSValueTransformer在model中是如何生成的,下面这个是写在model中的一个静态方法,返回一个NSValueTransformer对象,这个对象不仅支持JSON transform到Model,也支持Model 进行reverse transform到JSON,分别对应下面两个block中的代码块。比如下面这个例子中,JSON转换为model的时候,JSON数据中的string会被转换为NSDate *,在model被转换为JSON的时候,NSDate*会被转为JSON中数据string。
adapter通过 -JSONTransformerForKey: 方法来获取一个 NSValueTransformer,这个方法的代码实现如下。首先,通过MTLSelectorWithKeyPattern来生成selector,OC的方法签名比较简单,基本上使用字符串就行进行调用,这里的生成的规则是把属性的名称xxx与JSONTransformer进行字符串拼接。OC的灵活让我们可以通过字符串就能进行消息发送,这里后面会详细解析用法。可以看到除了按一定规则拼接selector的方式从model中获取transformer之外,最后还会调用model的+JSONTransformerForKey: 方法,当然这个很少用,写一大堆if/else判断代码阅读起来肯定不够干净。
讲完如何获取JSONTransformer之后,我们下面开始看一下transform阶段比较关键的代码,下面的代码实际上非常好阅读和理解,获取transformer之后,调用transformer的-transformedValue: 来直接将JSON中的value转换为我们需要的类型。这里可以看出Mantle对NSNull.null的处理,不用我们担心JSON中的null导致程序crash的问题。转换到我们需要的数据类型之后,接下来会把这个数据存放到一个dictaionaryValue临时字典中,JSON中所有的数据都transform之后,我们就能得到我们需要数据类型的dictaionary了,后面我们会利用这个dictionary来对model进行赋值操作。同时,可以看看这里对try/catch以及NSError的利用,这个是个比较简单,但是我们日常开发中却经常疏漏的东西,这种技巧在商业代码中使用是很有必要的。
通过上面的步骤,我们就能得到JSON转换我们需要的数据类型之后的结果,看下面的代码,就进入了赋值的阶段。
========================================================================
赋值过程
上面这段代码是赋值阶段的处理逻辑,这段代码读起来依然非常简洁清晰,我们从转换完的dictionary中取出value,然后把这个value赋值给对象的property。除了这里以及对NSNull.null做了额外处理之外,核心逻辑基本上都在MTLValidateAndSetValue()函数中。一开始我以为这个过程会直接去操作OC的property去实现,所以过程会比较复杂,但是看了代码之后发现,感谢KVC,整个过程非常简单却又很实用。
通过 validateValue:forKey:error: 方法来对赋值的合法性进行校验,校验合法之后,直接通过 setValue:forKey: 方法进行赋值即可,通过KVC让整个流程变得非常简洁。这里有个需要注意的地方,如果transformer转换之后的任意一个value与model的property不匹配,则整个model转换的过程就会失败,而不仅仅是这个property发生失败!
从Model到NSDictionary
把Model序列化为JSON数据,同样只需要下面两行代码即可:
入口方式MTLJSONAdapter的JSONDictionaryFromModel:error: 方法,详细逻辑实现在MTLJSONAdapter的 -initWithModel: 和 -JSONDictionary,initWithMode: 方法要求传入的model是继承于MTLModel并且conforms to MTLJSONSerializing 的。详细的转换和赋值过程都是在JSONDictionary方法中完成的,这个方法的代码不算长,直接把代码贴出来,然后解析其中的逻辑。
首先是第一行代码,self.model.dictionaryValue,这个是包含了model的property的key以及这个property的value的一个dictionary,我刚开始认为的做法是通过runtime实现,但是看来mantle的实现,发现需要再次感谢KVC,用非常简单的代码就实现了很强大的功能,下面的代码中,self.class.propertyKeys 对它我们已经有了说明,后面会再细说一下,这里比较有意思的方式 dictionaryWithValuesForKeys: 这个方法也是属于NSKeyValueCoding protocol的
看来一下头文件中 dictionaryWithValuesForKeys: 的说明,觉得之前对KVC的使用真的不够。
然后下面的逻辑与从NSDictionary到Model中的做法类似,同样是创建一个临时的NSMutableDictionary,然后通过获取NSValueTransformer进行 reverseTransformedValue: 把property属性转换为可以JSON数据支持的类型,以便后面序列化为字符串。这个循环中的代码,JSONKeyPathForPropertyKey: 与 JSONTransformerForKey: 方法前面已经说过了,校验property keypath以及获取这个keypath对应的NSValueTransformer。注意其中,同样对NSNull.null做了特殊处理,可以看出Mantle中对使用JSON过程中常见的NSNull问题处理的比较干净的。
将转换完的数据设置到临时Dictionary的时候,如果JSONKeyPath为有多个step的路径时,这个时候的处理比较有意思
明白了从JSON转换到Model的代码之后,这部分从Model转换为JSON的代码就非常简单和容易理解了。可以看到由于KVC的存在,我们不用去操作runtime就能很灵活实现很多功能,把复杂的部分交给API交给框架去做。
NSCoding
Mantle中对NSCoding的支持的代码主要在MTLModel+NSCoding文件中,但是除了与NSCoder API交互的部分之外,比较核心的逻辑与前面看过的是类似的,尤其是Transform部分,这样就避免了业务层的不必要的工作量。与NSCoding相关的主要涉及到model需要override的两个方法:-initWithCoder: 和 encodeWithCoder: ,而MTLModel+NSCoding已经默认帮我们实现了这两个方法。这部分实际上主要的冗余代码在于secure coding和老版本的兼容代码,去除这些之后,我们日常使用的功能的话,实际上核心代码非常少,但是很完整。而且我自己感觉思路上与上面JSON转换的过程实际上是极其类似的,唯一不同的地方是transform的过程,JSON与Model之间转换的过程使用了NSValueTransformer,而NSCoding则是依赖于property实现了NSCoding。
encode
encoderWithCoder:的逻辑主要集中在encoderWithCoder:中实现,如上图中的代码。首先我们看看第一行代码,coderRequireSecureCoding是检验NSCoder是否是支持NSSecureCoding安全措施。关于NSSecureCoding,我找了半天没有找到合适的资料,最后在nshipster上面发现了一篇短文进行介绍,这个主要是为了解决substitution attack安全问题的, http://nshipster.com/nssecurecoding/ 。接下来是verifyAllowedClassesByPropertyKey函数,这个secure相关的校验函数,说实话,我没有读懂,看来几遍,发现exception永远不可能抛出。
下面一行可以看到,Mantle还提供了一个比较有意思的特性,版本号,这个实际用途还是相当大的。model变化较大时,尤其需要注意这方面的问题,使用这个版本号就可以很方便管理了。
后面的代码就相对简单了,用到了我们之前讲过的 self.dictionaryValue,然后结合encodingBehaviorsByPropertyKey一起来使用coder的API进行encode的过程。可以看到self.dictionaryValue与上面从model到JSON的转换过程的第一步是相同的,不同的地方在于transform的过程,这里并没有使用NSValueTransformer来进行transform,而是依赖于各个property需要实现NSCoding,这里说实话,感觉比较突然,我以为会复用NSValueTransformer的逻辑。
假如我们想要是Mantle不去encode某个property的话,做法也很简单,mantle这方面也有充分考虑。override encodingBehaviorsByPropertyKey ,然后将要额外处理的property与super的结合即可,如下:
decode
decode的过程与从JSON到model比较类似,不同的是,同样没有使用NSValueTransformer来进行value的变换,而是使用NSCoder的decode方法,这个跟上面的encode过程是对应的,不过感觉也是比较合理的,充分利用API提供的便利。赋值的过程与JSON到model的赋值过程是比较类似的,下面这段代码就是这个过程的主要的逻辑代码了。
self.class.propertyKeys上面已经进行了讲解,后面的代码逻辑还是相对简单的,很容易就能看明白这个过程中做了什么事情。