通過(guò)探索Category底層原理回答以下問(wèn)題
1)?Category是否可以添加方法、屬性、成員變量?Category是否可以遵守Protocol?
2)?Category的本質(zhì)是什么,在底層是怎么存儲(chǔ)的?
3)?Category的實(shí)現(xiàn)原理是什么,Catagory中的方法是如何調(diào)用到的?
4)?Category中是否有Load方法,load方法是什么時(shí)候調(diào)用的?
5)?load、initialize的區(qū)別
Category可以直接添加 屬性、成員變量嗎?
創(chuàng)建一個(gè)ZHPerson類


添加分類

發(fā)現(xiàn)分類中可以添加屬性,方法,協(xié)議,但是不能添加成員變量。
分析為什么不能添加成員變量?
Category的底層數(shù)據(jù)結(jié)構(gòu)
? ? 首先創(chuàng)建兩個(gè)分類 協(xié)助測(cè)試




將分類文件編譯為.cpp文件,切換到文件所在文件夾下執(zhí)行:
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc ZHPerson+Sport.m
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc ZHPerson+Eat.m

檢索_I_ZHPerson_Sport_sport 和?_C_ZHPerson_Sport_sport

分析可知:上述代碼創(chuàng)建了_method_list_t類型的結(jié)構(gòu)體變量
_OBJC_$_CATEGORY_INSTANCE_METHODS_ZHPerson_$_Sport?
和?_OBJC_$_CATEGORY_CLASS_METHODS_ZHPerson_$_Sport
分別用于存儲(chǔ)實(shí)例方法列表和類方法列表


這里簡(jiǎn)單說(shuō)下_objc_method中的method_type,詳細(xì)介紹后續(xù)在探究runtime消息機(jī)制時(shí)再繼續(xù)扒。method_type其實(shí)可以看做用字符縮寫來(lái)表達(dá)的函數(shù)類型字符串,比如?v@:i?就是返回類型為void,第一個(gè)參數(shù)為id類型,第二個(gè)參數(shù)為指針類型,第三個(gè)參數(shù)為int類型的函數(shù),如- (void)addStepCount:(int)count(我們知道iOS中方法調(diào)用會(huì)默認(rèn)傳入隱式參數(shù) 方法調(diào)用者:self 和 方法名:_cmd。這也是為什么我們可以在方法內(nèi)部訪問(wèn)self、cmd的原因)
繼續(xù)檢索_OBJC_$_CATEGORY_ZHPerson_$_Sport

回答第二個(gè)問(wèn)題:分類底層如何存儲(chǔ)的
分析可知:上述代碼創(chuàng)建了一個(gè)_category_t類型的結(jié)構(gòu)體變量 _OBJC_$_CATEGORY_ZHPerson_$_Sport;并且傳入了類方法列表、實(shí)例方法列表、協(xié)議類別、屬性列表,具備了Category的所有信息,沒(méi)錯(cuò)Category在底層就是_category_t類型,下邊我們檢索結(jié)構(gòu)體類型_category_t,看下_category_t的定義;

到此我們已經(jīng)清楚的看到了Category在底層的存儲(chǔ)結(jié)構(gòu),并且可以看到底層并沒(méi)有存儲(chǔ)成員變量,這也就是為什么直接添加成員變量會(huì)報(bào)錯(cuò)的原因。
Category中的屬性
并且我們知道在類中添加一個(gè)屬性,系統(tǒng)為我們做了三件事
@property (nonatomic, copy) NSString *name;
? ? 1) 創(chuàng)建了一個(gè)成員變量_name ?
? ? 2)?生成了setter、getter方法的聲明
? ? 3)?生成了setter、getter方法的實(shí)現(xiàn)
對(duì)比?ZHPerson 編譯后的.cpp中的屬性和分類中的區(qū)別
ZHPerson.cpp:

ZHPerson (Sport).cpp 文件

發(fā)現(xiàn)原類中生成了屬性的settter 和getter 方法,但是分類中沒(méi)有屬性的settter 和getter?實(shí)現(xiàn)。
在Category添加屬性系統(tǒng)僅僅只生成了setter、getter方法的聲明
如何使Category中的屬性與類中的屬性具備同樣的效果(關(guān)聯(lián)對(duì)象)
? ??
可以通過(guò)runtime中的關(guān)聯(lián)對(duì)象方法來(lái)實(shí)現(xiàn)

關(guān)聯(lián)策略,和@property后的關(guān)鍵字對(duì)應(yīng).

系統(tǒng)是如何管理關(guān)聯(lián)對(duì)象的
? ??去官網(wǎng)下載runtime源碼,搜索objc_setAssociatedObject方法,這里不做過(guò)多分析,簡(jiǎn)單說(shuō)下結(jié)論,后續(xù)會(huì)單開一篇扒關(guān)聯(lián)對(duì)象的實(shí)現(xiàn)原理。
runtime用四個(gè)類來(lái)管理關(guān)聯(lián)對(duì)象,AssociationsManager、AssociationsHashMap、AssociationsMap、ObjectAssociation,
1)關(guān)聯(lián)對(duì)象并不是存儲(chǔ)在被關(guān)聯(lián)對(duì)象本身內(nèi)存中
2)?關(guān)聯(lián)對(duì)象存儲(chǔ)在全局的統(tǒng)一的一個(gè)AssociationsManager中
3)設(shè)置關(guān)聯(lián)對(duì)象為nil,就相當(dāng)于是移除關(guān)聯(lián)對(duì)象
Category中的方法調(diào)用順序 - 表象
再創(chuàng)建一個(gè)ZHPerson的子類ZHStudent ,再編寫一個(gè)ZHStudent的分類,分類里書寫life方法



