一、Category 本質(zhì)
我們知道,當(dāng)調(diào)用一個(gè)對(duì)象的方法時(shí),通過對(duì)象的 isa 指針找到類對(duì)象,然后在類對(duì)象的方法列表中查找方法,如果沒有找到,就通過類對(duì)象的 superclass 指針找到父類對(duì)象,接著去尋找該方法。
分類中的對(duì)象方法依然是存儲(chǔ)在類對(duì)象中的方法列表中,同對(duì)象方法在同一個(gè)地方,那么調(diào)用步驟也同調(diào)用對(duì)象方法一樣。如果是類方法的話,也同樣是存儲(chǔ)在元類對(duì)象中。
1.Category 的底層結(jié)構(gòu)
struct category_t {
const char *name;
classref_t cls;
struct method_list_t *instanceMethods; // 對(duì)象方法
struct method_list_t *classMethods; // 類方法
struct protocol_list_t *protocols; // 協(xié)議
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);
};
該結(jié)構(gòu)體中包含:
- 對(duì)象方法
- 類方法
- 協(xié)議
- 屬性
但不包含成員變量,因此分類中不能添加成員變量,分類中添加的屬性并不會(huì)幫助我們自動(dòng)生成成員變量以及 get 和 set 方法,需要我們自己去實(shí)現(xiàn)。
_method_list_t類型的結(jié)構(gòu)體

屬性列表結(jié)構(gòu)體

runtime初始化函數(shù)源碼如下

接著我們來到 &map_images讀取模塊(images這里代表模塊),來到map_images_nolock函數(shù)中找到_read_images函數(shù),在_read_images函數(shù)中我們找到分類相關(guān)代碼

分類的實(shí)現(xiàn)原理是將category中的方法,屬性,協(xié)議數(shù)據(jù)放在category_t結(jié)構(gòu)體中,然后將結(jié)構(gòu)體內(nèi)的方法列表拷貝到類對(duì)象的方法列表中。
3.load 和 initialize
load
介紹
+load方法會(huì)在runtime加載類、分類時(shí)調(diào)用。每個(gè)類、分類的+load,在程序運(yùn)行過程中只調(diào)用一次。
調(diào)用順序:
- 先調(diào)用類的+load
- 按照編譯先后順序調(diào)用(先編譯,先調(diào)用)
- 調(diào)用子類的+load之前會(huì)先調(diào)用父類的+load
- 再調(diào)用分類的+load
- 按照編譯先后順序調(diào)用(先編譯,先調(diào)用)
+load方法是根據(jù)方法地址直接調(diào)用,并不是經(jīng)過objc_msgSend函數(shù)調(diào)用
源碼
load方法會(huì)在程序啟動(dòng)就會(huì)調(diào)用,當(dāng)裝載類信息的時(shí)候就會(huì)調(diào)用。 調(diào)用順序看一下源代碼。

通過源碼我們發(fā)現(xiàn)是優(yōu)先調(diào)用類的load方法,之后調(diào)用分類的load方法。
我們通過代碼驗(yàn)證一下:
我們添加Student繼承Presen類,并添加Student+Test分類,分別重寫只+load方法,其他什么都不做通過打印發(fā)現(xiàn)

確實(shí)是優(yōu)先調(diào)用類的load方法之后調(diào)用分類的load方法,不過調(diào)用類的load方法之前會(huì)保證其父類已經(jīng)調(diào)用過load方法。
load 方法調(diào)用源碼如下:

直接拿到load方法的內(nèi)存地址直接調(diào)用方法,而不是通過消息發(fā)送機(jī)制調(diào)用。
我們可以看到分類中也是通過直接拿到load方法的地址進(jìn)行調(diào)用。
initialize
介紹
+initialize方法會(huì)在類第一次接收到消息時(shí)調(diào)用
調(diào)用順序
- 先調(diào)用父類的+initialize,再調(diào)用子類的+initialize(遞歸調(diào)用)
- (先初始化父類,再初始化子類,每個(gè)類只會(huì)初始化1次)
- 分類實(shí)現(xiàn)+initialize,會(huì)覆蓋本身的+initialize方法(因?yàn)橄l(fā)送機(jī)制)
- 子類不實(shí)現(xiàn)+initialize,由于消息轉(zhuǎn)發(fā)機(jī)制,會(huì)調(diào)用到父類的+initialize方法,多個(gè)不實(shí)現(xiàn)+initialize方法的子類進(jìn)行初始化,就會(huì)多次調(diào)用父類的+initialize,但這不代表父類進(jìn)行了多次初始化,父類的初始化只會(huì)進(jìn)行一次,但+initialize方法可能會(huì)調(diào)用多次。
源碼
我們?yōu)镻reson、Student 、Student+Test 添加initialize方法。
我們知道當(dāng)類第一次接收到消息時(shí),就會(huì)調(diào)用initialize,相當(dāng)于第一次使用類的時(shí)候就會(huì)調(diào)用initialize方法。調(diào)用子類的initialize之前,會(huì)先保證調(diào)用父類的initialize方法。如果之前已經(jīng)調(diào)用過initialize,就不會(huì)再調(diào)用initialize方法了。當(dāng)分類重寫initialize方法時(shí)會(huì)覆蓋原來類的initialize方法。首先我們來看一下initialize的源碼。
保證 initialize 只調(diào)用一次的源碼:
通過鎖來保證并發(fā)安全,且通過 cls->isInitialized() 保證只執(zhí)行一次

