一、Category 本質
我們知道,當調用一個對象的方法時,通過對象的 isa 指針找到類對象,然后在類對象的方法列表中查找方法,如果沒有找到,就通過類對象的 superclass 指針找到父類對象,接著去尋找該方法。
分類中的對象方法依然是存儲在類對象中的方法列表中,同對象方法在同一個地方,那么調用步驟也同調用對象方法一樣。如果是類方法的話,也同樣是存儲在元類對象中。
1.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);
};
該結構體中包含:
- 對象方法
- 類方法
- 協議
- 屬性
但不包含成員變量,因此分類中不能添加成員變量,分類中添加的屬性并不會幫助我們自動生成成員變量以及 get 和 set 方法,需要我們自己去實現。
_method_list_t類型的結構體
屬性列表結構體
runtime初始化函數源碼如下
接著我們來到 &map_images讀取模塊(images這里代表模塊),來到map_images_nolock函數中找到_read_images函數,在_read_images函數中我們找到分類相關代碼
分類的實現原理是將category中的方法,屬性,協議數據放在category_t結構體中,然后將結構體內的方法列表拷貝到類對象的方法列表中。
3.load 和 initialize
load
介紹
+load方法會在runtime加載類、分類時調用。每個類、分類的+load,在程序運行過程中只調用一次。
調用順序:
- 先調用類的+load
- 按照編譯先后順序調用(先編譯,先調用)
- 調用子類的+load之前會先調用父類的+load
- 再調用分類的+load
- 按照編譯先后順序調用(先編譯,先調用)
+load方法是根據方法地址直接調用,并不是經過objc_msgSend函數調用
源碼
load方法會在程序啟動就會調用,當裝載類信息的時候就會調用。 調用順序看一下源代碼。
通過源碼我們發現是優先調用類的load方法,之后調用分類的load方法。
我們通過代碼驗證一下:
我們添加Student繼承Presen類,并添加Student+Test分類,分別重寫只+load方法,其他什么都不做通過打印發現
確實是優先調用類的load方法之后調用分類的load方法,不過調用類的load方法之前會保證其父類已經調用過load方法。
load 方法調用源碼如下:
直接拿到load方法的內存地址直接調用方法,而不是通過消息發送機制調用。
我們可以看到分類中也是通過直接拿到load方法的地址進行調用。
initialize
介紹
+initialize方法會在類第一次接收到消息時調用
調用順序
- 先調用父類的+initialize,再調用子類的+initialize(遞歸調用)
- (先初始化父類,再初始化子類,每個類只會初始化1次)
- 分類實現+initialize,會覆蓋本身的+initialize方法(因為消息發送機制)
- 子類不實現+initialize,由于消息轉發機制,會調用到父類的+initialize方法,多個不實現+initialize方法的子類進行初始化,就會多次調用父類的+initialize,但這不代表父類進行了多次初始化,父類的初始化只會進行一次,但+initialize方法可能會調用多次。
源碼
我們為Preson、Student 、Student+Test 添加initialize方法。
我們知道當類第一次接收到消息時,就會調用initialize,相當于第一次使用類的時候就會調用initialize方法。調用子類的initialize之前,會先保證調用父類的initialize方法。如果之前已經調用過initialize,就不會再調用initialize方法了。當分類重寫initialize方法時會覆蓋原來類的initialize方法。首先我們來看一下initialize的源碼。
保證 initialize 只調用一次的源碼:
通過鎖來保證并發安全,且通過 cls->isInitialized() 保證只執行一次
在 _class_initialize 方法內部通過遞歸的方式,如果父類沒有初始化,那么先調用父類的初始化方法,再調用子類的初始化方法
最終通過 objc_megSend() 調用 initialized() 方法,initialize是通過消息發送機制調用的,消息發送機制通過isa指針找到對應的方法與實現,消息發送機制也使得如果分類也實現了 initialize 方法,那么會覆蓋類原來的 initialized 方法
注意點:由于 initialized 是通過 objc_megSend 調用的,遵循消息轉發機制,所以如果多個子類均沒有實現 initialized 方法,而父類實現了 initialized 方法,那么子類進行初始化時,沒有 initialized 則會將 initialized 轉發到父類上,調用父類的 initialized 方法,多個子類的初始化會導致父類調用多次 initialized 方法。所以要注意 initialized 不一定只會調用一次
load initialize 異同
1.調用方式
- 1> load是根據函數地址直接調用
- 2> initialize是通過objc_msgSend調用
2.調用時刻
- 1> load是runtime加載類、分類的時候調用(只會調用1次)
- 2> initialize是類第一次接收到消息的時候調用,每一個類只會initialize一次(父類的initialize方法可能會被調用多次)
3.調用順序
load
- 1> 先調用類的load
- a) 先編譯的類,優先調用load
- b) 調用子類的load之前,會先調用父類的load
- 2> 再調用分類的load
- a) 先編譯的分類,優先調用load
initialize
- 1> 先初始化父類
- 2> 再初始化子類(可能最終調用的是父類的initialize方法)
二、面試題
1.Category的實現原理,以及Category為什么只能加方法不能加屬性。
答:分類的實現原理是將category中的方法,屬性,協議數據放在category_t結構體中,然后將結構體內的方法列表拷貝到類對象的方法列表中。
Category可以添加屬性,但是并不會自動生成成員變量及set/get方法。因為category_t結構體中并不存在成員變量。通過之前對對象的分析我們知道成員變量是存放在實例對象中的,并且編譯的那一刻就已經決定好了。而分類是在運行時才去加載的。那么我們就無法再程序運行時將分類的成員變量中添加到實例對象的結構體中。因此分類中不可以添加成員變量。但是可以間接實現Category有成員變量的效果(關聯對象)
2.Category中有load方法嗎?load方法是什么時候調用的?load 方法能繼承嗎?
答:Category中有load方法,load方法在程序啟動裝載類信息的時候就會調用。load方法可以繼承。調用子類的load方法之前,會先調用父類的load方法,但是一般情況下不會主動去調用load方法,都是讓系統自動調用
3.load、initialize的區別,以及它們在category重寫的時候的調用的次序。
答:區別在于調用方式和調用時刻
調用方式:load是根據函數地址直接調用,initialize是通過objc_msgSend調用
調用時刻:load是runtime加載類、分類的時候調用(只會調用1次),initialize是類第一次接收到消息的時候調用,每一個類只會initialize一次(父類的initialize方法可能會被調用多次)
調用順序:先調用類的load方法,先編譯那個類,就先調用load。在調用load之前會先調用父類的load方法。分類中load方法不會覆蓋本類的load方法,先編譯的分類優先調用load方法。initialize先初始化父類,之后再初始化子類。如果子類沒有實現+initialize,會調用父類的+initialize(所以父類的+initialize可能會被調用多次),如果分類實現了+initialize,就覆蓋類本身的+initialize調用。
4.Category和Class Extension的區別是什么?
答:Class Extension在編譯的時候,它的數據就已經包含在類信息中
Category是在運行時,才會將數據合并到類信息中