手把手帶你擼一個 YYModel 的精簡版

發表於2016-10-10

讀完這篇文章你可以自己寫一個 YYModel 這樣的神器,這篇文章類似一個原始碼解析,但不同的是,它不光光是解析,更是實戰,因為我覺得學習一個東西必須要自己寫一遍才算是真的學了一遍,否則即便是讀完了原始碼印象還是不會太深刻,so,開始吧。

注:為了簡單起見,我的例子只是實現了一個精簡的版本,YYModel 有很多功能,我這裡就實現了一個核心的功能,JSON -> Model

注:文章的最後有完整的程式碼

從JSON對映到Model的原理

想一下平時我們是怎麼使用類似這樣子的庫的,當我們有一個JSON的時候,我們把所有JSON的欄位(比如name、page)全部寫成對應的類中的屬性。然後庫會自動把你JSON對應欄位的值賦值到這些對應的屬性裡去。屬性我們用 @property 來定義,就意味著編譯器會幫你生成對應的getset方法,我們使用的 . 其實也是在呼叫getset方法來實現賦值的。在 Objective-C 中有一個著名的函式 objc_msgSend(...) 我們所有的類似 [obj method] 的方法呼叫(傳送訊息)都會被轉換成 objc_msgSend(...) 的方式來呼叫。(具體這個函式怎麼用後面再說)

所以對於一個庫來說,要呼叫你這個 Model 的 set 方法,用 objc_msgSend(...) 會容易的多,所以JSON對映到Model的原理其實就是呼叫這個函式而已。

所以整個流程就是,你給我一個 Model 類,我會用 runtime 提供的各種函式來拿到你所有的屬性和對應的getset,判斷完相應的型別以後,呼叫objc_msgSend(…)。說起來真的非常簡單,做起來就有點麻煩…

前期的準備工作

為了後面的方便,我們需要把一些關鍵的東西封裝起來,我們需要單獨封裝 ivar property method,也就是例項變數、屬性、方法,但事實上我們的這個精簡版的YYModel並不需要 method ivar 的封裝,為了保證完整性,我還是打算寫下來。

封裝 ivar

先來封裝 ivar,看一下標頭檔案 CPClassIvarInfo.h(YYModel只有4個檔案,兩個 .h 兩個 .m 我為了讓程式碼看起來更清楚,所以我自己在重寫 YYModel 的時候把所有可以拆出來的類都分別拆成了一對.h .m)並把字首改成了 CP 意思是 copy。

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 的與(&)的操作就可以知道這個型別是具體是哪一個型別了,例子在後面。

這個列舉我們可以這樣定義:

比如有一個型別是這樣的

假設我們並不知道它是 CPEncodingTypeDouble 型別,那我們要怎麼樣才能知道它是什麼型別呢?只要這樣:

輸出: 12

在列舉的定義中

假設這個列舉值有很多種混在一起

可能有人知道這種神奇的用法,但在我讀YYModel之前我沒用過這種方法(技術比較菜)。

然後還有一個函式,這個函式可以把型別編碼(Type Encoding)轉換成剛才的列舉值,很簡單卻很長的一個函式:

很簡單,不用多講了。

回到 CPClassIvarInfo 剛才我們只給出了標頭檔案,現在看一下實現。

只有一個方法,這裡就用到了兩個 runtime 函式 ivar_getName(ivar)ivar_getTypeEncoding(ivar) 傳入 ivar 就行。

封裝Method

然後看一下對於 Method 的封裝,看一下標頭檔案(CPClassMethodInfo.h)

Objective-C 的 Optional

NS_ASSUME_NONNULL_BEGINNS_ASSUME_NONNULL_END 是成對出現的,因為 Swift 可以和 Objective-C 混用,但是 Swift 有 Optional 型別,而 Objective-C 沒有這樣的概念,為了和 Swift 保持一致,現在 Objective-C 有了 _Nullable 可空 _Nonnull不可空這樣的關鍵字,這兩個關鍵字可以在變數、方法返回值、方法引數上使用,比如:

還有另外一對 nullable nonnull,它們可以這樣用

對了,這些關鍵詞只能用在指標上,其他型別是不能用的。

當你一旦在某個地方寫上關鍵詞 nullable的時候,編譯器就會提出警告,Pointer is missing a nullability type specifier (_Nonnull, _Nullable, or _Null_unspecified) 然後你就可以加上NS_ASSUME_NONNULL_BEGINNS_ASSUME_NONNULL_END來表示只有我標記為 nullable 的地方才可空,其餘地方都是 nonnull

回到剛才的標頭檔案程式碼,method 表示一個方法

name 很明顯就是方法名了

selimp 是一個對應關係,一個物件的所有方法都會儲存在一張表裡,通過 sel 就能找到這個方法的 imp,我講的可能有點簡單,如果想要深入的瞭解可以查一下文件或者部落格。

typeEncoding 又是一個編碼,這裡是引數和返回值的編碼

returnTypeEncoding 返回值的編碼

