Runtime的相關(guān)知識(shí)

Runtime是近年來(lái)面試遇到的一個(gè)高頻方向,也是我們平時(shí)開發(fā)中或多或少接觸的一個(gè)領(lǐng)域,那么什么是runtime呢?它又可以用來(lái)做什么呢?

什么是Runtime?平時(shí)項(xiàng)目中有用過(guò)么?
    OC是一門動(dòng)態(tài)性比較強(qiáng)的編程語(yǔ)言,允許很多操作推遲到程序運(yùn)行時(shí)再進(jìn)行
    OC的動(dòng)態(tài)性就是由Runtime來(lái)支撐和實(shí)現(xiàn)的,Runtime是一套C語(yǔ)言的API,封裝了很多動(dòng) 態(tài)性相關(guān)的函數(shù)
    平時(shí)編寫的OC代碼,底層都是轉(zhuǎn)換成了Runtime API進(jìn)行調(diào)用

具體應(yīng)用
    利用關(guān)聯(lián)對(duì)象(AssociatedObject)給分類添加屬性
    遍歷類的所有成員變量(修改textfield的占位文字顏色、字典轉(zhuǎn)模型、自動(dòng)歸檔解檔)
    交換方法實(shí)現(xiàn)(交換系統(tǒng)的方法)
    利用消息轉(zhuǎn)發(fā)機(jī)制解決方法找不到的異常問(wèn)題

1、詳解isa

我們?cè)谘芯繉?duì)象的本質(zhì)的時(shí)候提到過(guò)isa,當(dāng)時(shí)說(shuō)的是isa是個(gè)指針,存儲(chǔ)的是個(gè)類對(duì)象或者元類對(duì)象的地址,實(shí)例對(duì)象的isa指向類對(duì)象,類對(duì)象的isa指向元類對(duì)象。確實(shí),在arm64架構(gòu)(真機(jī)環(huán)境)前,isa單純的就是一個(gè)指針,里面存儲(chǔ)著類對(duì)象或者元類對(duì)象地址,但是arm64架構(gòu)后,系統(tǒng)對(duì)isa指針進(jìn)行了優(yōu)化,我們?cè)谠创a中可以探其結(jié)構(gòu):


image.png

  可以看到,isa是個(gè)isa_t類型的數(shù)據(jù),我們?cè)邳c(diǎn)進(jìn)去看一下isa_t是什么數(shù)據(jù):

image.png

也就是這個(gè)機(jī)構(gòu)體里面包含很多東西,但是究竟是什么東西要根據(jù)系統(tǒng)來(lái)確定。

那么在arm64架構(gòu)下,isa指針的真實(shí)結(jié)構(gòu)是:


image.png

  在我們具體分析isa內(nèi)部各個(gè)參數(shù)分別代表什么之前,我們需要弄清楚這個(gè)union是什么呢?我們看著這個(gè)union和結(jié)構(gòu)體的結(jié)構(gòu)很像,這兩者的區(qū)別如下↓↓

union:共用體,顧名思義,就是多個(gè)成員共用一塊內(nèi)存。在編譯時(shí)會(huì)選取成員中長(zhǎng)度最長(zhǎng)的來(lái)聲明。     共用體內(nèi)存=MAX(各變量)
struct:結(jié)構(gòu)體,每個(gè)成員都是獨(dú)立的一塊內(nèi)存。                                          結(jié)構(gòu)的內(nèi)存=sizeof(各變量之和)+內(nèi)存對(duì)齊

也就是說(shuō),union共用體內(nèi)所有的變量,都用同一塊內(nèi)存,而struct結(jié)構(gòu)體內(nèi)的變量是各個(gè)變量有各個(gè)變量自己的內(nèi)存,舉例說(shuō)明:


image.png

我們分別定義了一個(gè)共用體test1和一個(gè)結(jié)構(gòu)體test2,里面都各自有八個(gè)char變量,打印出來(lái)各自占用內(nèi)存我們發(fā)現(xiàn)共用體只占用了1個(gè)內(nèi)存,而結(jié)構(gòu)體占用了8個(gè)內(nèi)存,

其實(shí)結(jié)構(gòu)體占用8個(gè)內(nèi)存很好理解,8個(gè)char變量,每個(gè)char占用一個(gè),所以是8;而union共用體為什么只占用一個(gè)呢?這是因?yàn)樗麄児蚕硗粋€(gè)內(nèi)存存儲(chǔ)東西,他們的內(nèi)存結(jié)構(gòu)是這樣的:

image.png

我們看到te就一個(gè)內(nèi)存空間,也就是所有的公用體成員公用一個(gè)空間,并且同一時(shí)間只能存儲(chǔ)其中一個(gè)成員變量的值,這一點(diǎn)我們可以打斷點(diǎn)或打印進(jìn)行確認(rèn)

image.png

我們發(fā)現(xiàn),第一次打印的時(shí)候,bdf這些值都是1的打印出來(lái)都是0,這是因?yàn)楫?dāng)te.g = '0',執(zhí)行完后,這個(gè)內(nèi)存存儲(chǔ)的是g的值0,所以訪問(wèn)的時(shí)候打印結(jié)果都是0。第二次打印同理,te.h執(zhí)行完內(nèi)存中存儲(chǔ)的是1,再訪問(wèn)這塊內(nèi)存那么得到的結(jié)果都會(huì)是1。所以我們從這也可以看出,union共用體就是系統(tǒng)分配一個(gè)內(nèi)存供里面的成員共同使用,某一時(shí)間只能存儲(chǔ)其中某一個(gè)變量的值,這樣做相比結(jié)構(gòu)體而言可以很大程度的節(jié)省內(nèi)存空間。

既然我們已經(jīng)知道isa_t使用共用體的原因是為了最大限度節(jié)省內(nèi)存空間,那么各個(gè)成員后面的數(shù)字代表什么呢?這就涉及到了位域.

我們看到union共用體為了節(jié)省空間是不斷的進(jìn)行值覆蓋操作,也就是新值覆蓋舊值,結(jié)合位域的話可以更大限度的節(jié)約內(nèi)存空間還不用覆蓋舊值。我們都知道一個(gè)字節(jié)是8個(gè)bit位,所以位域的作用就是將字節(jié)這個(gè)內(nèi)存單位縮小為bit位來(lái)存儲(chǔ)東西。我們把上面這個(gè)union共用體加上位域:

image.png

 上面這段代碼的意思就是,abcdefgh這八個(gè)char變量不再是不停地覆蓋舊值操作了,而是將一個(gè)字節(jié)分成8個(gè)bit位,每個(gè)變量一個(gè)bit位,按照順序從右到左一次排列。

我們都知道char變量占用一個(gè)字節(jié),一個(gè)字節(jié)有8個(gè)bit位,也就是char變量有8位,那么te和te2的內(nèi)存結(jié)構(gòu)如下所示:


image.png

 這個(gè)結(jié)構(gòu)我們也可以通過(guò)打印來(lái)驗(yàn)證:te占用一個(gè)字節(jié)位置,內(nèi)存地址對(duì)應(yīng)的值是0xaa,轉(zhuǎn)換成二進(jìn)制正好是10101010,也就是a~h存儲(chǔ)的值。


image.png

我們可以看到,現(xiàn)在是將一個(gè)字節(jié)中的8個(gè)bit位分別讓給8個(gè)char變量存儲(chǔ)數(shù)據(jù),所以這些char變量存儲(chǔ)的數(shù)據(jù)不是0就是1,可以看出來(lái)這種方式非常省內(nèi)存空間,將一個(gè)字節(jié)分成8個(gè)bit位存儲(chǔ)東西,物盡其用。所以我們根據(jù)isa_t結(jié)構(gòu)體中的所占用bit位加起來(lái)=64可以得知isa指針占用8個(gè)字節(jié)空間。

雖然位域極大限度的節(jié)省了內(nèi)存空間,但是現(xiàn)在面臨著一個(gè)問(wèn)題,那就是如何給這些變量賦值或者取值呢?普通結(jié)構(gòu)體中因?yàn)槊總€(gè)變量都有自己的內(nèi)存地址,所以直接根據(jù)地址讀取值即可, 但是union共用體中是大家共用同一個(gè)內(nèi)存地址,只是分布在不同的bit位上,所以是沒有辦法通過(guò)內(nèi)存地址讀取值的,那么這就用到了位運(yùn)算符,我們需要知道以下幾個(gè)概念:

