作者:李富強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上面已經進行了講解,後面的代碼邏輯還是相對簡單的,很容易就能看明白這個過程中做了什麼事情。