結(jié)論:1、分類中方法會(huì)覆蓋原類中的方法
2.、Compile Sources中編譯順序在后面的文件優(yōu)先級(jí)會(huì)更高。
ZHPerson有兩個(gè)分類Sport 和Eat,并且兩個(gè)分類都實(shí)現(xiàn)了life方法,
但是全部調(diào)用的分類Sport的方法,和Build Phases 里的Compile Sources 里的文件編譯順序有關(guān)

1、分類中的方法優(yōu)先級(jí)高于原類中的方法
2、后編譯的分類優(yōu)先級(jí)高于先編譯的分類
3、我們常說(shuō)的分類方法覆蓋原類方法并不是真正的覆蓋,只是objc_msgSend在分類中找到方法實(shí)現(xiàn)后不再繼續(xù)查找。
Category方法調(diào)用順序 - 本質(zhì)
? ? 1)OC中的方法調(diào)用簡(jiǎn)單的說(shuō)就是通過(guò)實(shí)例對(duì)象(或類對(duì)象)的isa指針和類對(duì)象(或元類對(duì)象)的superClass指針去類(或元類)對(duì)象中查找方法。
? ? 2)Category中的方法、屬性等編譯后是存儲(chǔ)在category_t結(jié)構(gòu)體中的,也就是說(shuō)編譯后分類中的方法并沒(méi)有合并到類(或元類)中,我們是無(wú)法在類對(duì)象(或元類對(duì)象)中找到Category中的方法的。
? ? 3)但是最終調(diào)用的時(shí)候我們卻可以通過(guò)isa和superClass指針找到這些方法。所以我們有理由猜測(cè)runtime幫我們做了方法合并
? ? 4)_objc_init就是runtime的初始化函數(shù),是在app啟動(dòng)過(guò)程的"初始化除可執(zhí)行文件外的所有Mach-O文件初始化調(diào)用的;按文件編譯倒序?qū)⒏鞣诸愔械姆椒?、協(xié)議、屬性列表分別整合成一個(gè)二維數(shù)組后,添加到原類中的方法列表,屬性列表,協(xié)議列表。(分類中的方法屬性等系統(tǒng)是什么時(shí)候如何添加到原類中的?)
Category中的+load方法
在創(chuàng)建的兩個(gè)類文件以及3個(gè)分類文件添加+load方法,也添加上initialize()方法,方便后面測(cè)試。





不導(dǎo)入上面相關(guān)任何文件,不創(chuàng)建對(duì)象,直接運(yùn)行

可以發(fā)現(xiàn)未做任何調(diào)用和對(duì)象創(chuàng)建的情況下,也會(huì)執(zhí)行+ (void)load方法。
嘗試在xcode->targets->build phases->compile sources中調(diào)整文件編譯順序,發(fā)現(xiàn)
1、父類中的load優(yōu)先于子類中調(diào)用,且不受編譯順序影響。
2、原類中的load方法優(yōu)先于分類調(diào)用,且不受編譯順序影響。
3、兩個(gè)分類中的load方法執(zhí)行順序根據(jù)編譯順序,且與其繼承的父類無(wú)關(guān)系。
Category中的+ (void)initialize方法
再創(chuàng)建一個(gè)新類ZHDog,作對(duì)比

像測(cè)試load方法一樣,不導(dǎo)入創(chuàng)建的任何文件不創(chuàng)建對(duì)象,直接運(yùn)行但是并沒(méi)有調(diào)用initialize方法。
創(chuàng)建對(duì)象
情況1

情況2

情況3

以上3種情況的打印結(jié)果都是下面結(jié)果:

3種情況編譯順序都是

說(shuō)明同一個(gè)類中的initialize方法在多次創(chuàng)建對(duì)象時(shí)僅調(diào)用一次,
現(xiàn)在只調(diào)整文件編譯順序:


打印結(jié)果沒(méi)變
initialize調(diào)用總結(jié):
1)父類調(diào)用優(yōu)先級(jí)高于子類,不受編譯順序影響;
2) 分類會(huì)覆蓋原類中的方法
注意,如果子類及子類分類沒(méi)有實(shí)現(xiàn)initialize方法,根據(jù)runtime消息發(fā)送機(jī)制,父類中的initialize會(huì)調(diào)用兩次
現(xiàn)在注釋掉ZHStudent及其分類的initialize方法,其他全不變

load是只要類所在的文件被引用就會(huì)被調(diào)用,而initialize在類或其子類的第一個(gè)方法調(diào)用之前被調(diào)用(runtime 中l(wèi)oad方法不能認(rèn)為第一個(gè)方法)。load在main函數(shù)之前調(diào)用,initialize在main函數(shù)之后調(diào)用。這兩個(gè)方法會(huì)被自動(dòng)調(diào)用。
· load和initialize方法都不用顯示的調(diào)用父類的方法而是自動(dòng)調(diào)用,即使子類沒(méi)有initialize方法也會(huì)調(diào)用父類的方法如果子類顯示調(diào)用[super initialize],則父類多次調(diào)用,load方法則不會(huì)調(diào)用父類。
·load方法通常用來(lái)進(jìn)行Method Swizzle,initialize方法一般用于初始化全局變量或靜態(tài)變量。
·load和initialize方法內(nèi)部使用了鎖,因此它們是線程安全的。實(shí)現(xiàn)時(shí)要盡可能保持簡(jiǎn)單,避免阻塞線程,不要再使用鎖。
·每個(gè)類只調(diào)用initialize一次。如果希望為類和類的類別執(zhí)行獨(dú)立初始化,則應(yīng)該實(shí)現(xiàn)load方法