&:按位與,同真為真,其余為假

|:按位或,有真則真,全假則假

<<:左移,表示左移動(dòng)一位 (默認(rèn)是00000001 那么1<<1 則變成了00000010 1<<2就是00000100)

~:按位取反

掩碼 : 一般把用來(lái)進(jìn)行按位與(&)運(yùn)算來(lái)取出相應(yīng)的值的值稱之為掩碼(Mask)。如 #define TallMask 0b00000100 :TallMask就是用來(lái)取出右邊第三個(gè)bit位數(shù)據(jù)的掩碼

好,那么我們來(lái)看下這些運(yùn)算符是怎么可以做到取值賦值的呢?比如說(shuō)我們上面的te共用體內(nèi)有8個(gè)char,要是我們想出去char b的值怎么取呢?這就用到了&:


image.png

按位與上1<<1 就可以取出b位的值了,b是1那么結(jié)果就是1,b是0那么結(jié)果就是0;

同理,當(dāng)我們?yōu)閒設(shè)置值的時(shí)候,也是類似的操作,就是在改變f的值的同時(shí)不影響其他值,這里我們要看賦的值是0還是1,不同值操作不同:


image.png

所以,這就是共同體中取值賦值的操作流程,那么我們接下來(lái)回到isa指針這個(gè)結(jié)構(gòu)體中,看一下它里面的各個(gè)成員以及怎么取賦值的↓↓

/*nonpointer
         0,代表普通的指針,存儲(chǔ)著Class、Meta-Class對(duì)象的內(nèi)存地址
         1,代表優(yōu)化過(guò),使用位域存儲(chǔ)更多的信息
         */
        uintptr_t nonpointer        : 1;                                       \
        
        /*has_assoc:是否有設(shè)置過(guò)關(guān)聯(lián)對(duì)象,如果沒有,釋放時(shí)會(huì)更快*/
        uintptr_t has_assoc         : 1;                                       \
        
        /*是否有C++的析構(gòu)函數(shù)(.cxx_destruct),如果沒有,釋放時(shí)會(huì)更快*/
        uintptr_t has_cxx_dtor      : 1;                                       \
        
        /*存儲(chǔ)著Class、Meta-Class對(duì)象的內(nèi)存地址信息*/
        uintptr_t shiftcls          : 33; /*MACH_VM_MAX_ADDRESS 0x1000000000*/ \
        
        /*用于在調(diào)試時(shí)分辨對(duì)象是否未完成初始化*/
        uintptr_t magic             : 6;                                       \
        
        /*是否有被弱引用指向過(guò),如果沒有,釋放時(shí)會(huì)更快*/
        uintptr_t weakly_referenced : 1;                                       \
        
        /*對(duì)象是否正在釋放*/
        uintptr_t deallocating      : 1;                                       \
        
        /*里面存儲(chǔ)的值是引用計(jì)數(shù)器減1*/
        uintptr_t has_sidetable_rc  : 1;                                       \
        
        /*
         引用計(jì)數(shù)器是否過(guò)大無(wú)法存儲(chǔ)在isa中
         如果為1,那么引用計(jì)數(shù)會(huì)存儲(chǔ)在一個(gè)叫SideTable的類的屬性中
         */
        uintptr_t extra_rc          : 19;

我們看到,isa指針確實(shí)做了很大的優(yōu)化,同樣是占用8個(gè)字節(jié),優(yōu)化后的共用體不僅存放這類對(duì)象或元類對(duì)象地址,還存放了很多額外屬性,接下來(lái)我們對(duì)這個(gè)結(jié)構(gòu)進(jìn)行驗(yàn)證:需要注意的是因?yàn)槭莂rm64架構(gòu) 所以這個(gè)驗(yàn)證需要是ios項(xiàng)目且需要運(yùn)行在真機(jī)上 這樣才會(huì)得出準(zhǔn)確的結(jié)果

首先,我們來(lái)驗(yàn)證這個(gè)shiftcls是否就是類對(duì)象內(nèi)存地址。


image.png

我們定義了一個(gè)dog對(duì)象,我們打印它的isa是0x000001a102a48de1

從上面的分析我們得知,要取出shiftcls的值需要isa的值&ISA_MASK(這個(gè)isa_mask在源碼中有定義),得出$1 = 0x000001a102a48de0

而$1的地址值正是我們上面打印出來(lái)Dog類對(duì)象的地址值,所以這也驗(yàn)證了isa_t的結(jié)構(gòu)。


image.png

 我們還可以來(lái)看一下其他一些成員,比如說(shuō)是否被弱指針指向過(guò)?我們先將上面沒有被__weak指向過(guò)的數(shù)據(jù)保存一下,其中紅色框中的就是這個(gè)屬性,0表示沒有被指向過(guò)


image.png

 然后我們修改代碼,添加弱指針指向dog:__weak Dog *weaKDog = dog;
注意:只要設(shè)置過(guò)關(guān)聯(lián)對(duì)象或者弱引用引用過(guò)對(duì)象,has_assoc或weakly_referenced的值就會(huì)變成1,不論之后是否將關(guān)聯(lián)對(duì)象置為nil或斷開弱引用。
image.png

  發(fā)現(xiàn)確實(shí)由0變成了1,所以可以驗(yàn)證isa_t的結(jié)構(gòu),這個(gè)實(shí)驗(yàn)要確保程序運(yùn)行在真機(jī)才能出現(xiàn)這個(gè)結(jié)果。所以arm64后確實(shí)對(duì)isa指針做了優(yōu)化處理,不在單純的存放類對(duì)象或者元類對(duì)象的內(nèi)存地址,而是除此之外存儲(chǔ)了更多內(nèi)容。

2、class的具體結(jié)構(gòu)

我們之前在講分類的時(shí)候講到了類的大體結(jié)構(gòu),如下圖所示:

image.png

 就如我們之前講到的,當(dāng)我們調(diào)用方法的時(shí)候是從bits中的methods中查找方法,分類的方法是排在主類方法前面的,所以調(diào)用同名方法是先調(diào)用分類的,而且究竟調(diào)用哪個(gè)分類的方法要取決于編譯的先后順序等等:
image.png

那么這個(gè)rw_t中的methods和ro_t中的methods有什么不一樣呢?

首先,ro_t中methods,是只包含原始類的方法,不包括分類的,而rw_t中的methods即包含原始類的也包含分類的;

其次,ro_t中的methods只能讀取不能修改,而rw_t中的methods既可以讀取也可以修改,所以我們今后在動(dòng)態(tài)添加方法修改方法的時(shí)候是在rw_t中的methods去操作的;

然后,ro_t中的methods是個(gè)一維數(shù)組,里面存放著method_t(對(duì)方法/函數(shù)的封裝,即一個(gè)method_t代表一個(gè)方法或函數(shù)),而rw_t中的methods是個(gè)二維數(shù)組,里面存放著各個(gè)分類和原始類的數(shù)組,分類和原始類的數(shù)組中存放著method_t。即:


image.png

我們也可以在源碼中找到rw_t和ro_t的關(guān)系,

static Class realizeClass(Class cls)
{
    runtimeLock.assertLocked();

    const class_ro_t *ro;
    class_rw_t *rw;
    Class supercls;
    Class metacls;
    bool isMeta;

    if (!cls) return nil;
    if (cls->isRealized()) return cls;
    assert(cls == remapClass(cls));
    
    // 最開始cls->data是指向ro的
    ro = (const class_ro_t *)cls->data();
    if (ro->flags & RO_FUTURE) {
        // rw已經(jīng)初始化并且分配內(nèi)存空間
        rw = cls->data(); // cls->data指向rw
        ro = cls->data()->ro; // cls->data()->ro指向ro  即rw中的ro指向ro
        cls->changeInfo(RW_REALIZED|RW_REALIZING, RW_FUTURE);
    } else {
        // 如果rw并不存在,則為rw分配空間
        rw = (class_rw_t *)calloc(sizeof(class_rw_t), 1);// 分配空間
        rw->ro = ro;// rw->ro重新指向ro
        rw->flags = RW_REALIZED|RW_REALIZING;
        // 將rw傳入setData函數(shù),等于cls->data()重新指向rw
        cls->setData(rw);
    }
}

首先,cls->data(即bits)是指向存儲(chǔ)類初始化信息的ro_t的,然后在運(yùn)行過(guò)程中創(chuàng)建了class_rw_t,等rw_t分配好內(nèi)存空間后,開始將cls->data指向了rw_t并將rw_t中的ro指向了存儲(chǔ)初始化信息的ro_t。

