《iOS進階指南》試讀之《Mantle解析》

發表於2016-06-21

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

 

相關文章