在 _class_initialize 方法內(nèi)部通過遞歸的方式,如果父類沒有初始化,那么先調(diào)用父類的初始化方法,再調(diào)用子類的初始化方法

最終通過 objc_megSend() 調(diào)用 initialized() 方法,initialize是通過消息發(fā)送機(jī)制調(diào)用的,消息發(fā)送機(jī)制通過isa指針找到對(duì)應(yīng)的方法與實(shí)現(xiàn),消息發(fā)送機(jī)制也使得如果分類也實(shí)現(xiàn)了 initialize 方法,那么會(huì)覆蓋類原來的 initialized 方法

注意點(diǎn):由于 initialized 是通過 objc_megSend 調(diào)用的,遵循消息轉(zhuǎn)發(fā)機(jī)制,所以如果多個(gè)子類均沒有實(shí)現(xiàn) initialized 方法,而父類實(shí)現(xiàn)了 initialized 方法,那么子類進(jìn)行初始化時(shí),沒有 initialized 則會(huì)將 initialized 轉(zhuǎn)發(fā)到父類上,調(diào)用父類的 initialized 方法,多個(gè)子類的初始化會(huì)導(dǎo)致父類調(diào)用多次 initialized 方法。所以要注意 initialized 不一定只會(huì)調(diào)用一次
load initialize 異同
1.調(diào)用方式
- 1> load是根據(jù)函數(shù)地址直接調(diào)用
- 2> initialize是通過objc_msgSend調(diào)用
2.調(diào)用時(shí)刻
- 1> load是runtime加載類、分類的時(shí)候調(diào)用(只會(huì)調(diào)用1次)
- 2> initialize是類第一次接收到消息的時(shí)候調(diào)用,每一個(gè)類只會(huì)initialize一次(父類的initialize方法可能會(huì)被調(diào)用多次)
3.調(diào)用順序
load
- 1> 先調(diào)用類的load
- a) 先編譯的類,優(yōu)先調(diào)用load
- b) 調(diào)用子類的load之前,會(huì)先調(diào)用父類的load
- 2> 再調(diào)用分類的load
- a) 先編譯的分類,優(yōu)先調(diào)用load
initialize
- 1> 先初始化父類
- 2> 再初始化子類(可能最終調(diào)用的是父類的initialize方法)
二、面試題
1.Category的實(shí)現(xiàn)原理,以及Category為什么只能加方法不能加屬性。
答:分類的實(shí)現(xiàn)原理是將category中的方法,屬性,協(xié)議數(shù)據(jù)放在category_t結(jié)構(gòu)體中,然后將結(jié)構(gòu)體內(nèi)的方法列表拷貝到類對(duì)象的方法列表中。
Category可以添加屬性,但是并不會(huì)自動(dòng)生成成員變量及set/get方法。因?yàn)閏ategory_t結(jié)構(gòu)體中并不存在成員變量。通過之前對(duì)對(duì)象的分析我們知道成員變量是存放在實(shí)例對(duì)象中的,并且編譯的那一刻就已經(jīng)決定好了。而分類是在運(yùn)行時(shí)才去加載的。那么我們就無法再程序運(yùn)行時(shí)將分類的成員變量中添加到實(shí)例對(duì)象的結(jié)構(gòu)體中。因此分類中不可以添加成員變量。但是可以間接實(shí)現(xiàn)Category有成員變量的效果(關(guān)聯(lián)對(duì)象)
2.Category中有l(wèi)oad方法嗎?load方法是什么時(shí)候調(diào)用的?load 方法能繼承嗎?
答:Category中有l(wèi)oad方法,load方法在程序啟動(dòng)裝載類信息的時(shí)候就會(huì)調(diào)用。load方法可以繼承。調(diào)用子類的load方法之前,會(huì)先調(diào)用父類的load方法,但是一般情況下不會(huì)主動(dòng)去調(diào)用load方法,都是讓系統(tǒng)自動(dòng)調(diào)用
3.load、initialize的區(qū)別,以及它們?cè)赾ategory重寫的時(shí)候的調(diào)用的次序。
答:區(qū)別在于調(diào)用方式和調(diào)用時(shí)刻
調(diào)用方式:load是根據(jù)函數(shù)地址直接調(diào)用,initialize是通過objc_msgSend調(diào)用
調(diào)用時(shí)刻:load是runtime加載類、分類的時(shí)候調(diào)用(只會(huì)調(diào)用1次),initialize是類第一次接收到消息的時(shí)候調(diào)用,每一個(gè)類只會(huì)initialize一次(父類的initialize方法可能會(huì)被調(diào)用多次)
調(diào)用順序:先調(diào)用類的load方法,先編譯那個(gè)類,就先調(diào)用load。在調(diào)用load之前會(huì)先調(diào)用父類的load方法。分類中l(wèi)oad方法不會(huì)覆蓋本類的load方法,先編譯的分類優(yōu)先調(diào)用load方法。initialize先初始化父類,之后再初始化子類。如果子類沒有實(shí)現(xiàn)+initialize,會(huì)調(diào)用父類的+initialize(所以父類的+initialize可能會(huì)被調(diào)用多次),如果分類實(shí)現(xiàn)了+initialize,就覆蓋類本身的+initialize調(diào)用。
4.Category和Class Extension的區(qū)別是什么?
答:Class Extension在編譯的時(shí)候,它的數(shù)據(jù)就已經(jīng)包含在類信息中
Category是在運(yùn)行時(shí),才會(huì)將數(shù)據(jù)合并到類信息中