那么ro_t和rw_t中存儲(chǔ)的這個(gè)method_t是個(gè)什么結(jié)構(gòu)呢?我們閱讀源碼發(fā)現(xiàn)結(jié)構(gòu)如下,我們發(fā)現(xiàn)有三個(gè)成員:name、types、imp,我們一一來(lái)看:

image

name,表示方法的名稱,一般叫做選擇器,可以通過(guò)@selector()sel_registerName()獲得。

/*
比如test方法,它的SEL就是@selector(test);或者sel_registerName("test");需要注意的一點(diǎn)就是不同類中的同名方法,它們的方法選擇器是相同的,比如A、B兩個(gè)類中都有test方法,那么這兩個(gè)test方法的名稱都是@selector(test);或者sel_registerName("test"); 
*/

types,表示方法的編碼,即返回值、參數(shù)的類型,通過(guò)字符串拼接的方式將返回值和參數(shù)拼接成一個(gè)字符串,來(lái)代表函數(shù)返回值及參數(shù)。

/*
   比如ViewDidload方法,我們都知道它的返回值是void,參數(shù)轉(zhuǎn)為底層語(yǔ)言后是self和_cmd,即一個(gè)id類型和一個(gè)方法選擇器,那么encode后就是v16@0:8(它所表示的意思是:返回值是void類型,參數(shù)一共占用16個(gè)字節(jié),第一個(gè)參數(shù)是@類型,內(nèi)存空間從0開始,第二個(gè)參數(shù)是:類型,內(nèi)存空間從8開始),當(dāng)然這里的數(shù)字可以不寫,簡(jiǎn)寫成V@:   
*/

關(guān)于更多encode規(guī)則,可以查看下面這個(gè)表:

image

當(dāng)然除了自己手寫外,iOS提供了@encode的指令,可以將具體的類型轉(zhuǎn)化成字符串編碼。

NSLog(@"%s",@encode(int));
NSLog(@"%s",@encode(float));
NSLog(@"%s",@encode(id));
NSLog(@"%s",@encode(SEL));

// 打印內(nèi)容
Runtime-test[25275:9144176] i
Runtime-test[25275:9144176] f
Runtime-test[25275:9144176] @
Runtime-test[25275:9144176] :

imp,表示指向函數(shù)的指針(函數(shù)地址),即方法的具體實(shí)現(xiàn),我們調(diào)用的方法實(shí)際上最后都是通過(guò)這個(gè)imp去進(jìn)行最終操作的。

3、方法緩存

我們?cè)诜治銮宄椒斜砗头椒ǖ慕Y(jié)構(gòu)后,我們?cè)賮?lái)看一下方法的調(diào)用是怎么一個(gè)流程呢?是直接去方法列表里面遍歷查找對(duì)應(yīng)的方法嗎?

其實(shí)不然,我們?cè)诜治鲱惖慕Y(jié)構(gòu)的時(shí)候,除了bits(指向類的具體信息,包括rw_t、ro_t等等一些內(nèi)容)外,還有一個(gè)方法緩存:cache,用來(lái)緩存曾經(jīng)調(diào)用過(guò)的方法

image

所以系統(tǒng)查找對(duì)應(yīng)方法不是通過(guò)遍歷rw_t這個(gè)二維數(shù)組來(lái)尋找方法的,這樣做太慢,效率太低,系統(tǒng)是先從方法緩存中找有沒有對(duì)應(yīng)的方法,有的話就直接調(diào)用緩存里的方法,根據(jù)imp去調(diào)用方法,沒有的話,就再去方法數(shù)組中遍歷查找,找到后調(diào)用并保存到方法緩存里,流程如下:

image

那么方法是怎么緩存到cache中的呢?系統(tǒng)又是怎么查找緩存中的方法的呢?我們通過(guò)源碼來(lái)看一下cache的結(jié)構(gòu):

散列表(Hash table,也叫哈希表),是根據(jù)關(guān)鍵碼值(Key value)而直接進(jìn)行訪問(wèn)的數(shù)據(jù)結(jié)構(gòu)。也就是說(shuō),它通過(guò)把關(guān)鍵碼值映射到表中一個(gè)位置來(lái)訪問(wèn)記錄,以加快查找的速度。這個(gè)映射函數(shù)叫做散列函數(shù),存放記錄的數(shù)組叫做散列表。

我們可以看到,cache_t里面就三個(gè)成員,后兩個(gè)代表長(zhǎng)度和數(shù)量,是int類型,肯定不是存儲(chǔ)方法的地方,所以方法應(yīng)該是存儲(chǔ)在_buckets這個(gè)散列表中。散列存儲(chǔ)的是一個(gè)個(gè)的bucket_t的結(jié)構(gòu)體,那么這個(gè)bucket_t又是個(gè)什么結(jié)構(gòu)呢?

image

所以cache_t底部結(jié)構(gòu)是這樣的:

image

  我們看到,bucket_t就兩個(gè)值,一個(gè)key一個(gè)imp,key的話就是方法名,也就是SEL,而imp就是Value,也就是當(dāng)我們調(diào)用一個(gè)方法是來(lái)到方法緩存中查找,通過(guò)比對(duì)方法名是不是一致,一致的話就返回對(duì)應(yīng)的imp,也就是方法地址,從而可以調(diào)用方法,那么這個(gè)散列表是怎么查找的呢?難道也是通過(guò)遍歷嗎?

我們通過(guò)閱讀源碼來(lái)一探究竟:

image

 通過(guò)上面代碼的閱讀,我們可以知道系統(tǒng)在cache_t中查找方法并不是通過(guò)遍歷,而是通過(guò)方法名SEL&mask得到一個(gè)索引,直接去讀數(shù)組索引中的方法,如果該方法的SEL與我們調(diào)用的方法名SEL一直,那么就返回這個(gè)方法,否則就一直向下尋找直到找完為止。

好,既然取值的時(shí)候不是遍歷,而是直接讀的索引,那么講方法存儲(chǔ)到緩存中也肯定是通過(guò)這種方式了,直接方法名&mask拿到索引,然后將_key和_imp存儲(chǔ)到對(duì)應(yīng)的索引上,這一點(diǎn)我們通過(guò)源碼也可以確認(rèn):

image

我們看到無(wú)論是存還是讀,都是調(diào)用了find函數(shù),查看SEL&mask對(duì)應(yīng)的索引的方法,不合適的話再向下尋找直到找到合適的位置。

那么這里有兩個(gè)疑問(wèn),為什么SEL&mask會(huì)出現(xiàn)不是該方法名(讀)或者不為空(寫)的情況呢?散列表擴(kuò)容后方法還在嗎?

首先,SEL&mask這個(gè)問(wèn)題,是因?yàn)椴煌姆椒?amp;mask可能出現(xiàn)同一個(gè)結(jié)果,比如test方法的SEL是011,run方法的SEL是010,mask是010,那么無(wú)論是test的SEL&mask還是run的SEL&mask 記過(guò)都是010,如果大家都存在這個(gè)索引里面是會(huì)出問(wèn)題的,所以為了解決這個(gè)索引重復(fù)的問(wèn)題需要先做判斷,即拿到索引后先判斷這個(gè)索引對(duì)應(yīng)的值是不是你想要的,是的話你拿走用,不是的話向下繼續(xù)找,方法緩存也是同樣的道理。我們先調(diào)用test方法,緩存到010索引,再調(diào)用run方法,發(fā)現(xiàn)010位置不為空了,那就判斷010下面的索引是否為空,為空的話就將run方法緩存到這個(gè)位置。

關(guān)于散列表擴(kuò)容后,緩存方法在不在的問(wèn)題,通過(guò)源碼就可以知道,舊散列表已經(jīng)釋放掉了,所以是不存在的,再次調(diào)用的時(shí)候就得重新去rw_t中遍歷找方法然后重新緩存到散列表中,比如下面這個(gè)例子:

image

  更正更正更正

我們前面講到當(dāng)SEL&mask出來(lái)一個(gè)索引發(fā)現(xiàn)被占用或者不是我想要的時(shí)候,系統(tǒng)是向索引下一位再次尋找,這個(gè)地方失誤了,不是向下是向上尋找,這個(gè)地方看源碼的時(shí)候忽略了條件,在x86或者i386架構(gòu)中是向下尋找,在arm64架構(gòu)中是向上尋找:(因?yàn)樯厦鎴D片資源都已經(jīng)刪掉了就沒有再更改,這里需要注意一下)