argumentTypeEncodings 所有引數的編碼

實現還是很簡單

和前面套路一樣。

封裝 Property

老樣子,看頭

這是在精簡版的YYModel中會用到的一個類,這裡尤其要注意的是typetypdEncoding兩個屬性,希望讀者能夠仔細除錯一下,看一下主要的一段程式碼:

我們通過property_copyAttributeList這個函式得到一個指向一個結構體objc_property_attribute_t的指標,這個結構體的結構如下:

說是一個指標,其實它是一個結構體陣列,指標指向的其實是這個陣列第一個元素。

這個結構體表示的是一個 Property 的屬性,關於 Property 的型別編碼可以看這裡

要說清這個陣列裡每一個結構體元素的namevalue都存了什麼,我們可以看一下下面這段程式碼:

這裡比如有一個類是 CPBook ,我們通過這個類的 Class 來拿到一個叫做 name 的 Property,然後在拿到這個 Property 所有屬性,輸出的結果是 T@"NSString",&,N,V_name

其實,我們用和上面一樣返回一個結構體陣列的方式來獲取這個 Property 的屬性的話,那麼這個結構體應該會有4個元素。

第一個元素 name = Tvalue = @"NSString",第二個元素 name = &value 沒有值,第三個元素 name = Nvalue 仍然沒有值,第四個元素 name = Vvalue = _name。不信可以執行一下下面的程式碼來看看。

至於 V N & 這樣的符號是什麼意思,可以開啟上面給出的連結自己看一下文件,一看便知。

這樣一來在 switch 分支中,只要匹配到 T 就能得到這個 Property 的型別是什麼,這樣就可以得到這個型別的 Type Encoding,並且能夠得到該類的 Class。只要匹配到 V 就能得到這個 Property 例項變數名。

該類全部程式碼如下:

這樣一來,我們就有了 ivar Method Property 的封裝類。接下來,我們需要一個叫做CPClassInfo的類,來封裝一些類的資訊,並且把以上三個類也封裝進去,用來描述整個類。

封裝 Class

繼續看頭:

Class 型別用來描述一個類,你可以使用

等方法來取到這個 Class。·注意object_getClass()和其他方式 有些不同具體看這裡

其餘的 Property 不用多介紹了,看到它們的名字就大概能猜到幹嘛的了。

最後的幾個 NSDictionary 用來存所有的 ivar Method Property。

有些時候,一個類有可能被更改,可能改掉了方法或者是 Property,那麼這時候應該通知CPClassInfo來重新獲取到更改過後的類的資訊。所以我們有兩個相關的方法來實現這個目的。

先來看一下初始化方法

你沒看錯,這和標頭檔案定義的classInfoWithClass:不是一個方法,標頭檔案定義的那個方法用來快取,因為例項化這個方法還是有點開銷的,所以沒有必要每一次都去例項化。

這裡有一個 _update 方法,剛才說過,如果這個類會在某一個時刻發生變化,應該通知,收到通知後,我們去執行一些更新的操作,所以把會發生變化的一部分程式碼單獨拿出來更好,現在看一下 _update 方法。

其實這個方法就是拿到一個類所有的 ivar Method Property ,一個類發生變化是不是主要就是這三個玩意的變化?

最後一行的 _needUpdate 是一個全域性變數,用來標識是否發生的變化,它被定義在這裡,以免暴露給外面。

當外界需要通知自己已經發生變化或者查一下是否發生變化時就呼叫這兩個相關方法

現在來看一下classInfoWithClass:

兩個 NSMutableDictionary 都是用來快取的,並宣告在了靜態區,並且使用dispatch_once()來確保只會被初始化一次,然後我們需要保證執行緒安全,因為有可能會在多執行緒的場景裡被用到,所以使用訊號量dispatch_semaphore_t來搞定,訊號量就像停車這樣的場景一樣,如果發現車滿了,就等待,一有空位就放行,也就是說,當一個執行緒要進入臨界區的時候,必須獲取一個訊號量,如果沒有問題就進入臨界區,這時另一個執行緒進來了,也要獲取,發現訊號量並沒有釋放,就繼續等待,直到前面一個訊號量被釋放後,該執行緒才准許進入。我們可以使用dispatch_semaphore_wait()來獲取訊號量,通過dispatch_semaphore_signal()來釋放訊號量。

在這段程式碼裡,我們首先確保要例項化的這個物件有沒有被快取,用傳進來的 cls 作為 key,如果快取命中,那直接取出快取,然後判斷一下,有沒有更新,如果有更新,呼叫_update重新整理一遍,返回,否則直接返回。快取沒有命中的話,還是乖乖的呼叫例項化方法,然後快取起來。

繼續封裝

CPModelPropertyMeta

先建一個檔案,叫做 CPMeta.hCPMeta.m,我們要在這裡寫兩個類,一個是對 Property 的再次封裝,一個是對 Class 的再次封裝。

我直接把標頭檔案程式碼全拿出來了:

