面試遇到Runtime的第一天-category
一. Runtime簡介
要說runtime就得從Objective-C說起,Objective-C是一門動態語言,這裡“動態”是什麼意思呢?簡單來說就是可以動態的建立類和物件,進行訊息傳遞和轉發。而runtime就是為Objective-C提供這種動態性所需要的動態的環境。說的比較抽象,實際去探索一下runtime的原始碼,可以更好的加深對runtime的理解。
runtime,中文名執行時,是一套C,C++,彙編寫成的底層API,給Objective-C提供了執行時的系統。與執行時相對應的是編譯時,對Objective-C而言,編譯時 (原始碼翻譯:OC swift Java 都是高階語言, 可讀性強 , 但是不會被機器識別 , 需要編譯成機器語言 二進位制才能被計算機識別,但是OC在編譯時應該是被編譯成了執行時程式碼
下面我們先從category開始,一步步去探索runtime中你不知道的知識。
二. category分析
簡介
先從objc_class開始 ,雖然runtime2.0之後這個結構體改變了,但是之前的程式碼也值得我們分析一下
struct objc_class {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class _Nullable super_class OBJC2_UNAVAILABLE;
const char * _Nonnull name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list * _Nullable ivars OBJC2_UNAVAILABLE;
struct objc_method_list * _Nullable * _Nullable methodLists OBJC2_UNAVAILABLE;
struct objc_cache * _Nonnull cache OBJC2_UNAVAILABLE;
struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
/* Use `Class` instead of `struct objc_class *` */
複製程式碼
我們知道category的作用呢,是為已有類新增方法的,那麼在上面原始碼中,我們要關心的自然是methodLists了。 其實,除了為已有類新增方法,category還有一些其他很實用的用途:
- 減少單個檔案的體積
- 可以把不同的功能組織到不同的category中 這樣每一個檔案可以有一個明確的功能
- 可以按需載入
- 宣告私有方法(分類裡宣告不實現 本類.m檔案中實現 外部呼叫)
- 把framework的私有方法公開
- 模擬多繼承,因為OC是不支援多繼承的,所以我們可以用category來模擬,這裡是通過訊息轉發實現
category和extension
這也是面試中常會被問到的問題,兩者的區別。因為平時extension我用的不多,所以這裡簡單說一下
- extension是類的一部分,在編譯期決議,一般用來隱藏類的私有屬性
- 無法為系統類新增extension,但是可以給其他類新增屬性
- category在執行期決議,不可以新增屬性
這裡又引出另外一個面試題:為什麼category不可以新增例項變數? 因為在執行時,物件的記憶體佈局已經確定,如果新增例項變數會破壞記憶體佈局
這是一句標準答案,但是到底是什麼意思你真的理解嘛?還是需要從原始碼出發去分析一下
原始碼分析
struct category_t {
const char *name;
classref_t cls;
struct method_list_t *instanceMethods;
struct method_list_t *classMethods;
struct protocol_list_t *protocols;
struct property_list_t *instanceProperties;
// Fields below this point are not always present on disk.
struct property_list_t *_classProperties;
method_list_t *methodsForMeta(bool isMeta) {
if (isMeta) return classMethods;
else return instanceMethods;
}
property_list_t *propertiesForMeta(bool isMeta,struct header_info *hi);
};
複製程式碼
在runtime層,category用結構體category_t表示,它包含:
- name: 類的名字
- cls: 類
- instanceMethods:給類新增的例項方法列表
- classMethods:給類新增的類方法列表
- protocols:給類新增的協議列表
- instanceProperties:給類新增的所有屬性
這裡就可以看到,這個結構體裡並沒有例項變數的列表
分類的實現原理是將category中的方法,屬性,協議資料放在category_t結構體中, 然後將結構體內的方法列表拷貝到類物件的方法列表中, Category可以新增屬性,但是並不會自動生成成員變數及set/get方法 因為category_t結構體中並不存在成員變數的列表 我們知道成員變數是存放在例項物件中的,並且編譯的那一刻就已經決定好了 而分類是在執行時才去載入的。那麼我們就無法在程式執行時將分類的成員變數新增到例項物件的結構體中。因此分類中不可以新增成員變數
這裡再嘮叨兩句所謂的“可變”和“不可變”的意思,因為物件在記憶體中的排布可以看成一個結構體,該結構體的大小並不能動態改變,所以無法在執行時動態的給物件增加成員變數。
相對的,物件的方法定義都儲存在類的可變區域中,方法列表如下:
struct objc_method_list * _Nullable * _Nullable methodLists
複製程式碼
是一個指向指標的指標,通過修改指向指標的指標的值,就可以實現動態的為某一個類增加成員方法,這也是category的實現原理,同時也說明瞭category不能增加成員變數。
載入category
這裡原始碼略長,就不貼了,主要記住如下兩點重要的結論:
- category的方法不會“覆蓋”掉原來類的同名方法 (category的方法被放到了新方法列表的前面)
- category的方法會“覆蓋”掉原來類的同名方法 (執行時在查詢方法的時候是順著方法列表的順序查詢的,只要一找到對應名字的方法,就返回不再繼續查找了)
那麼這裡就又有一個面試題了,如何呼叫原來類中被category覆蓋掉的方法呢?
分析到這裡我們思路就比較清晰了,category其實並不是完全替換掉原來類的同名方法,只是category在方法列表的前面而已,所以我們只要順著方法列表找到最後一個對應名字的方法,就可以呼叫原來類的方法
還有另外一個問題,多個category中的同名方法的執行順序是怎麼樣的呢?
執行順序是根據編譯順序決定的。
關聯物件
category本身沒有新增成員變數的功能,但是開發中我們實際會遇到這種需求,這時就需要用到關聯物件來實現了。相信你在開發中也用到過很多次了,怎麼用就不贅述了,這裡我們主要關注一下關聯物件的生命週期 比如存在什麼地方呢? 如何儲存? 物件銷燬的時候如何處理關聯物件呢?
關聯屬性通過自己定義一個新的資料結構 ObjcAssociation 容器來儲存使用者設定的內容 以及讀取使用者設定的內容 . 以此達到屬性那種通過方法訪問例項變數的效果. 分類關聯屬性的生命週期同原先類 . 通過在 isa 中標識是否有關聯物件來在 dealloc 中實現銷燬操作. runtime的銷燬物件函式objc_destructInstance裡面會判斷這個物件有沒有關聯物件,如果有,會呼叫_object_remove_assocations做關聯物件的清理工作。
load、initialize
先強調一下這也是一個常見的面試題-。-,感覺一個category能被問的東西可真多。
Category中可以新增load方法,load方法在程式啟動裝載類資訊的時候就會呼叫(但是附加category到類的工作會先於+load方法的執行)。load方法可以繼承。呼叫子類的load方法之前,會先呼叫父類的load方法
區別
區別在於呼叫方式和呼叫時刻 以及分類中的方法是否會覆蓋類本身
呼叫方式:load是根據函式地址直接呼叫,initialize是通過objc_msgSend呼叫
呼叫時刻:load是runtime載入類、分類的時候呼叫(只會呼叫1次)
initialize是類第一次接收到訊息的時候呼叫,每一個類只會initialize一次(父類的initialize方法可能會被呼叫多次)
呼叫順序: load方法中直接拿到load方法的記憶體地址直接呼叫方法,不再是通過訊息傳送機制呼叫。分類中也是通過直接拿到load方法的地址進行呼叫 先呼叫類的load方法,先編譯那個類,就先呼叫load。在呼叫load之前會先呼叫父類的load方法。 分類中load方法不會覆蓋本類的load方法,先編譯的分類優先呼叫load方法。
initialize是通過訊息傳送機制呼叫的,訊息傳送機制通過isa指標找到對應的方法與實現,因此先找到分類方法中的實現,會優先呼叫分類方法中的實現。 先初始化父類,之後再初始化子類。如果子類沒有實現+initialize,會呼叫父類的+initialize(所以父類的+initialize可能會被呼叫多次)如果分類實現了+initialize,就覆蓋類本身的+initialize呼叫。