image

到現(xiàn)在我們清楚了,那就是散列表中并不是按照索引依次排序或者遍歷索引依次讀取,那么就會(huì)出現(xiàn)個(gè)問(wèn)題,因?yàn)镾EL&mask是個(gè)小于mask的隨機(jī)值且散列表存儲(chǔ)空間超過(guò)3/4的時(shí)候就要擴(kuò)容,那就會(huì)導(dǎo)致散列表中有一部分空間始終被限制。確實(shí),散列表當(dāng)分配內(nèi)存后,每個(gè)地方最初都是null的,當(dāng)某個(gè)位置的索引被用到時(shí),對(duì)應(yīng)的位置才會(huì)存儲(chǔ)方法,其余位置仍處于空閑狀態(tài),但是這樣做可以極大提高查找速度(比遍歷快很多),所以這是一種空間換時(shí)間的方式。

image

4.方法的傳遞過(guò)程

我們現(xiàn)在已經(jīng)清楚方法的調(diào)用順序了,實(shí)現(xiàn)從緩存中找沒有的話再去rw_t中找,那么在沒有的話就去其父類中找,父類中查找也是如此,先去父類中的cache中查找,沒有的話再去父類的rw_t中找,以此類推。如果查找到基類還沒有呢?難道就直接報(bào)unrecognized selector sent to instance 這個(gè)經(jīng)典錯(cuò)誤嗎?

其實(shí)不是,方法的傳遞主要涉及到三個(gè)部分,這也是我們平時(shí)用得最多以及面試中經(jīng)常出現(xiàn)的問(wèn)題:

我們都知道,當(dāng)我們調(diào)用一個(gè)方法是,其實(shí)底層是將這個(gè)方法轉(zhuǎn)換成了objc_msgSend函數(shù)來(lái)進(jìn)行調(diào)用,objc_msgSend的執(zhí)行流程可以分為3大階段:

消息發(fā)送->動(dòng)態(tài)方法解析->消息轉(zhuǎn)發(fā)

這個(gè)流程我們是可以從源碼中得到確認(rèn),以下是源碼:

1 /***********************************************************************
  2 * _class_lookupMethodAndLoadCache.
  3 * Method lookup for dispatchers ONLY. OTHER CODE SHOULD USE lookUpImp().
  4 * This lookup avoids optimistic cache scan because the dispatcher 
  5 * already tried that.
  6 **********************************************************************/
  7 IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
  8 {
  9     return lookUpImpOrForward(cls, sel, obj, 
 10                               YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
 11 }
 12 
 13 
 14 /***********************************************************************
 15 * lookUpImpOrForward.
 16 * The standard IMP lookup. 
 17 * initialize==NO tries to avoid +initialize (but sometimes fails)
 18 * cache==NO skips optimistic unlocked lookup (but uses cache elsewhere)
 19 * Most callers should use initialize==YES and cache==YES.
 20 * inst is an instance of cls or a subclass thereof, or nil if none is known. 
 21 *   If cls is an un-initialized metaclass then a non-nil inst is faster.
 22 * May return _objc_msgForward_impcache. IMPs destined for external use 
 23 *   must be converted to _objc_msgForward or _objc_msgForward_stret.
 24 *   If you don't want forwarding at all, use lookUpImpOrNil() instead.
 25 **********************************************************************/
 26 //這個(gè)函數(shù)是方法調(diào)用流程的函數(shù) 即消息發(fā)送->動(dòng)態(tài)方法解析->消息轉(zhuǎn)發(fā)
 27 IMP lookUpImpOrForward(Class cls, SEL sel, id inst, 
 28                        bool initialize, bool cache, bool resolver)
 29 {
 30     IMP imp = nil;
 31     bool triedResolver = NO;
 32 
 33     runtimeLock.assertUnlocked();
 34 
 35     // Optimistic cache lookup
 36     if (cache) {
 37         imp = cache_getImp(cls, sel);
 38         if (imp) return imp;
 39     }
 40 
 41     // runtimeLock is held during isRealized and isInitialized checking
 42     // to prevent races against concurrent realization.
 43 
 44     // runtimeLock is held during method search to make
 45     // method-lookup + cache-fill atomic with respect to method addition.
 46     // Otherwise, a category could be added but ignored indefinitely because
 47     // the cache was re-filled with the old value after the cache flush on
 48     // behalf of the category.
 49 
 50     runtimeLock.lock();
 51     checkIsKnownClass(cls);
 52 
 53     if (!cls->isRealized()) {
 54         realizeClass(cls);
 55     }
 56 
 57     if (initialize  &&  !cls->isInitialized()) {
 58         runtimeLock.unlock();
 59         _class_initialize (_class_getNonMetaClass(cls, inst));
 60         runtimeLock.lock();
 61         // If sel == initialize, _class_initialize will send +initialize and 
 62         // then the messenger will send +initialize again after this 
 63         // procedure finishes. Of course, if this is not being called 
 64         // from the messenger then it won't happen. 2778172
 65     }
 66 
 67     
 68  retry:    
 69     runtimeLock.assertLocked();
 70 
 71     // Try this class's cache.
 72     //先從當(dāng)前類對(duì)象的方法緩存中查看有沒有對(duì)應(yīng)方法
 73     imp = cache_getImp(cls, sel);
 74     if (imp) goto done;
 75 
 76     // Try this class's method lists.
 77     //沒有的話再?gòu)念悓?duì)象的方法列表中尋找
 78     {
 79         Method meth = getMethodNoSuper_nolock(cls, sel);
 80         if (meth) {
 81             log_and_fill_cache(cls, meth->imp, sel, inst, cls);
 82             imp = meth->imp;
 83             goto done;
 84         }
 85     }
 86 
 87     // Try superclass caches and method lists.
 88     {
 89         unsigned attempts = unreasonableClassCount();
 90         //遍歷所有父類 知道其父類為空
 91         for (Class curClass = cls->superclass;
 92              curClass != nil;
 93              curClass = curClass->superclass)
 94         {
 95             // Halt if there is a cycle in the superclass chain.
 96             if (--attempts == 0) {
 97                 _objc_fatal("Memory corruption in class list.");
 98             }
 99             
100             // Superclass cache.
101             //先查找父類的方法緩存
102             imp = cache_getImp(curClass, sel);
103             if (imp) {
104                 if (imp != (IMP)_objc_msgForward_impcache) {
105                     // Found the method in a superclass. Cache it in this class.
106                     log_and_fill_cache(cls, imp, sel, inst, curClass);
107                     goto done;
108                 }
109                 else {
110                     // Found a forward:: entry in a superclass.
111                     // Stop searching, but don't cache yet; call method 
112                     // resolver for this class first.
113                     break;
114                 }
115             }
116             
117             // Superclass method list.
118             //再查找父類的方法列表
119             Method meth = getMethodNoSuper_nolock(curClass, sel);
120             if (meth) {
121                 log_and_fill_cache(cls, meth->imp, sel, inst, curClass);
122                 imp = meth->imp;
123                 goto done;
124             }
125         }
126     }
127 
128     // No implementation found. Try method resolver once.
129     //消息發(fā)送階段沒找到imp 嘗試進(jìn)行一次動(dòng)態(tài)方法解析
130     if (resolver  &&  !triedResolver) {
131         runtimeLock.unlock();
132         _class_resolveMethod(cls, sel, inst);
133         runtimeLock.lock();
134         // Don't cache the result; we don't hold the lock so it may have 
135         // changed already. Re-do the search from scratch instead.
136         triedResolver = YES;
137         //跳轉(zhuǎn)到retry入口  retry入口就在上面,也就是x消息發(fā)送過(guò)程即找緩存找rw_t
138         goto retry;
139     }
140 
141     // No implementation found, and method resolver didn't help. 
142     // Use forwarding.
143     //消息發(fā)送階段沒找到imp而且執(zhí)行動(dòng)態(tài)方法解析也沒有幫助 那么就執(zhí)行方法轉(zhuǎn)發(fā)
144     imp = (IMP)_objc_msgForward_impcache;
145     cache_fill(cls, sel, imp, inst);
146 
147  done:
148     runtimeLock.unlock();
149 
150     return imp;
151 }

消息傳遞過(guò)程源碼實(shí)現(xiàn)

首先,消息發(fā)送,就是我們剛才提到的系統(tǒng)會(huì)先去cache_t中查找,有的話調(diào)用,沒有的話去類對(duì)象的rw_t中查找,有的話調(diào)用并緩存到cache_t中,沒有的話根據(jù)supperclass指針去父類中查找。父類查找也是如此,先去父類的cache_t中查找,有的話進(jìn)行調(diào)用并添加到自己的cache_t中而不是父類的cache_t中,沒有的話再去父類的rw_t中查找,有的話調(diào)用并緩存到自己的cache_t中,沒有的話以此類推。流程如下:

image

當(dāng)消息發(fā)送找到最后一個(gè)父類還沒有找到對(duì)應(yīng)的方法時(shí),就會(huì)來(lái)到動(dòng)態(tài)方法解析。動(dòng)態(tài)解析,就是意味著開發(fā)者可以在這里動(dòng)態(tài)的往rw_t中添加方法實(shí)現(xiàn),這樣的話系統(tǒng)再次遍歷rw_t就會(huì)找到對(duì)應(yīng)的方法進(jìn)行調(diào)用了。

動(dòng)態(tài)方法解析的流程示意圖如下:

image

主要涉及到了兩個(gè)方法:

+resolveInstanceMethod://添加對(duì)象方法  也就是-開頭的方法
+resolveClassMethod://添加類方法  也就是+開頭的方法

我們?cè)趯?shí)際項(xiàng)目中進(jìn)行驗(yàn)證:

