YYModel原始碼閱讀–基礎知識

發表於2016-06-25

這段時間因為工作需要,閱讀了YYModel這個開源框架,至於它能做什麼,最直白的講述就是JSON與Model之間的相互轉化。

原始碼在Github,大家可以自行git clone或者download。

接下來,筆者主要分析閱讀原始碼而引出的各種問題與知識點,不足之處請大家指正。

NS_ASSUME_NONNULL_BEGIN & NS_ASSUME_NONNULL_END

這組巨集是成對使用的,不得不說我們自己寫程式碼的時候使用的很少,以至於遺漏這個知識點,現在我們就來看看這兩個巨集會引出什麼問題。

這組巨集會引出幾個關於Objective-C新特性的知識點:

  • Nullability Annotations
  • Lightweight Generics
  • __kindof

Nullability Annotations

我們都知道在swift中,可以使用!和?來表示一個物件是optional的還是non-optional,如view?和view!。而在 Objective-C中則沒有這一區分,view既可表示這個物件是optional,也可表示是non-optioanl。這樣就會造成一個問題:在 Swift與Objective-C混編時,Swift編譯器並不知道一個Objective-C物件到底是optional還是non-optional,因此這種情況下編譯器會隱式地將Objective-C的物件當成是non-optional。

為了解決這個問題,蘋果在Xcode 6.3引入了一個Objective-C的新特性:nullability annotations。這一新特性的核心是兩個新的型別註釋: __nullable__nonnull 。從字面上我們可以猜到,__nullable表示物件可以是NULL或nil,而__nonnull表示物件不應該為空。當我們不遵循這一規則時,編譯器就會給出警告。

我們來看看以下的例項,

不過這只是一個警告,程式還是能編譯通過並執行。

事實上,在任何可以使用const關鍵字的地方都可以使用__nullable__nonnull,不過這兩個關鍵字僅限於使用在指標型別上。而在方法的宣告中,我們還可以使用不帶下劃線的nullablenonnull,如下所示:

Nonnull區域設定(Audited Regions)

如果需要每個屬性或每個方法都去指定nonnullnullable,是一件非常繁瑣的事。蘋果為了減輕我們的工作量,專門提供了兩個巨集:NS_ASSUME_NONNULL_BEGINNS_ASSUME_NONNULL_END。在這兩個巨集之間的程式碼,所有簡單指標物件都被假定為 nonnull,因此我們只需要去指定那些nullable的指標。如下程式碼所示:

在上面的程式碼中,items屬性預設是nonnull的,itemWithName:方法的返回值也是nonnull,而引數是指定為nullable的。

不過,為了安全起見,蘋果還制定了幾條規則:

  • typedef定義的型別的nullability特性通常依賴於上下文,即使是在Audited Regions中,也不能假定它為nonnull。
  • 複雜的指標型別(如id )必須顯示去指定是nonnull還是nullable。例如,指定一個指向nullable物件的nonnull指標,可以使用”__nullable id __nonnull”。
  • 我們經常使用的NSError **通常是被假定為一個指向nullable NSError物件的nullable指標。

相容性

因為Nullability Annotations是Xcode 6.3新加入的,所以我們需要考慮之前的老程式碼。實際上,蘋果已以幫我們處理好了這種相容問題,我們可以安全地使用它們:

  • 老程式碼仍然能正常工作,即使對nonnull物件使用了nil也沒有問題。
  • 老程式碼在需要和swift混編時,在新的swift編譯器下會給出一個警告。
  • nonnull不會影響效能。事實上,我們仍然可以在執行時去判斷我們的物件是否為nil。

事實上,我們可以將nonnull/nullable與我們的斷言和異常一起看待,其需要處理的問題都是同一個:違反約定是一個程式設計師的錯誤。特別是,返回值是我們可控的東西,如果返回值是nonnull的,則我們不應該返回nil,除非是為了向後相容。

Lightweight Generics

Lightweight Generics 輕量級泛型,輕量是因為這是個純編譯器的語法支援(LLVM 7.0),和 Nullability 一樣,沒有藉助任何 objc runtime 的升級,也就是說,這個新語法在 Xcode 7 上可以使用且完全向下相容(更低的 iOS 版本)

帶泛型的容器

這無疑是本次最重大的改進,有了泛型後終於可以指定容器類中物件的型別了:

返回值的 id 被替換成具體的型別後,令人感動的程式碼提示也出來了。

假如向泛型容器中加入錯誤的物件,編譯器會不開心的。

系統中常用的一系列容器型別都增加了泛型支援,甚至連 NSEnumerator 都支援了,這是非常 Nice 的改進。和 Nullability 一樣,我認為最大的意義還是豐富了介面描述資訊,對比下面兩種寫法:

不用多想就清楚下面的陣列中存的是什麼,避免了 NSStringNSURL 的混亂。

自定義泛型類

比起使用系統的泛型容器,更好玩的是自定義一個泛型類,目前這裡還沒什麼文件,但攔不住我們寫測試程式碼,假設我們要自定義一個 Stack 容器類:

這個 ObjectType 是傳入型別的 placeholder,它只能在 @interface 上定義(類宣告、類擴充套件、Category),如果你喜歡用 T 表示也 OK,這個型別在 @interface@end 區間的作用域有效,可以把它作為入參、出參、甚至內部 NSArray 屬性的泛型型別,應該說一切都是符合預期的。我們還可以給 ObjectType 增加型別限制,比如:

若什麼都不加,表示接受任意型別 ( id );當型別不滿足時編譯器將產生 error。
例項化一個 Stack,一切工作正常:

對於多引數的泛型,用逗號隔開,其他都一樣,可以參考 NSDictionary 的標頭檔案。

