學(xué)習(xí)寫簡書博客,每次寫簡書都是對過往的回顧
前言
這段時(shí)間換工作引瀑,發(fā)現(xiàn)面試經(jīng)常會問到一個(gè)問題:
分類中能不能定義實(shí)例變量驼鹅,為什么?
答案:不能纽门。類的內(nèi)存布局在編譯時(shí)期就已經(jīng)確定了薛耻,category是運(yùn)行時(shí)才加載的早已經(jīng)確定了內(nèi)存布局所以無法添加實(shí)例變量,如果添加實(shí)例變量就會破壞category的內(nèi)部布局赏陵。
繼續(xù)追問:
1:為什么說category是在運(yùn)行時(shí)加載的饼齿?
2:不能添加實(shí)例變量,那為什么能添加屬性蝙搔?
參考了一些大神的說法之后缕溉,最終在runtime源碼里找到了答案。
先來看看在runtime中的結(jié)構(gòu)體的樣子
在分類轉(zhuǎn)化為c++文件中可以看出_category_t結(jié)構(gòu)體中吃型,存放著類名证鸥,對象方法列表,類方法列表勤晚,協(xié)議列表枉层,以及屬性列表。
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc CategoryName.m
1:category小括號里寫的名字
2:要擴(kuò)展的類對象赐写,編譯期間這個(gè)值是不會有的鸟蜡,在app被runtime加載時(shí)才會根據(jù)name對應(yīng)到類對象
3:這個(gè)category所有的-方法
4:這個(gè)category所有的+方法
5:這個(gè)category實(shí)現(xiàn)的protocol,比較不常用在category里面實(shí)現(xiàn)協(xié)議挺邀,但是確實(shí)支持的
6:這個(gè)category所有的property矩欠,這也是category里面可以定義屬性的原因,不過這個(gè)property不會@synthesize實(shí)例變量悠夯,一般有需求添加實(shí)例變量屬性時(shí)會采用objc_setAssociatedObject和objc_getAssociatedObject方法綁定方法綁定癌淮,不過這種方法生成的與一個(gè)普通的實(shí)例變量完全是兩碼事。
這里已經(jīng)可以回答第二個(gè)問題了沦补。
再看catagory如何添加進(jìn)runtime
runtime源碼下載鏈接:
鏈接: https://pan.baidu.com/s/11GLMCKU64trxksQaK9ZBvg 提取碼: xu23
其實(shí)在main函數(shù)之前乳蓄,將runtime通過dyld動態(tài)加載進(jìn)來的時(shí)候生效的。怎么驗(yàn)證夕膀,再來看runtime源碼:
先從objc_init開始虚倒,其中大量出現(xiàn)的image并不是圖片,而是一個(gè)二進(jìn)制文件(可執(zhí)行文件或 so 文件)产舞,里面是被編譯過的符號魂奥、代碼等,所以 ImageLoader 作用是將這些文件加載進(jìn)內(nèi)存易猫,且每一個(gè)文件對應(yīng)一個(gè)ImageLoader實(shí)例來負(fù)責(zé)加載耻煤。
void _objc_init(void)
{
// fixme defer initialization until an objc-using image is found?
environ_init();
tls_init();
lock_init();
exception_init();
// Register for unmap first, in case some +load unmaps something
_dyld_register_func_for_remove_image(&unmap_image);
dyld_register_image_state_change_handler(dyld_image_state_bound,
1/*batch*/, &map_images);
dyld_register_image_state_change_handler(dyld_image_state_dependents_initialized, 0/*not batch*/, &load_images);
}
在load_images之后調(diào)用_read_images方法初始化map后的image,這里面干了很多的事情,像load所有的類哈蝇、協(xié)議和category棺妓。
再仔細(xì)看category的初始化:
// Discover categories.
for (EACH_HEADER) {
category_t **catlist =
_getObjc2CategoryList(hi, &count);
for (i = 0; i < count; i++) {
category_t *cat = catlist[i];
class_t *cls = remapClass(cat->cls);
// Process this category.
// First, register the category with its target class.
// Then, rebuild the class's method lists (etc) if
// the class is realized.
BOOL classExists = NO;
if (cat->instanceMethods || cat->protocols
|| cat->instanceProperties)
{
addUnattachedCategoryForClass(cat, cls, hi);
if (isRealized(cls)) {
remethodizeClass(cls);
classExists = YES;
}
}
if (cat->classMethods || cat->protocols
/* || cat->classProperties */)
{
addUnattachedCategoryForClass(cat, cls->isa, hi);
if (isRealized(cls->isa)) {
remethodizeClass(cls->isa);
}
if (PrintConnecting) {
_objc_inform("CLASS: found category +%s(%s)",
getName(cls), cat->name);
}
}
}
}
__objc_catlist,就是上面category存放的數(shù)據(jù)段炮赦。
以上代碼做的事:
1把category的實(shí)例方法怜跑、協(xié)議以及屬性添加到類上。
2把category的類方法和協(xié)議添加到類的metaclass上吠勘。
具體怎么做性芬,主要是兩個(gè)方法addUnattachedCategoryForClass和remethodizeClass。
addUnattachedCategoryForClass實(shí)現(xiàn)映射剧防,remethodizeClass去做具體操作批旺。
再往下看category的各種列表是怎么最終添加到類上的。
點(diǎn)開attachCategoryMethods方法可以看到它將所有category的實(shí)例方法列表拼成了一個(gè)大的實(shí)例方法列表诵姜,再通過attachMethodLists去加到方法列表里
static void
attachCategoryMethods(class_t *cls, category_list *cats,
BOOL *inoutVtablesAffected)
{
if (!cats) return;
if (PrintReplacedMethods) printReplacements(cls, cats);
BOOL isMeta = isMetaClass(cls);
method_list_t **mlists = (method_list_t **)
_malloc_internal(cats->count * sizeof(*mlists));
// Count backwards through cats to get newest categories first
int mcount = 0;
int i = cats->count;
BOOL fromBundle = NO;
while (i--) {
method_list_t *mlist = cat_method_list(cats->list[i].cat, isMeta);
if (mlist) {
mlists[mcount++] = mlist;
fromBundle |= cats->list[i].fromBundle;
}
}
attachMethodLists(cls, mlists, mcount, NO, fromBundle, inoutVtablesAffected);
_free_internal(mlists);
}
結(jié)論:
1)category的方法并沒有“完全替換掉”原來類已經(jīng)有的方法,而是把擴(kuò)展的方法放入到方法列表的前頭搏熄,舉個(gè)栗子(原來的方法列表<a,b,c,>棚唆,擴(kuò)展的方法是<1,2,3>,會變成<1,2,3,a,b,c>心例。)
2)為什么平常所說的category的方法會“覆蓋”掉原來類的同名方法宵凌,就是因?yàn)檫\(yùn)行時(shí)在查找方法的時(shí)候是順著方法列表的順序查找的,而且只要一找到對應(yīng)名字的方法止后,就會結(jié)束查找瞎惫。
另外的一些疑問
問:Category中有l(wèi)oad方法嗎?load方法是什么時(shí)候調(diào)用的译株?load 方法能繼承嗎瓜喇?
答:Category中有l(wèi)oad方法,load方法在程序啟動裝載類信息的時(shí)候就會調(diào)用歉糜。load方法可以繼承乘寒。調(diào)用子類的load方法之前,會先調(diào)用父類的load方法
問:load匪补、initialize的區(qū)別伞辛,以及它們在category重寫的時(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)用(只會調(diào)用1次),initialize是類第一次接收到消息的時(shí)候調(diào)用踊兜,每一個(gè)類只會initialize一次(父類的initialize方法可能會被調(diào)用多次)
調(diào)用順序:先調(diào)用類的load方法竿滨,先編譯那個(gè)類,就先調(diào)用load。在調(diào)用load之前會先調(diào)用父類的load方法姐呐。分類中l(wèi)oad方法不會覆蓋本類的load方法殿怜,先編譯的分類優(yōu)先調(diào)用load方法。initialize先初始化父類曙砂,之后再初始化子類头谜。如果子類沒有實(shí)現(xiàn)+initialize,會調(diào)用父類的+initialize(所以父類的+initialize可能會被調(diào)用多次)鸠澈,如果分類實(shí)現(xiàn)了+initialize柱告,就覆蓋類本身的+initialize調(diào)用。
反觀擴(kuò)展(extension)笑陈,作用是為一個(gè)已知的類添加一些私有的信息际度,必須有這個(gè)類的源碼,才能擴(kuò)展涵妥,它是在編譯器生效的乖菱,所以能直接為類添加屬性或者實(shí)例變量。
由此推想protocol添加屬性會怎么蓬网?
由上面的分析是不是想到了protocol中添加屬性窒所?其實(shí)由protocol的用途也能猜測出:protocol是一系列的協(xié)議,要求代理去實(shí)現(xiàn)帆锋,自己并沒有實(shí)現(xiàn)吵取,方法或?qū)傩远际沁@樣,只是做了聲明要求代理去實(shí)現(xiàn)锯厢。所以添加的屬性也只是聲明其代理有實(shí)現(xiàn)這個(gè)屬性皮官,自身并沒有實(shí)現(xiàn)其getter、setter以及ivar实辑。有興趣的朋友可以實(shí)踐看看捺氢。