image.png

動(dòng)態(tài)添加類方法也是如此,只不過(guò)是添加到元類對(duì)象中(此時(shí)run方法已經(jīng)改成了個(gè)類方法)

image

而且我們也發(fā)現(xiàn),動(dòng)態(tài)添加方法的話其實(shí)無(wú)非就是找到方法實(shí)現(xiàn),添加到類對(duì)象或元類對(duì)象中,至于這個(gè)方法實(shí)現(xiàn)是什么形式都沒有關(guān)系,比如說(shuō)我們?cè)俳o對(duì)象方法添加方法實(shí)現(xiàn)時(shí),這個(gè)實(shí)現(xiàn)方法可以是個(gè)類方法,同樣給類方法動(dòng)態(tài)添加方法實(shí)現(xiàn)時(shí)也可以是對(duì)象方法。也就是說(shuō)系統(tǒng)根本沒有區(qū)分類方法和對(duì)象方法,只要把imp添加到元類對(duì)象的rw_t中就是類方法,添加到類對(duì)象中就是對(duì)象方法。

image

  當(dāng)我們?cè)谙l(fā)送和動(dòng)態(tài)消息解析階段都沒有找到對(duì)應(yīng)的imp的時(shí)候,系統(tǒng)回來(lái)到最后一個(gè)消息轉(zhuǎn)發(fā)階段。所謂消息轉(zhuǎn)發(fā),就是你這個(gè)消息處理不了后可以找其他人或者其他方法來(lái)代替,消息轉(zhuǎn)發(fā)的流程示意圖如下:

image

即分為兩步,第一步是看能不能找其他人代你處理這方法,可以的話直接調(diào)用這個(gè)人的這個(gè)方法,這一步不行的話就來(lái)到第二部,這個(gè)方法沒有的話有沒有可以替代的方法,有的話就執(zhí)行替代方法。我們通過(guò)代碼來(lái)驗(yàn)證:

我們調(diào)用dog的run方法是,因?yàn)閐og本身沒有實(shí)現(xiàn)這個(gè)方法,所以不能處理。正好cat實(shí)現(xiàn)了這個(gè)方法,所以我們就將這個(gè)方法轉(zhuǎn)發(fā)給cat處理:

image

我們發(fā)現(xiàn),確實(shí)調(diào)用了小貓run方法,但是只轉(zhuǎn)發(fā)方法執(zhí)行者太局限了,要求接收方法對(duì)象必須實(shí)現(xiàn)了同樣的方法才行,否則還是無(wú)法處理,所以實(shí)用性不強(qiáng)。這時(shí)候,我們可以通過(guò)methodSignatureForSelector來(lái)進(jìn)行更大限度的轉(zhuǎn)發(fā)。

需要注意的是要想來(lái)到methodSignatureForSelector這一步需要將*****forwardingTargetForSelector返回nil(即默認(rèn)狀態(tài))否則系統(tǒng)找到目標(biāo)執(zhí)行者后就不會(huì)再往下轉(zhuǎn)發(fā)了。*

開發(fā)者可以在forwardInvocation:方法中自定義任何邏輯。

////為方法重新轉(zhuǎn)發(fā)一個(gè)目標(biāo)執(zhí)行
//- (id)forwardingTargetForSelector:(SEL)aSelector{
//    if (aSelector == @selector(run)) {
//        //dog的run方法沒有實(shí)現(xiàn) 所以我們將此方法轉(zhuǎn)發(fā)到cat對(duì)象上去實(shí)現(xiàn) 也就是相當(dāng)于將[dog run]轉(zhuǎn)換成[cat run]
//        return [[Cat alloc] init];
//    }
//    return [super forwardingTargetForSelector:aSelector];
//}

//方法簽名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
    if (aSelector == @selector(run)) {
        //注意:這里返回的是我們要轉(zhuǎn)發(fā)的方法的簽名 比如我們現(xiàn)在是轉(zhuǎn)發(fā)run方法 那就是返回的就是run方法的簽名

        //1.可以使用methodSignatureForSelector:方法從實(shí)例中請(qǐng)求實(shí)例方法簽名,或者從類中請(qǐng)求類方法簽名。
        //2.也可以使用instanceMethodSignatureForSelector:方法從一個(gè)類中獲取實(shí)例方法簽名
        //這里使用self的話會(huì)進(jìn)入死循環(huán) 所以不可以使用 如果其他方法中有同名方法可以將self換成其他類
//        return [self methodSignatureForSelector:aSelector];
//        return [NSMethodSignature instanceMethodSignatureForSelector:aSelector];
        
        //3.直接輸入字符串
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }
    return [super methodSignatureForSelector:aSelector];
}
//當(dāng)返回方法簽名后 就會(huì)轉(zhuǎn)發(fā)到這個(gè)方法  所以我們可以在這里做想要實(shí)現(xiàn)的功能  可操作空間很大
//這個(gè)anInvocation里面有轉(zhuǎn)發(fā)方法的信息,比如方法調(diào)用者/SEL/types/參數(shù)等等信息
- (void)forwardInvocation:(NSInvocation *)anInvocation{
    //這樣寫不安全  可以導(dǎo)致cat被過(guò)早釋放掉引發(fā)懷內(nèi)存訪問(wèn)
//    anInvocation.target = [[Cat alloc] init];
    
    Cat *ca = [[Cat alloc] init];
    //指定target
    anInvocation.target = ca;
    //對(duì)anInvocation做出修改后要執(zhí)行invoke方法保存修改
    [anInvocation invoke];
    
    //或者干脆一行代碼搞定
    [anInvocation invokeWithTarget:[[Cat alloc] init]];
    
    //上面這段代碼相當(dāng)于- (id)forwardingTargetForSelector:(SEL)aSelector{}中的操作
    //當(dāng)然 轉(zhuǎn)發(fā)到這里的話可操作性更大  也可以什么都不寫 相當(dāng)于轉(zhuǎn)發(fā)到的這個(gè)方法是個(gè)空方法  也不會(huì)報(bào)方法找不到的錯(cuò)誤
    //也可以在這里將報(bào)錯(cuò)信息提交給后臺(tái)統(tǒng)計(jì) 比如說(shuō)某個(gè)方法找不到提交給后臺(tái) 方便線上錯(cuò)誤收集
    //...很多用處
}

當(dāng)然我們也可以訪問(wèn)修改anInvocation的參數(shù),比如現(xiàn)在run有個(gè)age參數(shù),

  // 參數(shù)順序:receiver、selector、other arguments
    int age;    
    //索引為2的參數(shù)已經(jīng)放到了&age的內(nèi)存中,我們可以通過(guò)age來(lái)訪問(wèn)
    [anInvocation getArgument:&age atIndex:2];
    NSLog(@"%d", age + 10);

我們發(fā)現(xiàn),消息轉(zhuǎn)發(fā)有兩種情況,一種是forwardingTargetForSelector,一種是methodSignatureForSelector+forwardInvocation:

其實(shí),第一種也稱快速轉(zhuǎn)發(fā),特點(diǎn)就是簡(jiǎn)單方便,缺點(diǎn)就是能做的事情有限,只能轉(zhuǎn)發(fā)消息調(diào)用者;第二種也稱標(biāo)準(zhǔn)轉(zhuǎn)發(fā),缺點(diǎn)就是寫起來(lái)麻煩點(diǎn),需要寫方法簽名等信息,但是好處就是可以很大成都的自定義方法的轉(zhuǎn)發(fā),可以在找不到方法imp的時(shí)候做任何邏輯。

當(dāng)然,我們上面的例子都是通過(guò)對(duì)象方法來(lái)演示消息轉(zhuǎn)發(fā)的,類方法同樣存在消息轉(zhuǎn)發(fā),只不過(guò)對(duì)應(yīng)的方法都是類方法,也就是-變+

  
image

所以,以上關(guān)于消息傳遞過(guò)程可以用下面這個(gè)流程圖進(jìn)一步總結(jié):

image

關(guān)于源碼閱讀指南:

image

5、super的相關(guān)內(nèi)容

首先我們來(lái)看一下這段代碼:

image

 我們發(fā)現(xiàn)最終的打印結(jié)果和我們預(yù)期的不一樣,按我們的思路Super就是指的的Dog的父類Animal,Animal調(diào)用class方法應(yīng)該返回Animal 但是結(jié)果卻不是這樣,這是為什么?首先我們先將這段代碼轉(zhuǎn)換成c++底層代碼來(lái)一探究竟:

static instancetype _I_Dog_init(Dog * self, SEL _cmd) {
    self = ((Dog *(*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("Dog"))}, sel_registerName("init"));
    if (self) {
        // NSLog(@"%@",[self class]);
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_f1_q0392lf551qfbg1b5sy48qb80000gn_T_Dog_db6ed5_mi_0,((Class (*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("class")));
        
       //NSLog(@"%@",[self superclass]);
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_f1_q0392lf551qfbg1b5sy48qb80000gn_T_Dog_db6ed5_mi_1,((Class (*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("superclass")));
        
        //NSLog(@"%@",[super class]);
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_f1_q0392lf551qfbg1b5sy48qb80000gn_T_Dog_db6ed5_mi_2,((Class (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("Dog"))}, sel_registerName("class")));
        
        //NSLog(@"%@",[super superclass]);
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_f1_q0392lf551qfbg1b5sy48qb80000gn_T_Dog_db6ed5_mi_3,((Class (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("Dog"))}, sel_registerName("superclass")));
    }
    return self;
}

將上述代碼簡(jiǎn)化后得到下面的結(jié)果:

image

我們發(fā)現(xiàn),當(dāng)self調(diào)用class方法時(shí),是執(zhí)行的objc_msdSend(self,@selector(class))函數(shù),消息的接收者是當(dāng)前所在類的實(shí)例對(duì)象(Dog) , 這個(gè)時(shí)候就會(huì)去self所在類 Dog去查找class方法 , 如果當(dāng)前類Dog沒有class方法會(huì)向其父類Animal類找 class 方法, 如果Animal類也沒有找到class方法,最終會(huì)找到最頂級(jí)父類NSObject的class方法, 最終找到NSObject的class方法 ,并調(diào)用了object_getClass(self) ,由于消息接收者是 self 當(dāng)前類實(shí)例對(duì)象, 所以最終 [self class]輸出Dog(class方法是返回方法調(diào)用者的類型,superclass方法是返回方法調(diào)用者的父類)

[self superclass] 也是同理,找到superclass方法,然后返回調(diào)用者的父類,即Animal;

但是當(dāng)我們調(diào)用super的class方法時(shí),底層不是轉(zhuǎn)換成objc_msdSend而是變成了objc_msgSendSuper函數(shù)。這個(gè)函數(shù)有兩個(gè)參數(shù),第一個(gè)參數(shù)是個(gè)結(jié)構(gòu)體,結(jié)構(gòu)體中有兩個(gè)成員:方法調(diào)用者和調(diào)用者的父類,第二個(gè)參數(shù)就是方法名,也就是class方法的SEL。

[super class] ->
objc_msgSendSuper(
                  //第一個(gè)參數(shù):結(jié)構(gòu)體
                  {self,//方法調(diào)用者
                   class_getSuperclass(objc_getClass("Dog"))//當(dāng)前類的父類
                  },
                  //第二個(gè)參數(shù):方法名
                  sel_registerName("class")));

所以,我們看到[self class]和[super class],他們轉(zhuǎn)換成的底層實(shí)現(xiàn)都不一致。objc_msgSendSuper函數(shù)的作用是告訴方法調(diào)用者去其父類中查找該方法,也就是相比objc_msdSend函數(shù)而言少了去自己類中查找方法這一步,而是直接去父類中找class方法,但是方法調(diào)用者還是沒變,都是Dog。class方法和superclass它們都是返回方法調(diào)用者的類型或父類,所以[super class]和[super superclass]還是返回的Dog的類型和父類,所以打印結(jié)果是Dog和Animal,與[self class]和[self superclass]結(jié)果一致。

所以,總結(jié)起來(lái)就是,super方法底層會(huì)轉(zhuǎn)換為objc_msgSendSuper函數(shù)的調(diào)用,這個(gè)函數(shù)的作用是告訴方法調(diào)用者去父類中查找方法。

6、runtime的常見API與應(yīng)用案例

動(dòng)態(tài)創(chuàng)建一個(gè)類(參數(shù):父類,類名,額外的內(nèi)存空間)
Class objc_allocateClassPair(Class superclass, const char *name, size_t extraBytes)

注冊(cè)一個(gè)類(要在類注冊(cè)之前添加成員變量)
void objc_registerClassPair(Class cls)

銷毀一個(gè)類
void objc_disposeClassPair(Class cls)

獲取isa指向的Class
Class object_getClass(id obj)

設(shè)置isa指向的Class
Class object_setClass(id obj, Class cls)

判斷一個(gè)OC對(duì)象是否為Class
BOOL object_isClass(id obj)

判斷一個(gè)Class是否為元類
BOOL class_isMetaClass(Class cls)

獲取父類
Class class_getSuperclass(Class cls)

獲取一個(gè)實(shí)例變量信息
Ivar class_getInstanceVariable(Class cls, const char *name)

拷貝實(shí)例變量列表(最后需要調(diào)用free釋放)
Ivar *class_copyIvarList(Class cls, unsigned int *outCount)

設(shè)置和獲取成員變量的值
void object_setIvar(id obj, Ivar ivar, id value)
id object_getIvar(id obj, Ivar ivar)

動(dòng)態(tài)添加成員變量(已經(jīng)注冊(cè)的類是不能動(dòng)態(tài)添加成員變量的)
BOOL class_addIvar(Class cls, const char * name, size_t size, uint8_t alignment, const char * types)

獲取成員變量的相關(guān)信息
const char *ivar_getName(Ivar v)
const char *ivar_getTypeEncoding(Ivar v)

獲取一個(gè)屬性
objc_property_t class_getProperty(Class cls, const char *name)

拷貝屬性列表(最后需要調(diào)用free釋放)
objc_property_t *class_copyPropertyList(Class cls, unsigned int *outCount)

動(dòng)態(tài)添加屬性
BOOL class_addProperty(Class cls, const char *name, const objc_property_attribute_t *attributes,
                       unsigned int attributeCount)

動(dòng)態(tài)替換屬性
void class_replaceProperty(Class cls, const char *name, const objc_property_attribute_t *attributes,
                           unsigned int attributeCount)

獲取屬性的一些信息
const char *property_getName(objc_property_t property)
const char *property_getAttributes(objc_property_t property)

獲得一個(gè)實(shí)例方法、類方法
Method class_getInstanceMethod(Class cls, SEL name)
Method class_getClassMethod(Class cls, SEL name)

方法實(shí)現(xiàn)相關(guān)操作
IMP class_getMethodImplementation(Class cls, SEL name)
IMP method_setImplementation(Method m, IMP imp)
void method_exchangeImplementations(Method m1, Method m2)

拷貝方法列表(最后需要調(diào)用free釋放)
Method *class_copyMethodList(Class cls, unsigned int *outCount)

動(dòng)態(tài)添加方法
BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types)

動(dòng)態(tài)替換方法
IMP class_replaceMethod(Class cls, SEL name, IMP imp, const char *types)