協變性和逆變性

當類支援泛型後,它們的 Type 發生了變化,比如下面三個物件看上去都是 Stack,但實際上屬於三個 Type:

當其中兩種型別做型別轉化時,編譯器需要知道哪些轉化是允許的,哪些是禁止的,比如,預設情況下:

在Xcode中我們可以看到,不指定泛型型別的 Stack 可以和任意泛型型別轉化,但指定了泛型型別後,兩個不同型別間是不可以強轉的,假如你希望主動控制轉化關係,就需要使用泛型的協變性和逆變性修飾符了:

協變

逆變

協變是非常好理解的,像 NSArray 的泛型就用了協變的修飾符。

__kindof

__kindof 這修飾符還是很實用的,解決了一個長期以來的小痛點,拿原來的 UITableView 的這個方法來說:

使用時前面基本會使用 UITableViewCell 子型別的指標來接收返回值,所以這個 API 為了讓開發者不必每次都蛋疼的寫顯式強轉,把返回值定義成了 id 型別,而這個 API 實際上的意思是返回一個 UITableViewCellUITableViewCell 子類的例項,於是新的 __kindof 關鍵字解決了這個問題:

既明確表明了返回值,又讓使用者不必寫強轉。再舉個帶泛型的例子,UIView 的 subviews 屬性被修改成了:

這樣,寫下面的程式碼時就沒有任何警告了:

NS_ENUM & NS_OPTIONS

列舉是指將變數的值一一列舉出來,變數的值只限於列舉出來的值的範圍內。

列舉本質上是一個整數,列舉的作用是把值限定在指定的範圍內,並且增加程式碼的可讀性。 列舉的成員如果沒有顯示指定值,那麼第一個成員的值總是0,後面成員的值依次遞增。列舉可以直接用於比較。

一般我們宣告列舉:

我們會發現列舉中一些不可自定義的部分,例如,列舉名。

NS_ENUM 和 NS_OPTIONS 都不算太古老的巨集,在iOS 6 / OS X Mountain Lion才開始有,它們都是代替 enum 的更好的辦法。

NS_ENUM

如果要在早期的iOS或OS X系統中使用這兩個巨集,簡單定義一下就好

在OS X 10.4 中的原始定義如下:

在之前列舉可以這麼定義:

或者

現在,有了統一的風格

NS_ENUM 的第一個引數是用於儲存的新型別的型別。在64位環境下,UITableViewCellStyleNSInteger 一樣有8 bytes長。你要保證你給出的所有值能被該型別容納,否則就會產生錯誤。第二個引數是新型別的名字。大括號裡面和以前一樣,是你要定義的各種值。

NS_OPTIONS

語法和 NS_ENUM 完全相同,但這個巨集提示編譯器值是如何通過位掩碼 | 組合在一起的。

attribute((always_inline))

我們知道一般的函式呼叫都會通過call的方式來呼叫,這樣讓攻擊很容易對一個函式做手腳,如果是以inline的方式編譯的會,會把該函式的code拷貝到每次呼叫該函式的地方。而static會讓生成的二進位制檔案中沒有清晰的符號表,讓逆向的人很難弄清楚邏輯。

__attribute__((always_inline)) 的意思是強制內聯,所有加了__attribute__((always_inline)) 的函式再被呼叫時不會被編譯成函式呼叫而是直接擴充套件到呼叫函式體內,比如定義了函式
__attribute__((always_inline)) void a()

b 呼叫 a 函式的彙編程式碼不會是跳轉到a執行,而是 a 函式的程式碼直接在 b 內成為 b 的一部分。
#define __inline __attribute__((always_inline)) 的意思就是用
__inline 代替__attribute__((always_inline))
內宣告a的時候可以直接寫成__inline void a() 這樣比較方便因為__attribute__((always_inline)) 字多。

undef

這是預編譯指令,和#define搭配使用,意思是取消之前的巨集定義。

__unsafe_unretained

__unsafe_unretained是對物件的非zeroing的weak reference,意思是當物件所指向的記憶體被銷燬了,物件還存在,稱為“野指標”。

在iOS引入了Automatic Reference Count(ARC)之後,編譯器可以在編譯時對obj-c物件進行記憶體管理。大致規則如下:

為了強制一個物件隱藏其資料,編譯器限制例項變數範圍以限制其在程式中的可見性,但是為了提供靈活性,蘋果也讓開發者顯式設定範圍。

以下是這些關鍵字的使用範圍:

  • @private

The instance variable is accessible only within the class that declares it.

例項變數只能被宣告它的類訪問.

  • @protected

The instance variable is accessible within the class that declares it and within classes that inherit it. All instance variables without an explicit scope directive have @protected scope.

例項變數能被宣告它的類和子類訪問,所有沒有顯式制定範圍的例項變數都是.

  • @public

The instance variable is accessible everywhere.

例項變數可以被在任何地方訪問.

  • @package

Using the modern runtime, an @package instance variable has @public scope inside the executable image that implements the class, but acts like @private outside.使用modern執行時,一個@package例項變數在實現這個類的可執行檔案映象中實際上是@public的,但是在外面就是@private【runtime需要再看一下蘋果文件Runtime Programming Guide】

The @package scope for Objective-C instance variables is analogous to private_extern for C variables and functions. Any code outside the class implementation’s image that tries to use the instance variable gets a link error.

Objective-C中的@package與C語言中變數和函式的private_extern類似。任何在實現類的映象之外的程式碼想使用這個例項變數都會引發link error

This scope is most useful for instance variables in framework classes, where @private may be too restrictive but @protected or @public too permissive.

這個型別最常用於框架類的例項變數,使用@private太限制,使用@protected或者@public又太開放.

相關文章