可以看到這裡有兩個類,姑且叫做 CPModelPropertyMetaCPModelMeta 以及一個列舉,這個列舉表示一個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 裡有兩個函式

這兩個函式不用多說了,很簡單,要說明一下巨集定義 force_inline 所有標記了 force_inline 的函式叫做行內函數,在呼叫的時候都不是一般的呼叫,而是在編譯的時候就已經整個丟進了呼叫這個函式的方法或函式裡去了,這和平時定義一個巨集一樣,你在哪裡使用到了這個巨集,那麼在編譯的時候編譯器就會把你使用這個巨集的地方替換成巨集的值。為什麼要這麼做呢?因為效率,呼叫一個函式也是有開銷的,呼叫一個函式有壓棧彈棧等操作。如果你的函式很小,你這麼一弄就免去了這些操作。

然後看一下CPModelPropertyMeta的初始化方法

判斷一下是否是 object 的型別,然後拿到具體的 NS 型別,或者判斷一下是不是 C 型別,然後拿到 getter setter 最後判斷一下能不能 KVC。

CPModelPropertyMeta

這個類主要是生成一個對映表,這個對映表就是 _mapper 這個變數,這個類也需要被快取起來,套路和上面講到的快取套路一樣

快取沒命中就呼叫 initWithClass: 來進行初始化

CPClassInfo 裡所有的 propertyInfo 遍歷出來,例項化成一個 CPModelPropertyMeta ,還順便把 CPClassInfo 父類的所有 propertyInfo 也拿出來,這樣一來,你的 Model 即便有一個父類也能把父類的 Property 賦值。

然後生成一個對映表,就基本完成了初始化工作了,這張對映表是關鍵,等一下所有的 JSON 的轉換都依賴這一張表。

從 JSON 到 Model 的轉換

現在進入正餐環節,我們剛才已經把所有的準備工作完成了,現在要開始正式的完成從 JSON 到 Model 的轉換了。

首先,先建一個 Category,取名 CPModel,因為我們只完成整個 YYMode 的一個主要功能,所以我們只給出一個介面就行了,所以標頭檔案很簡單。

使用者只需要呼叫 + modelWithJSON: 即可完成轉換的操作。

現在看看這個方法要怎麼實現:

首先先把 JSON 轉換成 NSDictionary ,然後得到該 Model 的 Class 去例項化這個 Model,接著呼叫一個叫做- modelSetWithDictionary: 的方法。

把 JSON 轉換成 NSDictionary 的方法很簡單

然後看一下 - modelSetWithDictionary:

這裡有一個結構體,這個結構體用來儲存 model(因為是給這個Model 裡的 Property 賦值)、modelMeta(剛才也看到了,這裡存放了對映表)、dictionary(這是由 JSON 轉換過來的),這個結構體的定義如下:

然後在- modelSetWithDictionary:有這麼一行程式碼

這個程式碼的作用是,把一對 Key - Value 拿出來,然後呼叫你傳進去的函式ModelSetWithDictionaryFunction(),你有多少對Key - Value,它就會呼叫多少次這個函式,相當於便利所有的Key - Value,為什麼要這樣做,而不用一個迴圈呢?在作者的部落格裡有這麼一段

遍歷容器類時,選擇更高效的方法

相對於 Foundation 的方法來說,CoreFoundation 的方法有更高的效能,用 CFArrayApplyFunction() 和 CFDictionaryApplyFunction() 方法來遍歷容器類能帶來不少效能提升,但程式碼寫起來會非常麻煩。

然後我們來看一下ModelSetWithDictionaryFunction()的實現

為什麼在變數前都加了__unsafe_unretained,作者也說了

避免多餘的記憶體管理方法

在 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語言型別,程式碼如下:

關於 objc_msgSend() 我們隨便拿一行例子來舉例,比如這個:

這是一個可以呼叫者決定返回值和引數的函式,一般的函式是做不到的,預設情況下這個函式是長這樣

id 是指呼叫某一個方法的物件,在這裡這個物件就是你的 Model

SEL 是指你這個物件要呼叫的方法是什麼,在這裡這個方法就是 setter方法

然而,setter 方法是有引數的,這個引數怎麼傳進去?這就需要強制型別轉換了,我們把這個函式強制轉換成這個模樣:

這樣代表這個函式是一個沒有返回值,並且有3個引數的函式,分別是 id SEL id,前面兩個引數之前講過了,第三個引數就是你要呼叫的這個 setter 方法需要的引數,所以經過強制型別轉換之後的變異版就成了一開始的那種樣子。

其餘的都沒有什麼好講的了,都很簡單,都是一些煩人的型別判斷,只要仔細一點一行行看就能看懂了。

全部搞定以後,和原版的 YYModel 一樣,你可以這麼來測試

結尾

如果你自己親自動手寫完了這個精簡版的 YYMode ,你再去看完整版的會容易很多,我寫的這篇文章是把我從讀 YYModel 原始碼中學到的一些有用的東西分享給大家,如有什麼寫錯的地方,歡迎指正。

完整程式碼

點選這裡

相關文章