獲取方法的相關(guān)信息(帶有copy的需要調(diào)用free去釋放)
SEL method_getName(Method m)
IMP method_getImplementation(Method m)
const char *method_getTypeEncoding(Method m)
unsigned int method_getNumberOfArguments(Method m)
char *method_copyReturnType(Method m)
char *method_copyArgumentType(Method m, unsigned int index)

選擇器相關(guān)
const char *sel_getName(SEL sel)
SEL sel_registerName(const char *str)

用block作為方法實(shí)現(xiàn)
IMP imp_implementationWithBlock(id block)
id imp_getBlock(IMP anImp)
BOOL imp_removeBlock(IMP anImp)

runtime相關(guān)API

這些api中有些我們用的比較少,有的比較常用,比如我們?cè)谛薷腢ITextField的占位文字顏色的話,可以通過(guò)獲取UITextField的成員列表,發(fā)現(xiàn)其中占位文字的顯示其實(shí)是個(gè)placeholderLabel,所以我們直接可以通過(guò)kvo修改這個(gè)label的顏色:

image

還有比如經(jīng)常用的字典轉(zhuǎn)模型框架MJExtension中,也是通過(guò)runtime函數(shù)遍歷所有的屬性或者成員變量,然后利用KVO去設(shè)值等等。

另外需要注意的一點(diǎn)是,我們一般將動(dòng)態(tài)添加方法、方法交換等等這些運(yùn)行時(shí)操作放在load方法里面實(shí)現(xiàn),我們?cè)谥爸v解分類的時(shí)候提到了:

當(dāng)類被引用進(jìn)項(xiàng)目的時(shí)候就會(huì)執(zhí)行l(wèi)oad函數(shù)(在main函數(shù)開始執(zhí)行之前),與這個(gè)類是否被用到無(wú)關(guān),每個(gè)類的load函數(shù)只會(huì)自動(dòng)調(diào)用一次.也就是load函數(shù)是系統(tǒng)自動(dòng)加載的,load方法會(huì)在runtime加載類、分類時(shí)調(diào)用。

方法交換一般用在將系統(tǒng)的某個(gè)方法交換成我們自己寫的方法從而實(shí)現(xiàn)相應(yīng)功能,這里也有兩點(diǎn)需要注意:

 ?、俦苊馑姥h(huán)

方法交換交換的只是兩個(gè)方法的實(shí)現(xiàn),也就是imp的交換,所以原理如下:

image

用代碼來(lái)演示,比如我們現(xiàn)在要攔截所有按鈕的點(diǎn)擊事件,在做出點(diǎn)擊相應(yīng)之前打印出相關(guān)信息,UIButton繼承自UIControl,button的addTarget: action: forControlEvents:方法底層也是調(diào)用了UIControl的sendAction:to:forEvent:方法,所以我們需要將sendAction:to:forEvent:來(lái)進(jìn)行方法交換,交換成我們自己的方法:

#import "UIControl+Extension.h"
#import <objc/runtime.h>

@implementation UIControl (Extension)
+ (void)load
{
    //這里最好加上一個(gè)dispatch_once 雖然load方法原則上只會(huì)調(diào)用一次,但是萬(wàn)一開發(fā)者手動(dòng)再調(diào)用一次的話,那么兩個(gè)方法交換了兩次就相當(dāng)于沒交換
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Method method1 = class_getInstanceMethod(self, @selector(sendAction:to:forEvent:));
        Method method2 = class_getInstanceMethod(self, @selector(my_sendAction:to:forEvent:));
        method_exchangeImplementations(method1, method2);
    });
}
//我們自己實(shí)現(xiàn)的方法
- (void)my_sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event
{
    NSLog(@"%@-%@-%@", self, target, NSStringFromSelector(action));
    // 調(diào)用系統(tǒng)原來(lái)的實(shí)現(xiàn)
    //調(diào)用sendAction:會(huì)出現(xiàn)死循環(huán) 因?yàn)閟endAction:方法的實(shí)現(xiàn)是my_sendAction:
    //[self sendAction:action to:target forEvent:event];
    //所以需要調(diào)用my_sendAction:方法來(lái)實(shí)現(xiàn)系統(tǒng)原來(lái)的實(shí)現(xiàn) 因?yàn)閙y_sendAction:方法實(shí)現(xiàn)就是系統(tǒng)的sendAction:方法實(shí)現(xiàn)
    [self my_sendAction:action to:target forEvent:event];
}

②需要注意類簇,確保交換的是正確的類

比如我們?cè)谑褂肗SMutableArray添加數(shù)據(jù)的時(shí)候,如果添加nil會(huì)出錯(cuò),所以我們要將系統(tǒng)的這個(gè)方法交換成我們自己的方法從而可以進(jìn)行判斷,我們也知道

addObject:方法底層是調(diào)用的insertObject:atIndex:方法,所以:

#import "NSMutableArray+Extension.h"
#import <objc/runtime.h>

@implementation NSMutableArray (Extension)
+ (void)load
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Method method1 = class_getInstanceMethod(self, @selector(insertObject:atIndex:));
        Method method2 = class_getInstanceMethod(self, @selector(my_insertObject:atIndex:));
        method_exchangeImplementations(method1, method2);
    });
}

- (void)my_insertObject:(id)anObject atIndex:(NSUInteger)index
{
    if (anObject == nil) return;
    [self my_insertObject:anObject atIndex:index];
}

但是我們發(fā)現(xiàn),這樣做還是不行,根本進(jìn)不去我們自己的my_insertObject:方法就會(huì)出錯(cuò):

//reason: '*** -[__NSArrayM insertObject:atIndex:]: object cannot be nil'

這是因?yàn)槲覀兘粨Q的類不對(duì),在出錯(cuò)信息我們可以看到這個(gè)insertObject:atIndex:是存在__NSArrayM中的,所以我們應(yīng)該交換__NSArrayM的方法而不是NSMutableArray的方法:

#import "NSMutableArray+Extension.h"
#import <objc/runtime.h>

@implementation NSMutableArray (Extension)
+ (void)load
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        //這里要是交換方法的真實(shí)類
        Class cls = NSClassFromString(@"__NSArrayM");
        Method method1 = class_getInstanceMethod(cls, @selector(insertObject:atIndex:));
        Method method2 = class_getInstanceMethod(cls, @selector(mj_insertObject:atIndex:));
        method_exchangeImplementations(method1, method2);
    });
}

- (void)mj_insertObject:(id)anObject atIndex:(NSUInteger)index
{
    if (anObject == nil) return;
    
    [self mj_insertObject:anObject atIndex:index];
}

幾種常見類的真實(shí)類型:

image

method swizzling(俗稱黑魔法),簡(jiǎn)單說(shuō)就是進(jìn)行方法交換,可以通過(guò)以下幾種方式實(shí)現(xiàn):

利用 method_exchangeImplementations 交換兩個(gè)方法的實(shí)現(xiàn)
利用 class_replaceMethod 替換方法的實(shí)現(xiàn)
利用 method_setImplementation 來(lái)直接設(shè)置某個(gè)方法的IMP

8、weak的底層實(shí)現(xiàn)

weak使我們開發(fā)中常見的修飾符,它是一種“非擁有關(guān)系”的指針(即弱引用)。通過(guò)weak修飾的指針變量,都不會(huì)改變被引用對(duì)象的引用計(jì)數(shù),最主要的作用是為了防止引用循環(huán)(retained cycle),經(jīng)常用于block和delegate。

weak、assign以及unsafe_unretained都是弱引用,這三者有什么區(qū)別嗎?

在一個(gè)對(duì)象被釋放后,weak會(huì)自動(dòng)將指針指向nil,而assign和unsafe_unretained則不會(huì)。在iOS中,向nil發(fā)送消息時(shí)不會(huì)導(dǎo)致崩潰的,所以assign和unsafe_unretained會(huì)導(dǎo)致野指針的錯(cuò)誤unrecognized selector sent to instance。

weak 只可以修飾對(duì)象。如果修飾基本數(shù)據(jù)類型,編譯器會(huì)報(bào)錯(cuò)-“Property with ‘weak’ attribute must be of object type”。
   assign 可修飾對(duì)象,和基本數(shù)據(jù)類型。unsafe_unretained也是只可修飾對(duì)象,所以用assign修飾對(duì)象和unsafe_unretained修飾對(duì)象其實(shí)是一樣的。

weak不論是用作property修飾符還是用來(lái)修飾一個(gè)變量的聲明其作用是一樣的,就是不增加新對(duì)象的引用計(jì)數(shù),被釋放時(shí)也不會(huì)減少新對(duì)象的引用計(jì)數(shù),同時(shí)在新對(duì)象被銷毀時(shí),weak修飾的屬性或變量均會(huì)被設(shè)置為nil,這樣可以防止野指針錯(cuò)誤,那么runtime如何將weak修飾的變量的對(duì)象在銷毀時(shí)自動(dòng)置為nil?weak底層實(shí)現(xiàn)原理是什么?

//用作property修飾符
    @property(weak,nonatomic) NSObject *weakObj;
//修飾變量的聲明
    NSObject *obj = [[NSObject alloc]init];
    __weak typeof(obj)weakObj = obj;    
runtime對(duì)注冊(cè)的類會(huì)進(jìn)行布局,對(duì)于weak修飾的對(duì)象會(huì)放入一個(gè)hash表中。用weak指向的對(duì)象內(nèi)存地址作為key,當(dāng)此對(duì)象的引用計(jì)數(shù)為0的時(shí)候會(huì)dealloc,假如weak指向的對(duì)象內(nèi)存地址是a,那么就會(huì)以a為鍵在這個(gè)weak表中搜索,找到所有以a為鍵的weak對(duì)象,從而設(shè)置為nil。

比如我們上面__weak typeof(obj)weakObj = obj;這個(gè)例子:

當(dāng)為weakObj這一weak類型的對(duì)象賦值時(shí),編譯器會(huì)根據(jù)obj的地址為key去查找weak哈希表,這個(gè)表可以理解成一個(gè)數(shù)組,將weakObj對(duì)象的地址(&weakObj)加入到數(shù)組中。當(dāng)obj引用計(jì)數(shù)為0時(shí),會(huì)執(zhí)行dealloc函數(shù),在執(zhí)行該函數(shù)時(shí),編譯器會(huì)以obj變量的地址去查找weak哈希表的值,并將數(shù)組里所有 weak對(duì)象全部賦值為nil。

也就是,系統(tǒng)會(huì)創(chuàng)建一個(gè)全局的weak表(其實(shí)是一個(gè)hash(哈希)表),Key是所指對(duì)象的地址,Value是weak指針的地址數(shù)組

NSObject *obj = [[NSObject alloc]init];
    __weak typeof(obj)weakObj = obj; 
    __weak typeof(obj)weakTest = obj; 


    PeopleClass *perple = [[PeopleClass alloc]init];
    __weak typeof(perple)weakTeacher = perple; 


    AnimalClass *animal = [[AnimalClass alloc]init];
    __weak typeof(animal)weakCat = animal; 
    __weak typeof(animal)weakDog= animal; 
    __weak typeof(animal)weakPig = animal;
image.png

接下來(lái)我們?cè)谠创a中去查看weak的實(shí)現(xiàn):

property中使用weak修飾

@property (nonatomic,weak) NSObject *referent;

// 底層實(shí)現(xiàn)函數(shù)入口
id objc_storeWeak(id *location, id newObj)
{
    return storeWeak<DoHaveOld, DoHaveNew, DoCrashIfDeallocating>
        (location, (objc_object *)newObj);
}

使用__weak修飾對(duì)象:

__weak NSObject *referent

// 底層實(shí)現(xiàn)函數(shù)入口
id objc_initWeak(id *location, id newObj)
{
    if (!newObj) {
        *location = nil;
        return nil;
    }

    return storeWeak<DontHaveOld, DoHaveNew, DoCrashIfDeallocating>
        (location, (objc_object*)newObj);
}

不論是使用weak還是__weak底層都是調(diào)用storeWeak這個(gè)函數(shù),區(qū)別在于模板的第一個(gè)參數(shù)HaveOld,這個(gè)參數(shù)用來(lái)表示這個(gè)弱指針是否有值。

Runtime維護(hù)了一個(gè)weak表,用于存儲(chǔ)指向某個(gè)對(duì)象的所有weak指針。weak表其實(shí)是一個(gè)hash(哈希)表,Key是所指對(duì)象的地址,Value是weak指針的地址(這個(gè)地址的值是所指對(duì)象的地址)數(shù)組。

weak 的實(shí)現(xiàn)原理可以概括一下三步:

1、初始化時(shí):runtime會(huì)調(diào)用objc_initWeak函數(shù),初始化一個(gè)新的weak指針指向?qū)ο蟮牡刂??!緋roperty中使用weak修飾不存在這一步】

2、添加引用時(shí):objc_initWeak函數(shù)會(huì)調(diào)用 objc_storeWeak() 函數(shù), objc_storeWeak() 的作用是更新指針指向,創(chuàng)建對(duì)應(yīng)的弱引用表。

3、釋放時(shí),調(diào)用clearDeallocating函數(shù)。clearDeallocating函數(shù)首先根據(jù)對(duì)象地址獲取所有weak指針地址的數(shù)組,然后遍歷這個(gè)數(shù)組把其中的數(shù)據(jù)設(shè)為nil,最后把這個(gè)entry從weak表中刪除,最后清理對(duì)象的記錄。

接下來(lái)用偽代碼說(shuō)明一下具體流程

//weak對(duì)象創(chuàng)建
static id storeWeak(id *location, objc_object *newObj){
    //首先,我們可以拿到兩個(gè)值:一個(gè)是弱指針指向的對(duì)象obj,一個(gè)是弱指針對(duì)象weakObj
    if(全局weak表中存在一個(gè)key為&obj的鍵值對(duì)){
        1、取出&obj這個(gè)key對(duì)應(yīng)的value,這個(gè)value是個(gè)數(shù)組array;
        2、將&weakObj插入到這個(gè)數(shù)組中 [weak_entry_insert(weak_table, &new_entry);]
        
    }else{//--如果weak表中沒有一個(gè)key為&obj的鍵值對(duì) 那么說(shuō)明這個(gè)對(duì)象從來(lái)沒有被弱指針對(duì)象指向過(guò)  所以就需要在weak表中創(chuàng)建一個(gè)新的鍵值對(duì)存儲(chǔ)了
        1、創(chuàng)建這樣一個(gè)鍵值對(duì)
        NSArry *value = @[&weakObj];
        NSDictory *dic = @{&obj:value};
        
        2、查看weak剩余存儲(chǔ)空間還多不多
        if(weak表使用空間不足3/4)
            將鍵值對(duì)dic插入到weak表中
        }else{//如果剩余空間不如1/4了  那么就進(jìn)行擴(kuò)容
            /* Grow if at least 3/4 full.
             if (weak_table->num_entries >= old_size * 3 / 4) {
             weak_resize(weak_table, old_size ? old_size*2 : 64);
             }
             */
            將weak表的存儲(chǔ)空間擴(kuò)展其兩倍大;
            將舊表中的數(shù)據(jù)通過(guò)for循環(huán) 全部copy到新表中
            將舊表內(nèi)存空間釋放
            將將鍵值對(duì)dic插入到weak表中
        }
}
//這個(gè)weak表很像我們之前講方法緩存中的那個(gè)cache_t,其實(shí)都一樣,weak表中插入數(shù)據(jù)也不是按照索引去插入的,而是由&obj&mask得到一個(gè)索引,如果這個(gè)索引有數(shù)據(jù)的話那就向下再找空間存儲(chǔ)
size_t begin = hash_pointer(new_entry->referent) & (weak_table->mask);
size_t index = begin;
size_t hash_displacement = 0;
while (weak_entries[index].referent != nil) {
    index = (index+1) & weak_table->mask;
    if (index == begin) bad_weak_table(weak_entries);
        hash_displacement++;
}
//weak的釋放
weak_clear_no_lock(weak_table_t *weak_table, id referent_id){
    1.從weak表中取出這個(gè)以&obj為key的鍵值對(duì)
    2.取出鍵值對(duì)的value 存放弱指針的數(shù)組,weakAry
    for(int i = 0,i<weakAry.count,i++){
        將weakAry中的指針指向的內(nèi)容全部置為nil
    }
    3.從weak表中刪除這個(gè)鍵值對(duì)
    weak_entry_remove(weak_table, entry);
}

文章轉(zhuǎn)自:https://www.cnblogs.com/gaoxiaoniu/p/10801356.html
如有侵權(quán),請(qǐng)聯(lián)系本人刪除

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容