03-OC方法調(diào)用的底層分析

OC底層原理探索文檔匯總

分析對象進(jìn)行方法調(diào)用在底層的執(zhí)行過程州邢,包括快速查找流程儡陨、慢速查找流程、動(dòng)態(tài)方法解析量淌、消息轉(zhuǎn)發(fā)骗村,以及最后查詢失敗的報(bào)錯(cuò)方法。

主要內(nèi)容:

  1. 快速查找流程
  2. 慢速查找流程
  3. 動(dòng)態(tài)方法解析
  4. 消息轉(zhuǎn)發(fā)(消息接收者重定向呀枢,消息重定向)
  5. 查詢失敗提示

1胚股、前期準(zhǔn)備

1.1 Runtime的簡單認(rèn)識(shí)

Runtime和方法調(diào)用的基本認(rèn)識(shí)可以看我的另一篇博客Runtime官方指導(dǎo)文檔閱讀

1.1.1 Runtime是什么

OC具有面向?qū)ο蟮奶匦院蛣?dòng)態(tài)機(jī)制裙秋。OC把一些決定性的工作從編譯時(shí)琅拌、鏈接時(shí)推遲到運(yùn)行時(shí)缨伊。而Runtime給我們提供了這個(gè)運(yùn)行時(shí),我們可以用runtime在運(yùn)行時(shí)動(dòng)態(tài)的創(chuàng)建修改類财忽、對象、方法泣侮、屬性即彪、協(xié)議等操作

編譯時(shí)是源代碼翻譯成機(jī)器能識(shí)別的代碼的過程,不同于編譯時(shí)活尊,運(yùn)行時(shí)是代碼跑起來隶校,被裝載到內(nèi)存中的過程,這是一個(gè)動(dòng)態(tài)的過程蛹锰。

1.1.2 Runtime的作用是什么

  • 為面向?qū)ο筇峁┻\(yùn)行時(shí)環(huán)境
  • 進(jìn)行內(nèi)存布局和底層執(zhí)行邏輯

1.2 isa深胳、cache、方法列表的簡單認(rèn)識(shí)

方法查找的過程涉及到了isa铜犬、cache舞终、方法列表的認(rèn)識(shí),這需要了解對象的底層和類的底層癣猾,詳情可以看我的另一篇博客OC類的底層分析

簡單認(rèn)識(shí):

isa是對象中的一個(gè)屬性敛劝,它包含有類信息,因此我們可以通過對象的isa來獲取到類纷宇,再通過類獲取類中的數(shù)據(jù)夸盟,比如方法、屬性像捶、協(xié)議上陕、成員變量。

cache是類中的一個(gè)成員拓春,它包含有sel和imp释簿,當(dāng)我們想要通過sel查詢imp時(shí),會(huì)先來到該類的cache中查找硼莽。

方法列表在類的rw中辕万,當(dāng)我們想要通過sel查詢imp時(shí),就需要在方法列表中查詢沉删。

1.3 如何探索呢渐尿?

之前在看博客時(shí),很多博客對于一些知識(shí)的說明都是憑空而來矾瑰,比如objc_msgSend的實(shí)現(xiàn)先進(jìn)行cache查找后進(jìn)行方法列表的查找砖茸,方法查找結(jié)束后會(huì)進(jìn)行動(dòng)態(tài)方法解析只說是這樣的,但是為什么是這樣的并沒有說殴穴。

\color{red}{ 因此我這里對于方法調(diào)用的分析每一步都是有跡可循的凉夯,重要的是探索货葬,而不是知識(shí)點(diǎn)記憶。 }

  1. 先從方法上層調(diào)用入手劲够,通過Clang查看底層是如何實(shí)現(xiàn)的震桶。經(jīng)查看發(fā)現(xiàn)是objc_msgSend來實(shí)現(xiàn)的
  2. 通過objc_msgSend在源碼中查看啊鸭,在匯編中找到了objc_msgSend的實(shí)現(xiàn)過程岳掐,探索匯編發(fā)現(xiàn)是進(jìn)行cache的查找杜跷。
  3. cache查找結(jié)束后會(huì)進(jìn)入到一個(gè)lookUpImpOrForward方法样屠,之后在源碼C語言中查找到lookUpImpOrForward的實(shí)現(xiàn)汪拥,探索發(fā)現(xiàn)其內(nèi)部是在進(jìn)行方法列表的查找鸭蛙。
  4. 方法的查找后如果沒有查找到茴扁,發(fā)現(xiàn)會(huì)進(jìn)入到一個(gè)resolveMethod_locked方法翩概,由此開啟動(dòng)態(tài)方法解析的過程凫岖。
  5. 方法查找失敗后江咳,會(huì)得到一個(gè)報(bào)錯(cuò)函數(shù)_objc_msgForward_impcache,繼續(xù)探索發(fā)現(xiàn)就是我們常見的方法調(diào)用失敗后的報(bào)錯(cuò)信息哥放。
  6. 在動(dòng)態(tài)方法解析后會(huì)再次進(jìn)行方法列表的查找歼指,下一步找不到了消息轉(zhuǎn)發(fā),因此通過instrumentObjcMessageSends方式打印發(fā)送消息的日志和通過hopper/IDA反編譯兩種方式探索消息轉(zhuǎn)發(fā)的過程甥雕。

2东臀、方法的本質(zhì)

既然要探索方法的調(diào)用,首先需要知道方法是什么犀农。這里使用Clang就可以清楚的看到方法調(diào)用在底層其實(shí)是通過objc_msgSend進(jìn)行消息發(fā)送惰赋。

\color{red}{ 一句話:方法的本質(zhì)就是消息發(fā)送 }

發(fā)送消息就是:給一個(gè)接受者對象發(fā)送一個(gè)消息,告訴接受者對象我們要執(zhí)行哪個(gè)函數(shù)呵哨。

消息函數(shù)有多種赁濒,我們只分析常見的兩種objc_msgSend和objc_msgSendSuper。

2.1 objc_msgSend的認(rèn)識(shí)

2.1.1 底層結(jié)構(gòu)

源碼:

OBJC_EXPORT void
objc_msgSend(void /* id self, SEL op, ... */ )
    OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);

說明:

  • self表示當(dāng)前對象孟害,通過對象獲取所在類的方法列表
  • op是方法選擇器拒炎,通過選擇器sel來查找imp
  • self和op是必須有的隱藏參數(shù),如果方法有其他參數(shù)挨务,還會(huì)有其他參數(shù)击你。

2.1.2 驗(yàn)證

代碼:

#import "WYPerson.h"
#import "WYCat.h"
#import "objc/runtime.h"
#import "objc/message.h"

@implementation WYPerson

- (void)msgSendTest:(BOOL)abc{
    NSLog(@"測試objc_msgSend");
}

注意:

  • 想要調(diào)用objc_msgSend,必須導(dǎo)入頭文件#import "objc/message.h"
  • 需要將target --> Build Setting -->搜索msg -- 將enable strict checking of calls由YES 改為NO谎柄,將嚴(yán)厲的檢查機(jī)制關(guān)掉丁侄,否則objc_msgSend的參數(shù)會(huì)報(bào)錯(cuò)

方法調(diào)用

//方法調(diào)用
WYPerson *person = [WYPerson alloc];
objc_msgSend(person,sel_registerName("msgSendTest:"),YES);

//結(jié)果:
2021-10-15 19:16:19.021944+0800 消息發(fā)送[2866:47753] 測試objc_msgSend

2.2 objc_msgSendSuper的認(rèn)識(shí)

2.2.1 底層結(jié)構(gòu)

源碼:

//結(jié)構(gòu)體    
struct objc_super {
    /// Specifies an instance of a class.類的實(shí)例
    __unsafe_unretained _Nonnull id receiver;//接受者

    /// Specifies the particular superclass of the instance to message. 
#if !defined(__cplusplus)  &&  !__OBJC2__
    /* For compatibility with old objc-runtime.h header */
    __unsafe_unretained _Nonnull Class class;
#else
    __unsafe_unretained _Nonnull Class super_class;//父類
#endif
    /* super_class is the first class to search */
}; 

//函數(shù)
OBJC_EXPORT void
objc_msgSendSuper(void /* struct objc_super *super, SEL op, ... */ )
    OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);  

說明:

  • super是一個(gè)objc_super結(jié)構(gòu)體
  • op是方法選擇器
  • objc_super結(jié)構(gòu)體中包含類的接受者和父類

2.2.2 使用驗(yàn)證

代碼:

父類WYPerson
實(shí)現(xiàn)msgSendSuperTest方法

@interface WYPerson : NSObject
- (void) msgSendSuperTest;
@end

@implementation WYPerson

- (void) msgSendSuperTest {
    NSLog(@"%s: 測試objc_msgSendSuper",__func__);
}
@end

子類WYStudent
沒有實(shí)現(xiàn)msgSendSuperTest方法

@interface WYStudent : NSObject

- (void) msgSendSuperTest;
@end

#import "WYStudent.h"

@implementation WYStudent

@end

main函數(shù)調(diào)用

WYPerson *person = [WYPerson alloc];
WYStudent *student = [WYStudent alloc];
    
struct objc_super wySuper;
wySuper.receiver = person;
wySuper.super_class = [WYStudent class];
    
objc_msgSendSuper(&wySuper, sel_registerName("msgSendSuperTest"));

結(jié)果:

2021-10-15 20:14:45.809788+0800 消息發(fā)送[4750:91856] -[WYPerson msgSendSuperTest]: 測試objc_msgSendSuper
2021-10-15 20:14:45.809861+0800 消息發(fā)送[4750:91856] -[WYPerson msgSendSuperTest]: 測試objc_msgSendSuper

說明:

  • 當(dāng)通過子類調(diào)用父類的方法時(shí)在底層會(huì)使用objc_msgSendSuper來獲取父類的方法
  • 只是我們在上層沒有感知
  • 在objc_super結(jié)構(gòu)體中設(shè)置接收者為當(dāng)前對象,再設(shè)置它的父類朝巫。注意此時(shí)接受者仍然是當(dāng)前對象鸿摇,而不是父類

2.2.3 案例分析

WYStudent繼承自WYPerson,WYStudent創(chuàng)建一個(gè)方法objc_msgSendSuperTest劈猿,大家覺得調(diào)用這個(gè)方法會(huì)打印什么呢拙吉?

- (void)objc_msgSendSuperTest{
    NSLog(@"父類:%@---子類:%@",[super class],[self class]);
}

分析:
均打印WYStudent潮孽,這是因?yàn)檎{(diào)用父類方法,objc_super結(jié)構(gòu)體中接收者為當(dāng)前對象筷黔,并不是父類對象往史。所以打印接受者的類就是WYStudent。self是隱藏參數(shù)佛舱,表示當(dāng)前對象椎例,所以當(dāng)前對象調(diào)用class方法,返回的對象的類WYStudent名眉。

驗(yàn)證結(jié)果:

2021-10-15 20:29:42.676387+0800 消息發(fā)送[5275:104688] 父類:WYStudent---子類:WYStudent

2.3 小結(jié)

  • 方法的本質(zhì)就是消息發(fā)送
  • 使用objc_msgSend發(fā)送當(dāng)前類的消息
  • 使用objc_msgSendSuper發(fā)送父類的消息

3粟矿、快速查找

快速查找流程是從當(dāng)前類的cache中查找imp凰棉。cache中存儲(chǔ)有sel和imp损拢,可以快速的通過sel查找到相應(yīng)的imp。

3.1 查找源碼

從上文我們知道方法調(diào)用其實(shí)底層是在執(zhí)行objc_msgSend函數(shù)撒犀,所以從objc_msgSend函數(shù)開始入手查找福压。

  • 在objc源碼中全局搜索objc_msgSend,找了一圈發(fā)現(xiàn)沒有找到這個(gè)函數(shù)實(shí)現(xiàn)或舞,這是因?yàn)閏ache的查找是通過匯編完成的
  • 所以需要全局搜索_objc_msgSend荆姆,找到了入口


    _objc_msgSend入口.png

3.2 匯編源碼分析

3.2.1 整體概括

可能有人看匯編實(shí)現(xiàn)會(huì)犯怵,為了方便大家映凳,我這里先對整體的過程進(jìn)行說明胆筒,之后再看匯編如何實(shí)現(xiàn)

在這里涉及到cache的結(jié)構(gòu),務(wù)必先熟悉OC類的底層分析中的cache部分诈豌。

下面這幅圖囊括了cache查詢流程仆救、重要的匯編實(shí)現(xiàn)、寄存器的變化過程矫渔,后面有看的不好理解的地方可以對照這幅圖來看彤蔽。


07-cache的方法查找流程.png
  1. 開始查詢,得到Class
    1. 通過self的isa獲取到該對象的Class
  2. 得到buckets
    1. 通過Class獲取到類中的cache
    2. 得到cache中的buckets和mask
  3. 哈希算法查找
    1. 通過mask和方法選擇_cmd進(jìn)行哈希計(jì)算得到bucket所在的地址值
    2. 取出bucket中的imp
  4. 哈希沖突算法查找
    1. 如果發(fā)生沖突庙洼,開始哈希沖突算法
    2. 先以當(dāng)前的bucket不斷向前查找顿痪,判斷是否是我們需要的bucket
    3. 如果一直找到第一個(gè)bucket,仍然沒有找到油够,則跳轉(zhuǎn)到最后一個(gè)bucket蚁袭,繼續(xù)向前查找
    4. 一直查找到第一個(gè)元素仍然沒有找到,就開始慢速查找

3.2.2 開始查詢石咬,得到Class

源碼:

//此處開始進(jìn)入到msgSend流程
 ENTRY _objc_msgSend//以后看到ENTRY就說明了一個(gè)流程的開始
 UNWIND _objc_msgSend, NoFrame//沒有視圖

//檢測是否為空和是否是小對象
//cmp表示比較撕阎。
//p0表示第1個(gè)寄存器的位置的相當(dāng)于變量的東西,此處存儲(chǔ)的是消息接受者碌补。這還是因?yàn)楫?dāng)傳入的時(shí)候虏束,第一個(gè)參數(shù)就是消息接受者棉饶。
//第二個(gè)參數(shù)是方法選擇器_cmd,所以p1肯定就是_cmd
//如果函數(shù)要返回值的時(shí)候,第一個(gè)寄存器存放的是返回值镇匀。
 cmp p0, #0   // nil check and tagged pointer check
//如果支持小對象類型照藻。要么返回小對象或?yàn)榭眨?//b是進(jìn)行跳轉(zhuǎn)
//b.le是小于判斷,也就是小于的時(shí)候LNilOrTagged
#if SUPPORT_TAGGED_POINTERS
 b.le LNilOrTagged  //  (MSB tagged pointer looks negative)
//b.eq是等于的是執(zhí)行
#else
 b.eq LReturnZero //如果不支持小對象汗侵,則直接返回空
#endif
//這里是肯定存在的流程幸缕。
//ldr是存放一個(gè)值到一個(gè)變量中,
//剛才所看到的p1是消息接受者晰韵,x0就是這個(gè)p1寄存器所存儲(chǔ)的內(nèi)容发乔,類的第一個(gè)屬性就是isa,所以直接就是將isaf保存到了p13中
 ldr p13, [x0]     // p13 = isa
//此處是相當(dāng)于方法調(diào)用雪猪,在匯編中是宏定義
//它的作用是取出isa中保存的Class 信息并保存到p16寄存器中
//也就是p16 = isa(p13) & ISA_MASK
 GetClassFromIsa_p16 p13  // p16 = class
//LGetIsaDone是一個(gè)入口
LGetIsaDone:
 // calls imp or objc_msgSend_uncached
    //接下來就是進(jìn)入到緩存查找或者沒有緩存查找方法的流程
    //這里傳入的參數(shù)是NORMAL
 CacheLookup NORMAL, _objc_msgSend

#if SUPPORT_TAGGED_POINTERS
LNilOrTagged:
 b.eq LReturnZero  // nil check判空處理栏尚,直接退出

 // tagged
 adrp x10, _objc_debug_taggedpointer_classes@PAGE
 add x10, x10, _objc_debug_taggedpointer_classes@PAGEOFF
 ubfx x11, x0, #60, #4
 ldr x16, [x10, x11, LSL #3]
 adrp x10, _OBJC_CLASS_$___NSUnrecognizedTaggedPointer@PAGE
 add x10, x10, _OBJC_CLASS_$___NSUnrecognizedTaggedPointer@PAGEOFF
 cmp x10, x16
 b.ne LGetIsaDone

 // ext tagged
 adrp x10, _objc_debug_taggedpointer_ext_classes@PAGE
 add x10, x10, _objc_debug_taggedpointer_ext_classes@PAGEOFF
 ubfx x11, x0, #52, #8
 ldr x16, [x10, x11, LSL #3]
 b LGetIsaDone//前往z查找IMP
// SUPPORT_TAGGED_POINTERS
#endif

匯編語句解讀:

  1. ENTRY _objc_msgSend
    • 此處開始進(jìn)入到msgSend流程
    • 以后看到ENTRY就說明了一個(gè)流程的開始
  2. UNWIND _objc_msgSend, NoFrame
    • 表示沒有視圖
  3. cmp p0, #0
    • 判斷p0的值是否為空
    • cmp表示比較
    • p0是self,此處存儲(chǔ)的是消息接受者。這是因?yàn)楫?dāng)傳入的時(shí)候只恨,第一個(gè)參數(shù)就是消息接受者
    • 當(dāng)函數(shù)返回時(shí)译仗,p0是返回值
  4. if SUPPORT_TAGGED_POINTERS
  • 是否支持小對象類型
  1. b.le LNilOrTagged
    • b表示進(jìn)行跳轉(zhuǎn)
    • b.le是小于判斷,也就是小于的時(shí)候開始執(zhí)行LNilOrTagged
  2. b.eq LReturnZero
    • b.eq判斷是等于的是執(zhí)行
    • 如果為0官觅,則直接退出
  3. ldr p13, [x0]
    • 將isa保存到了p13中
    • 剛才所看到的p0是消息接受者纵菌,x0就是這個(gè)p1寄存器所存儲(chǔ)的內(nèi)容,類的第一個(gè)屬性就是isa
    • ldr表示將x0值傳遞到p13上
  4. GetClassFromIsa_p16 p13
    1. 將Class信息保存到p16
    2. 它的作用是取出isa中保存的Class 信息并保存到p16寄存器中
    3. 也就是p16 = isa(p13) & ISA_MASK
    4. 此處是相當(dāng)于函數(shù)調(diào)用休涤,在匯編中是一個(gè)宏定義
  5. CacheLookup NORMAL, _objc_msgSend
    1. 開始緩存查找流程

代碼邏輯:

1咱圆、先拿到傳入的消息接受者,判斷是否為空
2功氨、不為空則判斷是否為小對象類型序苏,如果是小對象類型則執(zhí)行其他操作
3、獲取到isa存儲(chǔ)到p13,再獲取到isa中的類信息存儲(chǔ)到p16
4疑故、開始進(jìn)行緩存查找

3.2.3 緩存查找流程

源碼:

/*
 此處就是在cache中通過sel查找imp的核心流程
*/
.macro CacheLookup
    //
    // Restart protocol:
    //
    //   As soon as we're past the LLookupStart$1 label we may have loaded
    //   an invalid cache pointer or mask.
    //
    //   When task_restartable_ranges_synchronize() is called,
    //   (or when a signal hits us) before we're past LLookupEnd$1,
    //   then our PC will be reset to LLookupRecover$1 which forcefully
    //   jumps to the cache-miss codepath which have the following
    //   requirements:
    //
    //   GETIMP:
    //     The cache-miss is just returning NULL (setting x0 to 0)
    //
    //   NORMAL and LOOKUP:
    //   - x0 contains the receiver
    //   - x1 contains the selector
    //   - x16 contains the isa
    //   - other registers are set as per calling conventions
    //
LLookupStart$1:

/*
     ldr表示將一個(gè)值存入到p11寄存器中
     x16表示p16寄存器存儲(chǔ)的值杠览,當(dāng)前是Class
     #數(shù)值表示一個(gè)值,這里的CACHE經(jīng)過全局搜索發(fā)現(xiàn)是2倍的指針地址纵势,也就是16個(gè)字節(jié)
     #define CACHE (2 * __SIZEOF_POINTER__)
     經(jīng)計(jì)算踱阿,p11就是cache
     */
    // p1 = SEL, p16 = isa
    ldr p11, [x16, #CACHE]  //偏移16個(gè)字節(jié),也就是取到cache_t          // p11 = mask|buckets

//真機(jī)64位看這個(gè)
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
/*
     and表示與運(yùn)算钦铁,將與后的值保存到p10寄存器
     p10為buckets
     */
    and p10, p11, #0x0000ffffffffffff   // p10 = buckets软舌,后48位為buckets
/*
     LSR表示邏輯向右偏移
     p11, LSR #48表示cache偏移48位,拿到前16位牛曹,也就是得到mask
     and p12,p1,p11,LSR #48表示_cmd &mask并保存到p12
     x12 = _cmd & mask
     這個(gè)是哈希算法佛点,p12存儲(chǔ)的就是搜索下標(biāo)(哈希地址)
     */
    and p12, p1, p11, LSR #48       // x12 = _cmd & mask,存儲(chǔ)的是哈希地址
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
    and p10, p11, #~0xf         // p10 = buckets
    and p11, p11, #0xf          // p11 = maskShift
    mov p12, #0xffff
    lsr p11, p12, p11               // p11 = mask = 0xffff >> p11
    and p12, p1, p11                // x12 = _cmd & mask
#else
#error Unsupported cache mask storage for ARM64.
#endif

/*
 得到計(jì)算出的bucket并存放到p12中
 PTRSHIFT經(jīng)全局搜索發(fā)現(xiàn)是3,
 LSL #(1+PTRSHIFT)表示邏輯左移4位超营,也就是*16
 通過bucket的首地址進(jìn)行左平移下標(biāo)的16倍數(shù)并與p12相與得到bucket鸳玩,并存入到p12中,
 
 哈希算法_cmd & mask
 這里是通過內(nèi)存平移到達(dá)我們計(jì)算出的bucket
 想要獲取buckets中某個(gè)下標(biāo)的bucket,就需要進(jìn)行內(nèi)存平移
 而每個(gè)bucket結(jié)構(gòu)體包含的是sel和imp演闭,因此包含了16位不跟,所以需要向左平移16位
 下標(biāo)*16,就是buckets內(nèi)存平移的大小得到查詢的地址
 */
    add p12, p10, p12, LSL #(1+PTRSHIFT)
                     // p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT))

//分別將imp和sel存入到p17和p9
/*
     ldp表示出棧米碰,取出bucket中的imp和sel分別存放到p17和p9
     */
    ldp p17, p9, [x12]      // {imp, sel} = *bucket

//上面都是獲取到sel和imp窝革,下面進(jìn)行比較
//哈希沖突算法
/*
 sel與傳入的_cmd判斷是否相同,相同就拿到這個(gè)sel對應(yīng)的imp,不同則繼續(xù)查找
 cmp表示比較
 b.ne表示如果不相同則跳轉(zhuǎn)到2f
 如果相同則調(diào)用CacheHit吕座,查找imp
 */
1:  cmp p9, p1          // if (bucket->sel != _cmd)
    b.ne    2f          //     scan more
    CacheHit $0         // call or return imp
/*
 判斷獲取的是否是第一個(gè)元素虐译,
 第一個(gè)元素跳轉(zhuǎn)到最后一個(gè)元素,如果不是吴趴,則向前查找
 */
/*
 循環(huán)遍歷查找sel
 通過p12和p10來判斷是否是第一個(gè)bucket
 如果是第一個(gè)漆诽,則進(jìn)入到3f
 如果不是,則獲取到前一個(gè)bucket的sel繼續(xù)執(zhí)行第一個(gè)判斷
 x12是寄存器p12的地址史侣,減去一個(gè)bucket的大小就等于了前一個(gè)bucket的地址
 這里是通過反向查找拴泌,所以需要--
 通過ldp拿到這個(gè)地址的sel和imp
 */

2:  // not hit: p12 = not-hit bucket
    CheckMiss $0            // miss if bucket->sel == 0
    cmp p12, p10        // wrap if bucket == buckets 如果是第一個(gè)元素
    b.eq    3f          //拿到最后一個(gè)元素比較
    ldp p17, p9, [x12, #-BUCKET_SIZE]!  // {imp, sel} = *--bucket 魏身,否則向前查找
    b   1b          // loop

3:  // wrap: p12 = first bucket, w11 = mask
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
/*
 直接拿到最后一個(gè)元素惊橱,因?yàn)镸ask = capacity-1
 拿到最后一個(gè)元素后進(jìn)行第二次遞歸查找
 */
    add p12, p12, p11, LSR #(48 - (1+PTRSHIFT))
                    // p12 = buckets + (mask << 1+PTRSHIFT)  拿到最后一個(gè)元素賦給p12
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
    add p12, p12, p11, LSL #(1+PTRSHIFT)
                    // p12 = buckets + (mask << 1+PTRSHIFT)
#else
#error Unsupported cache mask storage for ARM64.
#endif

    // Clone scanning loop to miss instead of hang when cache is corrupt.
    // The slow path may detect any corruption and halt later.

//當(dāng)跳轉(zhuǎn)到最后一個(gè)元素時(shí)的第二輪遍歷
    ldp p17, p9, [x12]      // {imp, sel} = *bucket  //再重新獲取imp和sel
1:  cmp p9, p1          // if (bucket->sel != _cmd)
    b.ne    2f          //     scan more
    CacheHit $0         // call or return imp
    
2:  // not hit: p12 = not-hit bucket
    CheckMiss $0            // miss if bucket->sel == 0  //如果
    cmp p12, p10        // wrap if bucket == buckets//如果查找到第一個(gè)仍然沒有查找到,就退出循環(huán)箭昵,走查詢失敗流程
    b.eq    3f
    ldp p17, p9, [x12, #-BUCKET_SIZE]!  // {imp, sel} = *--bucket
    b   1b          // loop

LLookupEnd$1:
LLookupRecover$1:
3:  // double wrap
    JumpMiss $0

.endmacro

匯編語句解讀:

  1. .macro CacheLookup
    1. 此處表示CacheLookup的定義
    2. 以后方法的定義可以通過.macro來查找
  2. ldr p11, [x16, #CACHE]
    1. 將Class信息偏移16個(gè)字節(jié)税朴,就獲取到了cache
    2. 將緩存信息cache存入到p11寄存器中,p11 = mask|buckets家制,p11就是cache
    3. ldr表示將一個(gè)值存入到p11寄存器中
    4. x16表示p16寄存器存儲(chǔ)的值正林,當(dāng)前是Class
    5. “#數(shù)值”表示一個(gè)值,這里的CACHE經(jīng)過全局搜索發(fā)現(xiàn)是2倍的指針地址颤殴,也就是16個(gè)字節(jié)觅廓。#define CACHE (2 * SIZEOF_POINTER )
  3. and p10, p11, #0x0000ffffffffffff
    1. 得到buckets,存儲(chǔ)到p10
    2. p10 = mask|buckets & #0x0000ffffffffffff
    3. buckets占有后48位(真正數(shù)據(jù)是44位)
    4. and表示與運(yùn)算,將與后的值保存到p10寄存器
  4. and p12, p1, p11, LSR #48
    1. p12是哈希地址涵但,通過哈希算法得到杈绸,x12 = _cmd & mask
    2. LSR表示邏輯向右偏移,p11, LSR #48表示maskAndBuckets向左平移48位矮瘟,也就是得到mask
    3. and p12,p1,p11,LSR #48表示_cmd &mask并保存到p12
  5. add p12, p10, p12, LSL #(1+PTRSHIFT)
    1. 得到計(jì)算出的bucket并存放到p12中
    2. PTRSHIFT經(jīng)全局搜索發(fā)現(xiàn)是3瞳脓,LSL #(1+PTRSHIFT)表示邏輯左移4位,也就是*16
    3. p10此時(shí)是第一個(gè)bucket的地址澈侠,需要通過地址平移來得到下標(biāo)所在的bucket劫侧。
    4. 而平移的地址大小就是坐標(biāo)*16(因?yàn)閎ucket包含sel和imp,所以是16個(gè)字節(jié),所以需要乘以16)
    5. 所以此處是將首地址移動(dòng)下標(biāo)*16的位數(shù)就得到了計(jì)算出的bucket烧栋,并且放到p12中
  6. ldp p17, p9, [x12]
    1. 將imp和sel分別存儲(chǔ)到p17和p9
  7. cmp p9, p1
    1. 比較bucket中的sel與傳入的_cmd是否相等
  8. cmp p12, p10
    1. 比較bucket與buckets第一個(gè)bucket是否一致写妥,也就是是否是第一個(gè)bucket
  9. ldp p17, p9, [x12, #-BUCKET_SIZE]!
    1. 將bucket的地址向前平移一個(gè)bucket的大小
    2. 也就是得到前一個(gè)bucket,并且繼續(xù)將sel和imp存儲(chǔ)到p17和p9中
    3. BUCKET_SIZE經(jīng)全局搜索發(fā)現(xiàn)是一個(gè)bucket的大小
  10. add p12, p12, p11, LSR #(48 - (1+PTRSHIFT))
    1. 如果判斷是第一個(gè)bucket就挪到最后一個(gè)位置
    2. 將cache向右平移44位审姓,也就是mask向左平移4位耳标,也就是mask*16
    3. 移動(dòng)mask個(gè)bucket,所以是移動(dòng)到了最后一位邑跪,這是因?yàn)閙ask=容量-1,所以mask就是最后一個(gè)位置次坡,再乘上16,就是最后一個(gè)位置到第一個(gè)位置的地址大小了
    4. 經(jīng)查詢PTRSHIFT為4
    5. 這里也就是說明了為什么要把maskAndBuckets中間留下4位的0画畅,就是為了在此處更好的計(jì)算

代碼邏輯:

  1. 將Class從首地址內(nèi)存平移16位得到cache砸琅,并存入到p11中
  2. 將cache進(jìn)行掩碼計(jì)算得到buckets,并存入到p10中
  3. 將maskAndBuckets右移48位得到mask與上_cmd得到哈希地址,并存入到p12中(哈希算法)
  4. 通過buckets平移到這個(gè)下標(biāo)地址得到目標(biāo)bucket轴踱,并存入到p12中
  5. 將目標(biāo)bucket的imp和sel分別存入到p17和p9中
  6. 判斷當(dāng)前的bucket->sel 症脂!= _cmd
    1. 如果查找到,則獲取到imp返回
    2. 如果查找不到淫僻,就開始第一輪循環(huán)遍歷(哈希沖突算法)
  7. 判斷bucket是否是第一個(gè)元素诱篷,如果不是,則向前移動(dòng)一位雳灵,再進(jìn)行比較
  8. 如果是第一個(gè)元素棕所,則跳轉(zhuǎn)到最后一個(gè)元素,開啟第二輪循環(huán)循環(huán)向前移動(dòng)一位進(jìn)行比較悯辙。
  9. 如果第二輪循環(huán)直到查找到第一個(gè)元素仍然沒有找到琳省,說明該方法確實(shí)不存在cache中,就到類的方法列表中查找躲撰。

3.2.4 開始獲取imp

如果查找到了针贬,就開始執(zhí)行CacheHit。
源碼:

.macro CacheHit
.if $0 == NORMAL
//驗(yàn)證并得到imp
 TailCallCachedImp x17, x12, x1, x16 // authenticate and call imp
.elseif $0 == GETIMP
 mov p0, p17
 cbz p0, 9f   // don't ptrauth a nil imp
 AuthAndResignAsIMP x0, x12, x1, x16 // authenticate imp and re-sign as IMP
9: ret    // return IMP
.elseif $0 == LOOKUP
 // No nil check for ptrauth: the caller would crash anyway when they
 // jump to a nil IMP. We don't care if that jump also fails ptrauth.
 AuthAndResignAsIMP x17, x12, x1, x16 // authenticate imp and re-sign as IMP
 ret    // return imp via x17
.else
.abort oops
.endif
.endmacro

說明:
開始獲取IMP拢蛋,會(huì)執(zhí)行TailCallCachedImp x17, x12, x1, x16桦他。

3.2.5 開始去查找方法列表

3.2.5.1 CheckMiss

源碼:

.macro CheckMiss
 // miss if bucket->sel == 0
.if $0 == GETIMP
 cbz p9, LGetImpMiss
.elseif $0 == NORMAL
 cbz p9, __objc_msgSend_uncached //進(jìn)入到?jīng)]有緩存的慢速查找
.elseif $0 == LOOKUP
 cbz p9, __objc_msgLookup_uncached
.else
.abort oops
.endif
.endmacro

說明: 如果沒有找到,則開始進(jìn)入慢速查找谆棱,進(jìn)入到__objc_msgSend_uncached

3.2.5.2 __objc_msgSend_uncached

源碼:

STATIC_ENTRY __objc_msgSend_uncached
 UNWIND __objc_msgSend_uncached, FrameWithNoSaves

 // THIS IS NOT A CALLABLE C FUNCTION
 // Out-of-band p16 is the class to search
 
 MethodTableLookup
 TailCallFunctionPointer x17

 END_ENTRY __objc_msgSend_uncached

說明: 這里可以看到跳轉(zhuǎn)到了__objc_msgSend_uncached快压,在這里最終會(huì)執(zhí)行MethodTableLookup來查詢方法列表

3.2.5.3 MethodTableLookup

源碼:

.macro MethodTableLookup
 
 // push frame
 SignLR
 stp fp, lr, [sp, #-16]!
 mov fp, sp

 // save parameter registers: x0..x8, q0..q7
 sub sp, sp, #(10*8 + 8*16)
 stp q0, q1, [sp, #(0*16)]
 stp q2, q3, [sp, #(2*16)]
 stp q4, q5, [sp, #(4*16)]
 stp q6, q7, [sp, #(6*16)]
 stp x0, x1, [sp, #(8*16+0*8)]
 stp x2, x3, [sp, #(8*16+2*8)]
 stp x4, x5, [sp, #(8*16+4*8)]
 stp x6, x7, [sp, #(8*16+6*8)]
 str x8,     [sp, #(8*16+8*8)]

 // lookUpImpOrForward(obj, sel, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER)
 // receiver and selector already in x0 and x1
 mov x2, x16
 mov x3, #3
 bl _lookUpImpOrForward

 // IMP in x0
 mov x17, x0
 
 // restore registers and return
 ldp q0, q1, [sp, #(0*16)]
 ldp q2, q3, [sp, #(2*16)]
 ldp q4, q5, [sp, #(4*16)]
 ldp q6, q7, [sp, #(6*16)]
 ldp x0, x1, [sp, #(8*16+0*8)]
 ldp x2, x3, [sp, #(8*16+2*8)]
 ldp x4, x5, [sp, #(8*16+4*8)]
 ldp x6, x7, [sp, #(8*16+6*8)]
 ldr x8,     [sp, #(8*16+8*8)]

 mov sp, fp
 ldp fp, lr, [sp], #16
 AuthenticateLR

.endmacro

說明:

  • 其他的不用看,可以看到最終會(huì)跳轉(zhuǎn)到_lookUpImpOrForward础锐,
  • 而通過注釋可以看到傳入的參數(shù)為(obj,sel,cls,LOOKUP_INITIALIZE | LOOKUP_RESOLVER)嗓节,也就是說behavior為0011
  • \color{red}{_lookUpImpOrForward這個(gè)方法很重要,接下來慢速查找就從它開始皆警。}

3.3 總結(jié)分析

代碼執(zhí)行流程:


07-cache的方法查找流程.png

寄存器的存儲(chǔ)示意圖:


寄存器存儲(chǔ)流程.png

3.4 代碼邏輯難點(diǎn)解析

3.4.1 為什么有兩次循環(huán)遍歷拦宣?

  • 第一次循環(huán)是從當(dāng)前bucket向前查找循環(huán),第二種是從最后一個(gè)bucket向前循環(huán)。

  • 這是cache存儲(chǔ)的哈希沖突算法決定的鸵隧,哈希沖突算法就是先向前查詢绸罗,如果查到了第一個(gè)就從最后一個(gè)再繼續(xù)查詢,一直查到剛才的位置

  • 哈希沖突算法:

static inline mask_t cache_next(mask_t i, mask_t mask) {
    return i ? i-1 : mask;//判斷如果i存在豆瘫,將下標(biāo)-1珊蟀,也就是向前一位存儲(chǔ),如果為0外驱,也就是計(jì)算到第一個(gè)位置育灸,就直接放到mask,也就是最后一個(gè)位置
}

3.4.2 如何人為的將當(dāng)前bucket設(shè)置為buckets的最后一個(gè)元素昵宇?

  • 通過buckets首地址+mask右移44位(等同于左移4位)直接定位到bucker的最后一個(gè)元素

  • mask = bucket總數(shù)-1磅崭,因此偏移mask的16字節(jié),就跳轉(zhuǎn)到最后一個(gè)bucket


    maskAndBuckets結(jié)構(gòu)圖.png
  • 這里的后4位就是0瓦哎,因?yàn)槠鋵?shí)buckets存儲(chǔ)的是后44位砸喻,中間這4位就是為現(xiàn)在使用

  • mask加上后四位,就是平移到最后一個(gè)bucket的地址大小

3.4.3 如何查找相應(yīng)的bucket

通過哈希算法蒋譬,_cmd與mask

哈希算法

static inline mask_t cache_hash(SEL sel, mask_t mask) 
{
    return (mask_t)(uintptr_t)sel & mask;
}

3.4.4 為什么在通過哈希算法計(jì)算了哈希地址后割岛,還要循環(huán)遍歷,而不是直接判斷就結(jié)束了

因?yàn)椴迦氲臅r(shí)候遇到哈希沖突會(huì)進(jìn)行哈希沖突算法犯助,所以在查詢的時(shí)候?yàn)榱祟A(yù)防哈希沖突而存入其他的位置癣漆,所以就再需要循環(huán)向前查找。

3.5 關(guān)于匯編的幾個(gè)小問題

1也切、為什么objc_msgSend底層使用匯編

  • 匯編實(shí)現(xiàn)特別快
  • 具備參數(shù)的不確定性扑媚,而C或C++更具有確定性腰湾,去實(shí)現(xiàn)動(dòng)態(tài)參數(shù)更麻煩雷恃。

2、怎么查找匯編文件

  • 匯編的后綴名是.s
  • 由于我們?nèi)粘i_發(fā)的都是架構(gòu)是arm64费坊,所以需要在arm64.s后綴的文件中查找

3倒槐、匯編的函數(shù)前面都帶有,如果要在C文件中查找要把這個(gè)刪掉,比如_objc_msgSend

3.6 總結(jié)

  1. 在消息發(fā)送時(shí),先在本類的cache中查找是否存在附井,謂之快速查找
  2. 先通過接受者對象得到Class讨越,再查看Class中的cache,之后拿到cache中的bucket的sel與傳入的_cmd進(jìn)行比較永毅,如果存在則返回imp把跨。
  3. 這里查找bucket的過程涉及到哈希算法,先通過哈希算法_cmd &mask得到哈希地址的bucket
  4. 如果不存在沼死,可能會(huì)是哈希沖突之后的地址着逐,就先向前查找,如果找到第一個(gè)位置還沒找到,就從最后一個(gè)向前查找耸别,一直找到第一個(gè)元素健芭,最終沒找到就開始慢速查找了
  5. 簡單的邏輯就是sel->isa->Class->cache->buckets(_cmd &mask)->通過下標(biāo)獲取到每個(gè)buckets->獲取到sel和imp->進(jìn)行比較

4、慢速查找

慢速查找流程:在快速查找流程中未查找到IMP秀姐,就會(huì)進(jìn)入到類的方法列表以及父類的cache和方法列表中繼續(xù)查詢慈迈,這個(gè)過程就是慢速查找流程。

底層是通過C語言實(shí)現(xiàn)省有。

在進(jìn)行快速查找的過程中我們已經(jīng)發(fā)現(xiàn)了最終會(huì)進(jìn)入到lookUpImpOrForward去執(zhí)行痒留,因此我們就從這個(gè)函數(shù)開始分析。

主要內(nèi)容包括整體流程蠢沿、二分查找法狭瞎、父類查找流程、動(dòng)態(tài)方法解析搏予。這里不對動(dòng)態(tài)方法解析進(jìn)行分析熊锭,會(huì)在下一章進(jìn)行詳細(xì)說明。

慢速查找流程在對方法列表的查詢過程涉及到了類雪侥、分類的方法列表的構(gòu)造方式碗殷,后續(xù)會(huì)詳細(xì)分析類和分類的加載過程,會(huì)解析方法列表是如何構(gòu)造的速缨,在這里遇到關(guān)于方法列表的構(gòu)造內(nèi)容可以先記下不用深究锌妻。

4.1 整體流程

4.1.1 源碼分析

源碼:

/*
 1、如果是從匯編中進(jìn)來旬牲,也就是cache中沒有找到imp,則behavior為0011仿粹,LOOKUP_INITIALIZE | LOOKUP_RESOLVER
 2、如果是通過lookUpIMpOrNil進(jìn)來的原茅,behavior為1100吭历,behavior | LOOKUP_CACHE | LOOKUP_NIL
 3、如果是class_getInstanceMethod進(jìn)來的擂橘,也就是僅僅在查詢方法列表時(shí)晌区,behavior為0010,LOOKUP_RESOLVER
 4通贞、在動(dòng)態(tài)解析過程中會(huì)通過resolveMethod_locked調(diào)用:behavior為0100娇掏,behavior | LOOKUP_CACHE
 */

IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
{
    /*
      method lookup
     enum {
         LOOKUP_INITIALIZE = 1, 0001
         LOOKUP_RESOLVER = 2,   0010
         LOOKUP_CACHE = 4,      0100
         LOOKUP_NIL = 8,        1000
     };
     behavior是其中的某個(gè)值
     因此behavior與這幾個(gè)數(shù)相與敬飒,只有相等径簿,才會(huì)不為0饭冬,如果不相等肯定會(huì)為0,以此來判斷是否是這幾個(gè)枚舉值
     */
    
    
    //消息轉(zhuǎn)發(fā)(報(bào)錯(cuò)方法)
    const IMP forward_imp = (IMP)_objc_msgForward_impcache;
    IMP imp = nil;
    Class curClass;

    runtimeLock.assertUnlocked();

    // Optimistic cache lookup
    //多線程
    /*
     這里是從動(dòng)態(tài)方法解析的過程中來的
     也就是說明此處的方法調(diào)用是查找緩存
     */
    if (fastpath(behavior & LOOKUP_CACHE)) {
        imp = cache_getImp(cls, sel);
        if (imp) goto done_nolock;
    }

    // runtimeLock is held during isRealized and isInitialized checking
    // to prevent races against concurrent realization.

    // runtimeLock is held during method search to make
    // method-lookup + cache-fill atomic with respect to method addition.
    // Otherwise, a category could be added but ignored indefinitely because
    // the cache was re-filled with the old value after the cache flush on
    // behalf of the category.

    runtimeLock.lock();

    // We don't want people to be able to craft a binary blob that looks like
    // a class but really isn't one and do a CFI attack.
    //
    // To make these harder we want to make sure this is a class that was
    // either built into the binary or legitimately registered through
    // objc_duplicateClass, objc_initializeClassPair or objc_allocateClassPair.
    //
    // TODO: this check is quite costly during process startup.
    //是否為已知類茎用,也就是是否已經(jīng)被加載到內(nèi)存中
    checkIsKnownClass(cls);

    //類的實(shí)現(xiàn)遣总,(也就是是否將類的數(shù)據(jù)按照類的結(jié)構(gòu)構(gòu)造完成)你虹,需要將類和元類的繼承鏈都要實(shí)現(xiàn)一下
    if (slowpath(!cls->isRealized())) {
        cls = realizeClassMaybeSwiftAndLeaveLocked(cls, runtimeLock);
        // runtimeLock may have been dropped but is now locked again
    }

    /*
        當(dāng)從cache中沒有查找到進(jìn)入該方法時(shí),behavior為0011,
        behavior & LOOKUP_INITIALIZE說明此處是進(jìn)行查找初始化方法
        初始化彤避,執(zhí)行initialize函數(shù)
        這里可以看出只有在查找方法列表時(shí)才會(huì)調(diào)用initialize函數(shù)
     
        所以條件為:1)cache中沒找到進(jìn)入到方法列表中查找方法傅物;2)且該類還沒有被初始化
     */
    if (slowpath((behavior & LOOKUP_INITIALIZE) && !cls->isInitialized())) {
        cls = initializeAndLeaveLocked(cls, inst, runtimeLock);
        // runtimeLock may have been dropped but is now locked again

        // If sel == initialize, class_initialize will send +initialize and 
        // then the messenger will send +initialize again after this 
        // procedure finishes. Of course, if this is not being called 
        // from the messenger then it won't happen. 2778172
    }

    runtimeLock.assertLocked();
    curClass = cls;

    // The code used to lookpu the class's cache again right after
    // we take the lock but for the vast majority of the cases
    // evidence shows this is a miss most of the time, hence a time loss.
    //
    // The only codepath calling into this without having performed some
    // kind of cache lookup is class_getInstanceMethod().

    // unreasonableClassCount -- 表示類的迭代的上限
    /*
     1、類的方法查找
     2琉预、查找父類為nil
     3董饰、for循環(huán)用來查詢父類的方法列表
     */
    //這個(gè)for循環(huán)用來循環(huán)查詢父類
    for (unsigned attempts = unreasonableClassCount();;) {
        // curClass method list.
        Method meth = getMethodNoSuper_nolock(curClass, sel);
        if (meth) {
            //查找到,就返回imp圆米,并存放到cache中
            imp = meth->imp;
            goto done;
        }

        /*
         1卒暂、給cureClass賦值superclass
         2、判斷父類如果為nil娄帖,也就是NSObject的父類為nil也祠,就開始默認(rèn)轉(zhuǎn)發(fā)
         */
        if (slowpath((curClass = curClass->superclass) == nil)) {
            // No implementation found, and method resolver didn't help.
            // Use forwarding.
            imp = forward_imp;
            break;
        }

        // Halt if there is a cycle in the superclass chain.
        //循環(huán)如果達(dá)到上限了,就提示內(nèi)存損壞近速,不再執(zhí)行
        if (slowpath(--attempts == 0)) {
            _objc_fatal("Memory corruption in class list.");
        }

        // Superclass cache.
        //得到父類的imp(從緩存中查找)诈嘿,最終返回只能是cache中存儲(chǔ)的imp
        imp = cache_getImp(curClass, sel);
        
        //如果父類緩存中得到報(bào)錯(cuò)函數(shù),就直接返回削葱,找初始類的動(dòng)態(tài)方法解析和消息轉(zhuǎn)發(fā)
        //這里如果是報(bào)錯(cuò)函數(shù)奖亚,直接跳出開始默認(rèn)轉(zhuǎn)發(fā)
        if (slowpath(imp == forward_imp)) {
            // Found a forward:: entry in a superclass.
            // Stop searching, but don't cache yet; call method
            // resolver for this class first.
            /*
             如果在父類中查找到了報(bào)錯(cuò)函數(shù),就停止搜索析砸,并且不進(jìn)行緩存昔字,開始對當(dāng)前類進(jìn)行動(dòng)態(tài)方法解析
             */
            break;
        }
        //如果父類存在該方法,則存入到初始類緩存中
        if (fastpath(imp)) {
            // Found the method in a superclass. Cache it in this class.
            goto done;
        }
    }

    //當(dāng)上邊的循環(huán)中遇到break退出循環(huán)時(shí)進(jìn)入到這里
    // No implementation found. Try method resolver once.當(dāng)沒有查找到Imp時(shí)首繁,嘗試一次動(dòng)態(tài)方法解析
    /*
     
     當(dāng)從動(dòng)態(tài)方法解析后再次進(jìn)入該方法時(shí)作郭,behavior為1100
     而LOOKUP_RESOLVER為0010,所以就不會(huì)進(jìn)入。
     */
    //behavior這個(gè)作為標(biāo)識(shí)弦疮,只能進(jìn)一次
    if (slowpath(behavior & LOOKUP_RESOLVER)) {
        //這里是異或操作夹攒,不相等為1,相等為0挂捅,
        //如果如果可以進(jìn)入這里芹助,說明是xx1x,異或一下之后就變成xx0x
        behavior ^= LOOKUP_RESOLVER;
        //動(dòng)態(tài)方法解析
        //這里的返回值不會(huì)是nil闲先,如果查詢不到返回的是forward_imp
        return resolveMethod_locked(inst, sel, cls, behavior);
    }

    //通過查看done發(fā)現(xiàn)如果沒有查找到,不會(huì)存儲(chǔ)進(jìn)cache中无蜂,也就是說這里是不會(huì)存入forward_imp
    //只有查找到imp才會(huì)進(jìn)入到done
 done:
    log_and_fill_cache(cls, imp, sel, inst, curClass);
    runtimeLock.unlock();
 done_nolock:
    //如果behavior為1xxx伺糠,與1000相與,就為YES斥季,此時(shí)再加上查不到imp训桶,就會(huì)返回nil
    //只有一種情況累驮,那就是動(dòng)態(tài)方法解析之后再次執(zhí)行該函數(shù),此時(shí)在cache中查詢得到的是forward_imp舵揭,就會(huì)返回nil
    //這里很疑惑的一點(diǎn)谤专,什么情況下會(huì)把forward_imp存入到緩存中
    if (slowpath((behavior & LOOKUP_NIL) && imp == forward_imp)) {
        return nil;
    }
    //如果是動(dòng)態(tài)方法解析完成后再進(jìn)入該方法一定會(huì)執(zhí)行done_nolock,因?yàn)閞eturn是在done_nolock下面的
    return imp;
}

代碼分析:

  1. 前期準(zhǔn)備
    1. 判斷cache_t中是否已經(jīng)有該sel和imp
    2. 判斷是否是一個(gè)已知的類:判斷當(dāng)前類是否是已經(jīng)被認(rèn)可的類午绳,即已經(jīng)加載的類(類加載時(shí)會(huì)分析)
    3. 將該類的繼承鏈和該類的元類繼承鏈的相關(guān)類都實(shí)現(xiàn)一遍置侍,方便后續(xù)進(jìn)行父類的查找、類方法的查找(類加載時(shí)會(huì)分析)
    4. 判斷該類是否初始化拦焚,如果沒有則進(jìn)行初始化蜡坊, 包括該類的繼承鏈和該類的元類的繼承鏈都進(jìn)行初始化
  2. 方法查詢
    1. 得到sel對應(yīng)的Method
    2. 如果存在,直接拿到IMP
    3. 判斷父類如果為空赎败,則開始報(bào)錯(cuò)秕衙,如果父類存在,則查找父類的方法
  3. 父類方法查詢
    1. 此處又開始在匯編中查找父類的cache僵刮,需要在匯編中查找
    2. 父類的報(bào)錯(cuò)方法
    3. 查找到IMP存在据忘,則繼續(xù)執(zhí)行,存儲(chǔ)到當(dāng)前類的cache中
  4. 方法動(dòng)態(tài)解析(先不處理)

4.1.2 整體流程

整體流程:
1搞糕、初始化類->方法列表查詢->循環(huán)父類查詢->動(dòng)態(tài)方法解析
2若河、查詢到方法后需要保存到當(dāng)前類的cache中
3、如果父類返回報(bào)錯(cuò)函數(shù)或者父類為nil時(shí)返回報(bào)錯(cuò)函數(shù)寞宫,此時(shí)就賦值為報(bào)錯(cuò)函數(shù)萧福,并開始動(dòng)態(tài)方法解析

快速查找:
當(dāng)動(dòng)態(tài)解析之后再次進(jìn)來時(shí)會(huì)先進(jìn)行快速查找,避免經(jīng)過動(dòng)態(tài)方法解析后已經(jīng)有了方法辈赋,在其他線程已經(jīng)將方法插入緩存中

初始化:
1鲫忍、包括類的加載、類的實(shí)現(xiàn)钥屈、類的初始化
2悟民、當(dāng)在方法列表中調(diào)用方法時(shí),如果這個(gè)類從來沒有調(diào)用過initialize函數(shù)篷就,此時(shí)就會(huì)調(diào)用initialize函數(shù)射亏。
3、為什么整個(gè)的繼承鏈竭业、元類的繼承鏈都要實(shí)現(xiàn)一下智润,因?yàn)檫€要找類方法、父類方法
4未辆、Class是雙向鏈表結(jié)構(gòu)窟绷,父類保存有自己的子類,子類保存有自己的父類

方法列表查詢:
1咐柜、方法列表查詢采用二分查找算法實(shí)現(xiàn)的
2兼蜈、方法列表在加載的時(shí)候就已經(jīng)排好序了(通過方法的sel的地址進(jìn)行排序)攘残,因此可以使用二分查找法來快速查找
3、這個(gè)二分查找比較好为狸,因?yàn)樗闹虚g位置都是通過起始位置計(jì)算的歼郭。而后面更改只需要更改起始位置就可以了

父類查詢:
1、當(dāng)前類的cache和methodList都沒有查詢到辐棒,就開始循環(huán)遍歷父類的cache和methodList
2病曾、如果是父類查詢到該方法,需要保存到本類的cache
3涉瘾、父類的cache也會(huì)進(jìn)入到匯編中進(jìn)行
4知态、父類的methodList的循環(huán)是通過for循環(huán)實(shí)現(xiàn)的

示意圖:

08-慢速查找流程.png

小結(jié):
1、先判斷類的加載立叛、類的實(shí)現(xiàn)负敏、類的初始化,如果都完成秘蛇,就可以開始查詢methodList了
2其做、methodList的查詢是通過二分查找法實(shí)現(xiàn)的,二分查找法的前提條件是進(jìn)行排序赁还,在類的實(shí)現(xiàn)過程中就已經(jīng)進(jìn)行了排序了妖泄。(排序是通過sel的地址來排的)
3、通過for循環(huán)來進(jìn)行父類的查找艘策,先查找父類的cache蹈胡,再查找父類的methodList,cache也是通過匯編拉查找的
4朋蔫、一直查詢到父類為nil時(shí)罚渐,或者返回的是一個(gè)報(bào)錯(cuò)方法,就拿到報(bào)錯(cuò)方法驯妄,并進(jìn)行方法的動(dòng)態(tài)解析和消息轉(zhuǎn)發(fā)

4.2 二分查找法

4.2.1 getMethodNoSuper_nolock

源碼:

static method_t *
getMethodNoSuper_nolock(Class cls, SEL sel)
{
    runtimeLock.assertLocked();

    ASSERT(cls->isRealized());
    // fixme nil cls? 
    // fixme nil sel?

    //得到當(dāng)前類的方法列表
    auto const methods = cls->data()->methods();
    //循環(huán)遍歷得到對應(yīng)的Method
    //一個(gè)方法列表數(shù)組里有多個(gè)方法列表荷并,詳情可以看后續(xù)類的加載過程,方法列表是如何加載的
    for (auto mlists = methods.beginLists(),
              end = methods.endLists();
         mlists != end;
         ++mlists)
    {
        // <rdar://problem/46904873> getMethodNoSuper_nolock is the hottest
        // caller of search_method_list, inlining it turns
        // getMethodNoSuper_nolock into a frame-less function and eliminates
        // any store from this codepath.
        //進(jìn)行二分查找法
        method_t *m = search_method_list_inline(*mlists, sel);
        if (m) return m;
    }

    return nil;
}

說明:
1青扔、我們知道方法列表存儲(chǔ)在類的rw中源织,所以可以通過cls->data()->methods來獲取,如果了解類的底層結(jié)構(gòu)微猖,這一點(diǎn)很簡單
2谈息、至于為什么這里有很多方法列表呢?

  • 首先在類的底層結(jié)構(gòu)中我們可以看到通過methods函數(shù)獲取的是方法列表數(shù)組励两,方法列表數(shù)組中存放的所有的方法列表黎茎。方法列表中存儲(chǔ)的是每個(gè)方法。
  • 所以需要先對方法列表數(shù)組進(jìn)行循環(huán)遍歷得到每個(gè)方法列表当悔,之后再對方法列表進(jìn)行二分查找

4.2.1 findMethodInSortedMethodList

通過search_method_list_inline調(diào)用到findMethodInSortedMethodList傅瞻,具體的二分查找算法就在findMethodInSortedMethodList函數(shù)中

源碼:

ALWAYS_INLINE static method_t *
findMethodInSortedMethodList(SEL key, const method_list_t *list)
{
    ASSERT(list);

    const method_t * const first = &list->first;
    const method_t *base = first;
    const method_t *probe;//查找指針
    uintptr_t keyValue = (uintptr_t)key;//需要查找的方法選擇器
    uint32_t count;
    
    /*
     每次循環(huán),查找的數(shù)量是之前的一半(如果除不整盲憎,就向下取整)
     base是起始位置嗅骄,probe是中間位置,count是本輪最大數(shù)量
     每次循環(huán)都要將count/2
     */
    for (count = list->count; count != 0; count >>= 1) {
        /*
         起始位置偏移整體數(shù)量的一半饼疙,就移動(dòng)到了中間位置
         每一次循環(huán)需要讓起始位置偏移count/2的位置得到中間位置
         */
        probe = base + (count >> 1);//右移1位溺森,就是count/2
        
        uintptr_t probeValue = (uintptr_t)probe->name;
        
        //這里是為了獲取后插入的類別的同名方法,
        //后插入的在前面
        if (keyValue == probeValue) {
            // `probe` is a match.
            // Rewind looking for the *first* occurrence of this value.
            // This is required for correct category overrides.
            //如果不是第一個(gè)窑眯,而且probe的前一個(gè)也有這個(gè)name屏积,向前查找
            while (probe > first && keyValue == (uintptr_t)probe[-1].name) {
                probe--;
            }
            return (method_t *)probe;
        }
        
        //如果在右側(cè),則將起始位置放置到probe+1磅甩,(count-1)/2炊林,這里減一是因?yàn)榘阎暗膒robe也減去了
        /*
         如果在中間位置的右側(cè),需喲啊改變起始位置
         改變后需要將數(shù)量-1卷要,因?yàn)槠鹗嘉恢闷坪笤郏紦?jù)了一個(gè)位置,所以總數(shù)要減少1
         */
        if (keyValue > probeValue) {
            base = probe + 1;
            count--;
        }
    }
    
    return nil;
}

說明:
只是一個(gè)簡單的算法沒有難度僧叉,而且代碼層面注釋已經(jīng)寫得足夠詳細(xì)了奕枝,也就不展開多說了。這里說下算法注意點(diǎn)

注意點(diǎn)

  1. 比較的只是中間位置瓶堕,前后位置不需要比較隘道,而中間位置是通過起始位置和總數(shù)來進(jìn)行計(jì)算的
  2. 總數(shù)的計(jì)算,總數(shù)每次都要變郎笆,也就是除以2谭梗,向右位移1位
  3. 起始位置的計(jì)算
    1. 如果實(shí)際位置在中間位置右側(cè),則需要改變起始位置题画,也就是中間位置probe+1,因?yàn)榘裵robe也減去了默辨,所以總數(shù)還要先減一,后面再去除以2.
    2. 如果實(shí)際位置在中間位置左側(cè)苍息,就不需要改變
  4. 這里還有一個(gè)比較不好想象的小細(xì)節(jié)缩幸,就是如果沒有中間位置,比如中間只有偶位數(shù)竞思,中間位置何去何從呢
    1. count>>1,這個(gè)是簡單計(jì)算一下就會(huì)發(fā)現(xiàn)最后一位是會(huì)抹去的表谊,也就是說會(huì)向下取整
    2. 也就是如果是偶位數(shù),那么中間位置會(huì)是中間偏后的那個(gè)盖喷。
      5爆办、多個(gè)分類的加載,越遲加載的越在前邊课梳,所以需要向前查找距辆,已得到分類的同名方法余佃。

小結(jié):
1、方法列表中查找采用二分查找法查找
2跨算、分類比類加載要晚爆土,所以分類的方法會(huì)在類的方法前面,而且越遲加載的分類越在前面
3诸蚕、因此我們在查找方法列表時(shí)當(dāng)查找到方法時(shí)步势,會(huì)繼續(xù)向前查找分類的同名方法。
4背犯、排序是通過方法選擇器地址來排的坏瘩,判斷方法是通過方法名來判斷的。這個(gè)要記住漠魏,很多人會(huì)認(rèn)為排序是通過方法名來排的倔矾,其實(shí)并不是。

4.3 父類查找流程

父類的流程重點(diǎn)在于先通過匯編查找cache蛉幸,之后再回來C語言中查找方法列表破讨。

cache緩存查找太簡單,通過cache_getImp在匯編中一搜奕纫,之后一步一步往下走即可提陶。這里就不再贅言了。

源碼:

//這個(gè)for循環(huán)用來循環(huán)查詢父類
    for (unsigned attempts = unreasonableClassCount();;) {
        // curClass method list.
        Method meth = getMethodNoSuper_nolock(curClass, sel);
        if (meth) {
            //查找到匹层,就返回imp隙笆,并存放到cache中
            imp = meth->imp;
            goto done;
        }

        /*
         1、給cureClass賦值superclass
         2升筏、判斷父類如果為nil撑柔,也就是NSObject的父類為nil,就開始默認(rèn)轉(zhuǎn)發(fā)
         */
        if (slowpath((curClass = curClass->superclass) == nil)) {
            // No implementation found, and method resolver didn't help.
            // Use forwarding.
            imp = forward_imp;
            break;
        }

        // Halt if there is a cycle in the superclass chain.
        //循環(huán)如果達(dá)到上限了您访,就提示內(nèi)存損壞铅忿,不再執(zhí)行
        if (slowpath(--attempts == 0)) {
            _objc_fatal("Memory corruption in class list.");
        }

        // Superclass cache.
        //得到父類的imp(從緩存中查找),最終返回只能是cache中存儲(chǔ)的imp
        imp = cache_getImp(curClass, sel);
        
        //如果父類緩存中得到報(bào)錯(cuò)函數(shù)灵汪,就直接返回檀训,找初始類的動(dòng)態(tài)方法解析和消息轉(zhuǎn)發(fā)
        //這里如果是報(bào)錯(cuò)函數(shù),直接跳出開始默認(rèn)轉(zhuǎn)發(fā)
        if (slowpath(imp == forward_imp)) {
            // Found a forward:: entry in a superclass.
            // Stop searching, but don't cache yet; call method
            // resolver for this class first.
            /*
             如果在父類中查找到了報(bào)錯(cuò)函數(shù)享言,就停止搜索峻凫,并且不進(jìn)行緩存,開始對當(dāng)前類進(jìn)行動(dòng)態(tài)方法解析
             */
            break;
        }
        //如果父類存在該方法览露,則存入到初始類緩存中
        if (fastpath(imp)) {
            // Found the method in a superclass. Cache it in this class.
            goto done;
        }
    }

說明:

  1. 簡單的循環(huán)流程
    1. 先查找當(dāng)前類的methodList
    2. 再查找父類的cache
    3. 再查找父類的methodList
  2. cache_getImp方法是通過匯編_cache_getImp實(shí)現(xiàn)荧琼,傳入的$0 是 GETIMP
  3. 如果父類緩存中找到了方法實(shí)現(xiàn),則跳轉(zhuǎn)至CacheHit即命中,則直接返回imp
  4. 如果在父類緩存中命锄,沒有找到方法實(shí)現(xiàn)堰乔,則跳轉(zhuǎn)至CheckMiss 或者 JumpMiss,通過判斷$0 跳轉(zhuǎn)至LGetImpMiss累舷,直接返回nil

流程示意圖:

08-父類查找流程.png

小結(jié):

  1. 當(dāng)前類查詢methodList沒有找到后浩考,會(huì)先查找父類的cache
  2. 如果父類的cache沒有找到夹孔,就查找父類的方法列表
  3. 如果父類仍然沒有找到被盈,繼續(xù)找父類,一直到父類為nil搭伤,當(dāng)父類為nil時(shí)只怎,說明當(dāng)前類是NSObject類。也就是找到頭了怜俐,此時(shí)會(huì)退出開始開始進(jìn)行動(dòng)態(tài)方法解析身堡。
  4. 父類查找到方法后會(huì)存儲(chǔ)在當(dāng)前類的cache中。

4.4 總結(jié)

  1. 慢速查找流程先查找類的方法列表拍鲤,再查找父類的cache和方法列表贴谎,一直找到NSObject類仍然沒有找到就開始動(dòng)態(tài)方法解析,解析完成后會(huì)再次查找方法列表季稳。如果仍然沒有找到擅这,就返回forward_imp,也就是報(bào)錯(cuò)函數(shù)景鼠。如果找到就存儲(chǔ)到當(dāng)前類的cache中仲翎。
  2. 方法列表的查找是通過二分查找法來實(shí)現(xiàn)的,當(dāng)查找到時(shí)會(huì)繼續(xù)在方法列表中向前查找铛漓,尋找分類中的同名方法溯香。

5、動(dòng)態(tài)方法解析

在上文可知浓恶,lookUpImpOrForward函數(shù)中玫坛,當(dāng)前類以及父類都沒有查找到方法時(shí),會(huì)break跳出循環(huán)包晰,開始執(zhí)行動(dòng)態(tài)方法解析湿镀,下面就開始分析動(dòng)態(tài)方法解析的過程。

5.1 整體說明

以前我們都知道動(dòng)態(tài)方法解析的做法就是通過resovleInstanceMethod方法或resolveClassMethod方法來給原來的sel動(dòng)態(tài)的增加一個(gè)imp杜窄。增加后再去cache中或方法列表中查詢就可以正確的給sel發(fā)送消息了肠骆。

所以我們接下來的任務(wù)就是查看在底層是如何實(shí)現(xiàn)這一過程的。

5.2 behavior的認(rèn)識(shí)

我們看到在進(jìn)入lookUpImpOrForward函數(shù)時(shí)塞耕,會(huì)傳入一個(gè)參數(shù)behavior蚀腿,在代碼中會(huì)通過這個(gè)參數(shù)來判斷哪些代碼需要執(zhí)行,哪些不需要執(zhí)行,所以為了看懂代碼流程莉钙,一定要理解behavior是怎么使用的

定義:

method lookup
     enum {
         LOOKUP_INITIALIZE = 1, 0001
         LOOKUP_RESOLVER = 2,   0010
         LOOKUP_CACHE = 4,      0100
         LOOKUP_NIL = 8,        1000
     };

可以看到有四種類型廓脆,behavior與這幾個(gè)數(shù)相與,只有相等磁玉,才會(huì)不為0停忿,如果不相等肯定會(huì)為0,以此來判斷是否是這幾個(gè)枚舉值蚊伞。

共有初始化席赂、動(dòng)態(tài)方法解析、緩存查找时迫、返回nil四種執(zhí)行判斷

通過全局搜索查找可以看到不同的地方調(diào)用該函數(shù)颅停,這四種執(zhí)行分別是否會(huì)執(zhí)行。

1掠拳、如果是從匯編中進(jìn)來癞揉,也就是cache中沒有找到imp,則behavior為0011,LOOKUP_INITIALIZE | LOOKUP_RESOLVER溺欧,可以進(jìn)行初始化和動(dòng)態(tài)方法解析喊熟。
2、如果是通過lookUpIMpOrNil進(jìn)來的姐刁,behavior為1100芥牌,behavior | LOOKUP_CACHE | LOOKUP_NIL,可以進(jìn)行cache查找和返回nil.
3龙填、如果是class_getInstanceMethod進(jìn)來的胳泉,也就是僅僅在查詢方法列表時(shí),behavior為0010岩遗,LOOKUP_RESOLVER扇商,可以進(jìn)行動(dòng)態(tài)方法解析。(\color{red}{ 這個(gè)方法在消息轉(zhuǎn)發(fā)的消息重定向中會(huì)調(diào)用) }
4宿礁、在動(dòng)態(tài)解析過程中會(huì)通過resolveMethod_locked調(diào)用:behavior為0100案铺,behavior | LOOKUP_CACHE,可以進(jìn)行動(dòng)態(tài)方法解析梆靖。

具體使用:

LOOKUP_CACHE

// Optimistic cache lookup
   //多線程
   /*
    這里是從動(dòng)態(tài)方法解析的過程中來的
    也就是說明此處的方法調(diào)用是查找緩存
    */
   if (fastpath(behavior & LOOKUP_CACHE)) {
       imp = cache_getImp(cls, sel);
       if (imp) goto done_nolock;
   }

LOOKUP_INITIALIZE

/*
       當(dāng)從cache中沒有查找到進(jìn)入該方法時(shí),behavior為0011控汉,
       behavior & LOOKUP_INITIALIZE說明此處是進(jìn)行查找初始化方法
       初始化,執(zhí)行initialize函數(shù)
       這里可以看出只有在查找方法列表時(shí)才會(huì)調(diào)用initialize函數(shù)
    
       所以條件為:1)cache中沒找到進(jìn)入到方法列表中查找方法返吻;2)且該類還沒有被初始化
    */
   if (slowpath((behavior & LOOKUP_INITIALIZE) && !cls->isInitialized())) {
       cls = initializeAndLeaveLocked(cls, inst, runtimeLock);
       // runtimeLock may have been dropped but is now locked again

       // If sel == initialize, class_initialize will send +initialize and 
       // then the messenger will send +initialize again after this 
       // procedure finishes. Of course, if this is not being called 
       // from the messenger then it won't happen. 2778172
   }

LOOKUP_RESOLVER

 當(dāng)從動(dòng)態(tài)方法解析后再次進(jìn)入該方法時(shí)姑子,behavior為1100
    而LOOKUP_RESOLVER為0010,所以就不會(huì)進(jìn)入。
    */
   //behavior這個(gè)作為標(biāo)識(shí)测僵,只能進(jìn)一次
   if (slowpath(behavior & LOOKUP_RESOLVER)) {
       //這里是異或操作街佑,不相等為1谢翎,相等為0,
       //如果如果可以進(jìn)入這里沐旨,說明是xx1x森逮,異或一下之后就變成xx0x
       behavior ^= LOOKUP_RESOLVER;
       //動(dòng)態(tài)方法解析
       //這里的返回值不會(huì)是nil,如果查詢不到返回的是forward_imp
       return resolveMethod_locked(inst, sel, cls, behavior);
   }

LOOKUP_NIL

//如果behavior為1xxx磁携,與1000相與褒侧,就為YES,此時(shí)再加上查不到imp谊迄,就會(huì)返回nil
   //只有一種情況闷供,那就是動(dòng)態(tài)方法解析之后再次執(zhí)行該函數(shù),此時(shí)在cache中查詢得到的是forward_imp鳞上,就會(huì)返回nil
   //這里很疑惑的一點(diǎn)这吻,什么情況下會(huì)把forward_imp存入到緩存中
   if (slowpath((behavior & LOOKUP_NIL) && imp == forward_imp)) {
       return nil;
   }

5.3 lookUpImpOrForward中開始進(jìn)入動(dòng)態(tài)方法解析

源碼:

 //當(dāng)上邊的循環(huán)中遇到break退出循環(huán)時(shí)進(jìn)入到這里
    // No implementation found. Try method resolver once.當(dāng)沒有查找到Imp時(shí),嘗試一次動(dòng)態(tài)方法解析
    /*
     
     當(dāng)從動(dòng)態(tài)方法解析后再次進(jìn)入該方法時(shí)篙议,behavior為1100
     而LOOKUP_RESOLVER為0010,所以就不會(huì)進(jìn)入。
     */
    //behavior這個(gè)作為標(biāo)識(shí)怠硼,只能進(jìn)一次
    if (slowpath(behavior & LOOKUP_RESOLVER)) {
        //這里是異或操作鬼贱,不相等為1,相等為0香璃,
        //如果如果可以進(jìn)入這里这难,說明是xx1x汹族,異或一下之后就變成xx0x
        behavior ^= LOOKUP_RESOLVER;
        //動(dòng)態(tài)方法解析
        //這里的返回值不會(huì)是nil恢恼,如果查詢不到返回的是forward_imp
        return resolveMethod_locked(inst, sel, cls, behavior);
    }

說明:

  1. 上邊代碼中如果通過break退出循環(huán)時(shí),就會(huì)開始進(jìn)入到動(dòng)態(tài)方法解析五辽。上文已經(jīng)分析過眯牧,如果是沒有查找到方法就會(huì)break蹋岩。所以是當(dāng)沒有找到方法后開始進(jìn)入動(dòng)態(tài)方法解析
  2. 一個(gè)類每次進(jìn)行慢速查找后只會(huì)執(zhí)行一次動(dòng)態(tài)方法解析
  3. 通過behavior來判斷,避免動(dòng)態(tài)方法解析之后去查詢imp時(shí)再次進(jìn)入学少,造成死循環(huán)
  4. 這里的behavior異或完之后變成了xx0x剪个,之后作為參數(shù)傳入,而在resolveMethod_locked中會(huì)再次調(diào)用lookUpImpOrForward中將behavior作為參數(shù)傳入版确,所以再進(jìn)入到該方法中不會(huì)再進(jìn)入到動(dòng)態(tài)解析了扣囊。
  5. 最終通過resolveMethod_locked函數(shù)來執(zhí)行動(dòng)態(tài)方法解析。

5.4 resolveMethod_locked中開始執(zhí)行動(dòng)態(tài)方法解析

源碼:

static NEVER_INLINE IMP
resolveMethod_locked(id inst, SEL sel, Class cls, int behavior)
{
    runtimeLock.assertLocked();
    ASSERT(cls->isRealized());

    runtimeLock.unlock();

    //不是元類,直接使用實(shí)例方法的解析
    if (! cls->isMetaClass()) {
        // try [cls resolveInstanceMethod:sel]
        resolveInstanceMethod(inst, sel, cls);
    }
    //如果是元類绒疗,需要調(diào)用類方法
    else {
        // try [nonMetaClass resolveClassMethod:sel]
        // and [cls resolveInstanceMethod:sel]
        //這里是因?yàn)閕sa繼承關(guān)系中最終會(huì)走到NSObject侵歇,所以還需要一個(gè)實(shí)例方法
        //這個(gè)是resolveClassMethod里將類方法解析成類方法
        resolveClassMethod(inst, sel, cls);
        //這里還會(huì)再查詢一遍,此時(shí)behavior為1100吓蘑,因此不會(huì)再動(dòng)態(tài)方法解析了
        /*
         這個(gè)sel在cache中查詢到的imp是forward_imp惕虑,那么就返回nil
         */
        if (!lookUpImpOrNil(inst, sel, cls)) {
            //這個(gè)是resolveInstanceMethod里將實(shí)例方法解析成類方法
            resolveInstanceMethod(inst, sel, cls);
        }
    }
    // chances are that calling the resolver have populated the cache
    // so attempt using it
    //在調(diào)用方法解析的時(shí)機(jī)已經(jīng)添加到了cache,所以需要去查詢一下緩存的方法(chances這里應(yīng)該表示為時(shí)機(jī))
    //多線程,此時(shí)可能已經(jīng)增加了該方法
    //可以看到這里的behavior為xx0x枷遂,所以本次的查詢是不會(huì)進(jìn)入到動(dòng)態(tài)方法解析中了
    return lookUpImpOrForward(inst, sel, cls, behavior | LOOKUP_CACHE);
}

說明:

  1. 如果是傳入的類不是元類樱衷,則調(diào)用resolveInstanceMethod()來對實(shí)例方法進(jìn)行解析
  2. 如果是傳入的類是元類,則調(diào)用resolveClassMethod()來對類方法進(jìn)行解析
  3. 解析完成后酒唉,會(huì)再次調(diào)用lookUpImpOrForward再次進(jìn)行查詢方法矩桂。可以看到這里的behavior為xx0x痪伦,所以本次的查詢是不會(huì)進(jìn)入到動(dòng)態(tài)方法解析中了侄榴。
  4. 使用LOOKUP_CACHE,是考慮到多線程此時(shí)可能已經(jīng)有其他線程執(zhí)行了該方法

5.5 resolveInstanceMethod對實(shí)例方法進(jìn)行動(dòng)態(tài)方法解析

源碼:

/***********************************************************************
* resolveInstanceMethod
* Call +resolveInstanceMethod, looking for a method to be added to class cls.
* cls may be a metaclass or a non-meta class.可能是一個(gè)元類网沾,也可能是一個(gè)非元類
* Does not check if the method already exists.
**********************************************************************/

static void resolveInstanceMethod(id inst, SEL sel, Class cls)
{
    runtimeLock.assertUnlocked();
    ASSERT(cls->isRealized());
    //得到這個(gè)方法癞蚕,系統(tǒng)提供的resolveInstanceMethod,需要自己實(shí)現(xiàn)
    SEL resolve_sel = @selector(resolveInstanceMethod:);
    //再去查詢辉哥,發(fā)現(xiàn)沒有實(shí)現(xiàn)resolveInstanceMethod就直接退出
    /*
     如果cls是元類桦山,則根元類也是有resolveInstanceMethod的,所以也可以判斷
     */
    if (!lookUpImpOrNil(cls, resolve_sel, cls->ISA())) {
        // Resolver not implemented.
        return;
    }

    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    //調(diào)用一個(gè)objc_msgSend函數(shù)執(zhí)行resolveInstanceMethod:
    //如果返回一個(gè)YES
    /*
     如果cls是元類醋旦,也會(huì)執(zhí)行resolveInstanceMethod函數(shù)
     也就是說如果查找的是類方法恒水,也會(huì)進(jìn)入到resolveInstanceMethod
     */
    bool resolved = msg(cls, resolve_sel, sel);

    // Cache the result (good or bad) so the resolver doesn't fire next time.
    // +resolveInstanceMethod adds to self a.k.a. cls
    //這里可以看到不管返回的resolved是不是YES,都會(huì)進(jìn)行一次查詢
    //再查詢一次得到imp
    //此時(shí)雖然會(huì)查詢一遍饲齐,但是因?yàn)閘ookUpImpOrNil函數(shù)中的behavior固定為1100钉凌,所以不會(huì)再次進(jìn)行動(dòng)態(tài)方法解析了
    IMP imp = lookUpImpOrNil(inst, sel, cls);
    //只有yes才會(huì)進(jìn)入
    if (resolved  &&  PrintResolving) {
        //進(jìn)行打印
        if (imp) {
            _objc_inform("RESOLVE: method %c[%s %s] "
                         "dynamically resolved to %p", 
                         cls->isMetaClass() ? '+' : '-', 
                         cls->nameForLogging(), sel_getName(sel), imp);
        }
        else {
            // Method resolver didn't add anything?
            _objc_inform("RESOLVE: +[%s resolveInstanceMethod:%s] returned YES"
                         ", but no new implementation of %c[%s %s] was found",
                         cls->nameForLogging(), sel_getName(sel), 
                         cls->isMetaClass() ? '+' : '-', 
                         cls->nameForLogging(), sel_getName(sel));
        }
    }
}

執(zhí)行過程:
1、先判斷元類或根元類中是否實(shí)現(xiàn)了resolveInstanceMethod
2捂人、執(zhí)行resolveInstanceMethod
3御雕、再次對sel進(jìn)行查詢,通過lookUpImpOrNil

說明:

  1. 根據(jù)源碼注釋滥搭,也通過resolveMethod_locked函數(shù)中均可以看到resolveInstanceMethod傳入的可能是類酸纲,也可能是元類,也就是它也可以進(jìn)行類方法的動(dòng)態(tài)解析论熙。
  2. 會(huì)先執(zhí)行一下resolveInstanceMethod方法福青。
  3. 執(zhí)行后就再通過lookUpImpOrNil查詢一遍imp,注意此時(shí)直接就查詢脓诡,而不會(huì)看resolveInstanceMethod執(zhí)行返回的是否是YES无午。
  4. resolveInstanceMethod方法的返回值只是為了打印,并沒有其他作用祝谚,真正起作用的就是看到底有沒有給這個(gè)sel增加imp
  5. lookUpImpOrNil函數(shù)中調(diào)用lookUpImpOrForward函數(shù)傳入的behavior是0011宪迟,所以并不會(huì)再次進(jìn)行動(dòng)態(tài)方法解析了。
  6. 因此我們只需要在這個(gè)resolveInstanceMethod方法中給這個(gè)sel加上imp交惯,就可以保證查詢成功

lookUpImpOrNil

lookUpImpOrNil(id obj, SEL sel, Class cls, int behavior = 0)
{
    //behavior | LOOKUP_CACHE | LOOKUP_NIL為1100
    return lookUpImpOrForward(obj, sel, cls, behavior | LOOKUP_CACHE | LOOKUP_NIL);
}

5.6 resolveClassMethod對類方法進(jìn)行動(dòng)態(tài)方法解析

源碼:

/***********************************************************************
* resolveClassMethod
* Call +resolveClassMethod, looking for a method to be added to class cls.
* cls should be a metaclass.這里的cls是元類
* Does not check if the method already exists.
**********************************************************************/
static void resolveClassMethod(id inst, SEL sel, Class cls)
{
    runtimeLock.assertUnlocked();
    ASSERT(cls->isRealized());
    ASSERT(cls->isMetaClass());

    //判斷resolveClassMethod有沒有實(shí)現(xiàn)次泽,如果沒有實(shí)現(xiàn)直接退出
    if (!lookUpImpOrNil(inst, @selector(resolveClassMethod:), cls)) {
        // Resolver not implemented.
        return;
    }

    Class nonmeta;
    {
        mutex_locker_t lock(runtimeLock);
        nonmeta = getMaybeUnrealizedNonMetaClass(cls, inst);//返回元類的原始類(如果不是元類穿仪,就返回它自己)
        // +initialize path should have realized nonmeta already
        if (!nonmeta->isRealized()) {
            _objc_fatal("nonmeta class %s (%p) unexpectedly not realized",
                        nonmeta->nameForLogging(), nonmeta);
        }
    }
    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    //消息發(fā)送,執(zhí)行resolveClassMethod函數(shù)
    bool resolved = msg(nonmeta, @selector(resolveClassMethod:), sel);

    // Cache the result (good or bad) so the resolver doesn't fire next time.
    // +resolveClassMethod adds to self->ISA() a.k.a. cls
    //再一次進(jìn)行消息發(fā)送
    IMP imp = lookUpImpOrNil(inst, sel, cls);

    if (resolved  &&  PrintResolving) {
        if (imp) {
            _objc_inform("RESOLVE: method %c[%s %s] "
                         "dynamically resolved to %p", 
                         cls->isMetaClass() ? '+' : '-', 
                         cls->nameForLogging(), sel_getName(sel), imp);
        }
        else {
            //返回為空意荤,說明沒有動(dòng)態(tài)方法解析沒有給這個(gè)sel添加imp
            // Method resolver didn't add anything?
            _objc_inform("RESOLVE: +[%s resolveClassMethod:%s] returned YES"
                         ", but no new implementation of %c[%s %s] was found",
                         cls->nameForLogging(), sel_getName(sel), 
                         cls->isMetaClass() ? '+' : '-', 
                         cls->nameForLogging(), sel_getName(sel));
        }
    }
}

執(zhí)行過程:
1啊片、判斷當(dāng)前的元類中是否有resolveClassMethod方法
2、獲取到該元類的原始類
3玖像、執(zhí)行resolveClassMethod方法
4紫谷、再一次進(jìn)行消息發(fā)送,通過lookUpImpOrNil

說明:

  1. 這里傳入的cls只是元類捐寥,不會(huì)是類
  2. 針對類方法進(jìn)行動(dòng)態(tài)方法解析
  3. 內(nèi)部會(huì)執(zhí)行resolveClassMethod方法
  4. 通過nonmeta = getMaybeUnrealizedNonMetaClass(cls, inst);獲取到元類的初始類笤昨。
  5. 可以看到調(diào)用類方法,objc_msgSend函數(shù)中傳遞的接受者參數(shù)也必須是類握恳,而不能是元類瞒窒,雖然方法存儲(chǔ)在元類中,但是消息接受者仍然是類乡洼。
  6. 執(zhí)行完resolveClassMethod崇裁,再執(zhí)行一次消息發(fā)送,查詢imp

5.7 一些疑問的解答

1就珠、為什么當(dāng)cls是元類時(shí)寇壳,除了執(zhí)行一次resolveClassMethod,當(dāng)判斷l(xiāng)ookUpImpOrNil沒有成功后妻怎,需要再執(zhí)行一次resolveInstanceMethod。這是為啥呢泞歉?

首先我看到有人說這是因?yàn)轭惙椒ㄔ谠愔惺且詫?shí)例方法的姿態(tài)存在逼侦,如果是這樣的話,那么用一種方式去做就行了腰耙,為什么要用兩種呢榛丢,沒必要,而且在resolveClassMethod也是通過獲取到元類的原始類才去調(diào)用的方法呀挺庞,并不是直接用的元類去調(diào)用的晰赞,按照這種說法的話,通過resolveClassMethod沒有獲取到选侨,就會(huì)再次通過resolveInstanceMethod來獲取掖鱼,然而實(shí)際調(diào)試發(fā)現(xiàn)并不會(huì)進(jìn)入,說明此處是另有深意援制,并不是簡單的第一種方式不行戏挡,再用第二種方式查詢。

這個(gè)也不是因?yàn)樵惖睦^承鏈鏈中有NSObject導(dǎo)致的晨仑,因?yàn)樵趓esolveClassMethod也是拿到原始類直接調(diào)用的褐墅,而如果傳入的不是元類拆檬,就直接用自己。

接下來需要看看調(diào)用lookUpImpOrNil返回nil條件妥凳。

查看lookUpImpOrNil函數(shù)

static inline IMP
lookUpImpOrNil(id obj, SEL sel, Class cls, int behavior = 0)
{
    //behavior | LOOKUP_CACHE | LOOKUP_NIL為1100
    return lookUpImpOrForward(obj, sel, cls, behavior | LOOKUP_CACHE | LOOKUP_NIL);
}

說明:
這里可以看到竟贯,傳入的behavior包含有LOOKUP_NIL。上文我們在分析behavior知道只有這里調(diào)用時(shí)才會(huì)判斷LOOKUP_NIL逝钥。

再查看lookUpImpOrForward函數(shù)

cache查找

// Optimistic cache lookup
    //多線程
    /*
     這里是從動(dòng)態(tài)方法解析的過程中來的
     也就是說明此處的方法調(diào)用是查找緩存
     */
    if (fastpath(behavior & LOOKUP_CACHE)) {
        imp = cache_getImp(cls, sel);
        if (imp) goto done_nolock;
    }

說明: 這里也是唯一的可以進(jìn)入到done_nolock的地方屑那。并且當(dāng)查詢到imp后執(zhí)行done_nolock

done_nolock

done_nolock:
    //如果behavior為1xxx,與1000相與晌缘,就為YES齐莲,此時(shí)再加上查不到imp,就會(huì)返回nil
    //只有一種情況磷箕,那就是動(dòng)態(tài)方法解析之后再次執(zhí)行該函數(shù)选酗,此時(shí)在cache中查詢得到的是forward_imp,就會(huì)返回nil
    //這里很疑惑的一點(diǎn)岳枷,什么情況下會(huì)把forward_imp存入到緩存中
    if (slowpath((behavior & LOOKUP_NIL) && imp == forward_imp)) {
        return nil;
    }

說明:

  • 在nonlock中如果imp為forward_imp芒填,就直接返回nil
  • 此時(shí)的behavior為1100,所以(behavior & LOOKUP_NIL)肯定是1

總結(jié):所以當(dāng)動(dòng)態(tài)解析之后cache中查詢到的imp為forward_imp時(shí)會(huì)返回nil,此時(shí)會(huì)執(zhí)行resolveInstanceMethod

5.8 幾個(gè)沒有理解的問題空繁,有知道的可以評論告訴我

1殿衰、為什么傳入的cls可以是元類呢,我們在調(diào)用方法時(shí)盛泡,傳入的不都是類嗎闷祥?誰會(huì)直接傳元類呢,而且查看調(diào)用lookUpImpOrForward的地方也沒找到哪里傳的是元類傲诵,很奇怪凯砍。

2、在resolveInstanceMethod或resolveClassMethod中已經(jīng)執(zhí)行了一次lookUpImpOrNil了拴竹,為什么再執(zhí)行一次lookUpImpOrForward呢悟衩?

猜測:

  • 雖然進(jìn)行了方法動(dòng)態(tài)解析,并且也通過lookUpImpOrNil再一次進(jìn)行查詢了
  • 但是上一次執(zhí)行的lookUpImpOrForward方法的執(zhí)行還沒有返回值呢栓拜,所以需要在這里執(zhí)行一下獲取到返回值
  • 而在動(dòng)態(tài)方法解析過程中的再一次查詢的過程中可能已經(jīng)緩存到cache中了座泳,所以此時(shí)傳入的behavior要包含LOOKUP_CACHE用來查詢cache

5.9 簡單驗(yàn)證

在WYStudent類中寫有mehtodDynamically方法的聲明,但是沒有實(shí)現(xiàn)幕与,在resolveInstanceMethod方法中判斷mehtodDynamically的SEL挑势,就將resolveInstanceMethodTest函數(shù)作為mehtodDynamically的函數(shù)實(shí)現(xiàn)。

代碼:

//實(shí)例方法的動(dòng)態(tài)方法解析
/*
 如果給該sel添加了imp纽门,則直接執(zhí)行
 如果沒有添加成功薛耻,不管返回的YES還是NO,都會(huì)執(zhí)行消息轉(zhuǎn)發(fā)
 */
+ (BOOL)resolveInstanceMethod:(SEL)sel {
    //mehtodDynamically方法在.h中聲明了赏陵,但是沒有在.m文件中定義
    if (sel == @selector(mehtodDynamically)) {
        //這里的參數(shù)很重要饼齿,具體情況具體看
        //給一個(gè)類添加方法饲漾,參數(shù)分別是類名、方法選擇器缕溉、IMP考传,參數(shù)類型(涉及到類型編碼)
        class_addMethod([self class],sel,(IMP)resolveInstanceMethodTest,"v@:");
        return YES;
    }
    if (sel == @selector(eat)) {
        return NO;
    }
    return [super resolveInstanceMethod:sel];
}

//這里的參數(shù)必須這樣寫,因?yàn)榈讓颖緛砭褪沁@樣寫的证鸥。保持一致僚楞。
void resolveInstanceMethodTest(id self,SEL _cmd){
    NSLog(@"大家好,我是一個(gè)被動(dòng)態(tài)解析的作為mehtodDynamically實(shí)現(xiàn)函數(shù)");
}


//調(diào)用
//動(dòng)態(tài)方法解析
WYStudent *student = [WYStudent alloc];
[student mehtodDynamically];

結(jié)果:

2021-10-17 14:58:39.232592+0800 消息發(fā)送[84736:1535620] 大家好枉层,我是一個(gè)被動(dòng)態(tài)解析的作為mehtodDynamically實(shí)現(xiàn)函數(shù)

5.10 總結(jié)

  1. 慢速查找會(huì)查找當(dāng)前類的方法列表泉褐,如果方法列表也不存在,則開始查詢父類的cache和方法列表
  2. 慢速查找過程:先查找當(dāng)前類的方法列表鸟蜡,之后再查找父類的cache和父類的方法列表膜赃,一直到NSObject還沒有查找到就開始進(jìn)行動(dòng)態(tài)方法解析,解析完成后會(huì)再次查找方法列表揉忘。如果仍然沒有找到跳座,就返回forward_imp,也就是報(bào)錯(cuò)函數(shù)
  3. 在方法列表中通過sel查找imp是通過二分查找來獲取的
  4. 動(dòng)態(tài)方法解析是通過resolveInstanceMethod和resolveClassMethod實(shí)現(xiàn)的泣矛。

6疲眷、消息轉(zhuǎn)發(fā)

在上文我們從objc_msgSend開始查詢,查到了cache流程您朽、方法列表查找流程狂丝、動(dòng)態(tài)方法解析流程,可是動(dòng)態(tài)方法解析之后再次執(zhí)行了lookUpImpOrForward哗总,如果沒有找到方法實(shí)現(xiàn)美侦,會(huì)將報(bào)錯(cuò)函數(shù)的賦給imp,再繼續(xù)找源碼并沒有發(fā)現(xiàn)消息轉(zhuǎn)發(fā)相關(guān)的代碼魂奥。所以這個(gè)方法調(diào)用的流程已經(jīng)結(jié)束了,也就是objc_msgSend的流程結(jié)束了易猫。

人們都說在動(dòng)態(tài)方法解析之后會(huì)進(jìn)行消息轉(zhuǎn)發(fā)耻煤,那么是怎么來的呢?

查看官方文檔時(shí)知道當(dāng)一個(gè)對象由于沒有相應(yīng)的方法實(shí)現(xiàn)而無法響應(yīng)某消息時(shí)准颓,運(yùn)行時(shí)系統(tǒng)將通過 forwardInvocation:消息通知該對象哈蝇。

因此我們可以在對這個(gè)對象進(jìn)行通知時(shí)將消息轉(zhuǎn)發(fā)給其它對象來實(shí)現(xiàn)。

注:

  • 因此在這個(gè)意義上說消息轉(zhuǎn)發(fā)并不是消息發(fā)送的流程攘已,真正消息發(fā)送的流程是objc_msgSend的過程炮赦,上文我們分析過了,最后走到了動(dòng)態(tài)方法解析即結(jié)束了样勃。
  • 而我們可以通過消息轉(zhuǎn)發(fā)實(shí)現(xiàn)吠勘,是因?yàn)楫?dāng)消息發(fā)送失敗后性芬,系統(tǒng)會(huì)給這個(gè)對象發(fā)送一個(gè)通知。所以我們在這個(gè)通知中進(jìn)行消息轉(zhuǎn)發(fā)剧防。
  • 消息發(fā)送是給這個(gè)對象發(fā)送一個(gè)消息植锉,消息轉(zhuǎn)發(fā)其實(shí)已經(jīng)脫離了這個(gè)范疇。

6.1 消息轉(zhuǎn)發(fā)的分析思路

雖然通過官方文檔知道了在動(dòng)態(tài)方法解析后如果仍然沒有找到imp會(huì)進(jìn)行消息轉(zhuǎn)發(fā)來通知該方法峭拘,可是在源碼中并沒有找到該部分代碼俊庇,那么應(yīng)該怎么分析它具體的執(zhí)行過程呢?

第一個(gè)比較容易想到的是通過反編譯查看底層實(shí)現(xiàn)鸡挠。因?yàn)樯蠈犹O果沒有給我們提供源碼實(shí)現(xiàn)辉饱,可是在編譯時(shí)肯定是有的,我們通過反編譯就可以看到這個(gè)過程拣展。

還有一種方式就是查看在崩潰之前都執(zhí)行了哪些方法彭沼,處在動(dòng)態(tài)方法解析之后、報(bào)錯(cuò)方法之前的方法就是消息轉(zhuǎn)發(fā)的方法瞎惫。

6.1 hopper反編譯的分析

反編譯的使用溜腐,比較難看懂,如果再進(jìn)行詳細(xì)的介紹瓜喇,博客就太大了...挺益,因此我這里只進(jìn)行簡單的介紹,更詳細(xì)的反編譯的使用后面會(huì)寫博客分析乘寒。

一般查看源碼是查看如何將上層代碼編譯為底層代碼的望众,也就是進(jìn)行匯編后的效果,如果底層代碼無法查看伞辛,就只能反匯編烂翰,從底層代碼編成上層代碼。

Hopper和IDA是一個(gè)可以幫助我們靜態(tài)分析可視性文件的工具蚤氏,可以將可執(zhí)行文件反匯編成偽代碼甘耿、控制流程圖等。這里使用Hopper竿滨。

【第一步】:先拿到鏡像文件

  • image list指令可以獲取到所有的鏡像文件路徑


    鏡像文件路徑.png
  • 在路徑中獲取到鏡像文件


    鏡像文件.png

【第二步】:使用Hopper Disassembler打開鏡像文件

反匯編界面.png

  • 全局搜索一下自己想要查找的方法佳恬,會(huì)進(jìn)入這個(gè)界面
  • 接下來就可以通過方法來查找。

6.2 通過instrumentObjcMessageSends方式打印發(fā)送消息的日志

instrumentObjcMessageSends可以用來打印方法調(diào)用的信息于游,所以我們可以使用它來查看方法的調(diào)用過程毁葱,是否包含消息轉(zhuǎn)發(fā)。

6.2.1 打開objcMsgLogEnabled開關(guān)

通過lookUpImpOrForward --> log_and_fill_cache --> logMessageSend,在logMessageSend源碼下方找到instrumentObjcMessageSends的源碼實(shí)現(xiàn)贰剥,所以倾剿,在main中調(diào)用
instrumentObjcMessageSends打印方法調(diào)用的日志信息。

代碼:

extern void instrumentObjcMessageSends(BOOL flag);

int main(int argc, const char * argv[]) {
    @autoreleasepool {

        LGPerson *person = [LGPerson alloc];
        instrumentObjcMessageSends(YES);
        [person sayHello];
        instrumentObjcMessageSends(NO);
        NSLog(@"Hello, World!");
    }
    return 0;
}
  • 需要將外部引用的導(dǎo)入進(jìn)來蚌成,否則會(huì)報(bào)錯(cuò)前痘,這樣就告訴編譯器到其他文件中加載這個(gè)文件
  • 打開開關(guān)也就是設(shè)置為YES
  • 打開后還需要設(shè)置為NO凛捏,避免影響其他地方

6.2.1 運(yùn)行代碼,并前往/tmp/msgSends 目錄

消息發(fā)送日志路徑.png
  • 通過logMessageSend源碼际度,了解到消息發(fā)送打印信息存儲(chǔ)在/tmp/msgSends 目錄
  • 一次在運(yùn)行后葵袭,就可以前往這個(gè)目錄查找日志文件

6.2.1 查看日志文件

日志.png
  • 兩次動(dòng)態(tài)方法決議:resolveInstanceMethod方法
  • 兩次消息快速轉(zhuǎn)發(fā):forwardingTargetForSelector方法
  • 兩次消息慢速轉(zhuǎn)發(fā):methodSignatureForSelector + resolveInvocation

6.3 消息接受者重定向

在WYCat中創(chuàng)建有eat方法,并帶有實(shí)現(xiàn)乖菱。在WYPerson中并沒有這個(gè)方法坡锡,我們通過WYPerson來調(diào)用,看看是否可以將消息接收者重定向到cat窒所。

WYCat源碼:

@interface WYCat : NSObject

@property (nonatomic ,assign ,readonly) int age;
@property (nonatomic, copy,readwrite) NSString *name;

- (void)eat;

@end

@implementation WYCat

- (void)eat{
    NSLog(@"大家好鹉勒,雖然我是cat,但我是被Person調(diào)用的");
}

@end

WYPerson源碼:


@interface WYPerson : NSObject

- (void) runtimeTest;
- (void)mehtodDynamically;//沒有方法實(shí)現(xiàn)
- (void)eat;
- (void)getCatProperty;
- (void) msgSendSuperTest;
@end

@implementation WYPerson
//返回接受者對象
- (id)forwardingTargetForSelector:(SEL)aSelector{
    if (aSelector == @selector(eat)) {
        return [[WYCat alloc]init];
    }
    return [super forwardingTargetForSelector:aSelector];
//    return nil;
}

@end

main調(diào)用

//消息接收者重定向
WYPerson *person = [WYPerson alloc];
[person eat];

運(yùn)行結(jié)果:

2021-10-17 15:12:28.041183+0800 消息發(fā)送[85460:1551709] 大家好吵取,雖然我是cat禽额,但我是被Person調(diào)用的

總結(jié):

可以看到當(dāng)我們調(diào)用WYPerson的eat方法時(shí),發(fā)現(xiàn)它并沒有eat的函數(shù)實(shí)現(xiàn)皮官。但是我們可以通過消息接收者重定向脯倒,判斷當(dāng)前方法是eat時(shí),改變消息接受者為WYCat的對象捺氢,這樣就會(huì)讓eat來執(zhí)行了藻丢。

6.4 消息重定向

在WYPerson類中不實(shí)現(xiàn)eat方法,并且消息接受者重定向方法中返回nil摄乒。我們在消息重定向中改變選擇器指向或者消息接收者指向悠反。

WYPerson的函數(shù)實(shí)現(xiàn):

/*
 返回一個(gè)方法簽名對象,表示這個(gè)函數(shù)的返回值類型和參數(shù)類型
 */
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    if ([NSStringFromSelector(aSelector) isEqualToString:@"eat"]) {
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }

    return [super methodSignatureForSelector:aSelector];
}

//forwardInvoWYCation通知當(dāng)前對象馍佑,并將NSInvoWYCation消息傳遞過來
/*
 有兩個(gè)要點(diǎn):
    1斋否、決定消息接收者
    2、轉(zhuǎn)發(fā)
 */
- (void)forwardInvocation:(NSInvocation *)anInvocation{
    //1拭荤、消息接收者

    WYCat *cat = [[WYCat alloc] init];
    //anInvoWYCation表示消息茵臭,獲取該消息的方法選擇器
    if ([cat respondsToSelector:[anInvocation selector]]) {
        //重新指向接受者并發(fā)出消息
//        [anInvocation invokeWithTarget:cat];
        
        //重新指向消息接收者
//        anInvocation.target = cat;
        
        //重新指向選擇器
        anInvocation.selector = @selector(forwardInvocationTest);
        //發(fā)出消息
        [anInvocation invoke];
    } else {
        [super forwardInvocation:anInvocation];
    }

}

- (void)forwardInvocationTest{
    NSLog(@"大家好,我是一個(gè)消息重定向的的實(shí)現(xiàn)函數(shù)舅世,如果找不到eat函數(shù)笼恰,就會(huì)執(zhí)行我");
}

運(yùn)行結(jié)果:

2021-10-17 15:25:12.703920+0800 消息發(fā)送[86077:1569274] 大家好,我是一個(gè)消息重定向的的實(shí)現(xiàn)函數(shù)歇终,如果找不到eat函數(shù),就會(huì)執(zhí)行我

注意:

  • 在消息接受者重新中更改消息逼龟,NSInvocation就代表消息评凝,進(jìn)入到該類中可以查看到我們可以更改哪些內(nèi)容。
  • 經(jīng)查看發(fā)現(xiàn)我們可以更改的消息內(nèi)容只有target腺律、selector兩種奕短,分別表示消息接收者宜肉、方法選擇器。
  • NSMethodSignature是只讀的翎碑,我們無法更改谬返,必須在methodSignatureForSelector方法中設(shè)置。
@interface NSInvocation : NSObject

+ (NSInvocation *)invocationWithMethodSignature:(NSMethodSignature *)sig;

@property (readonly, retain) NSMethodSignature *methodSignature;

- (void)retainArguments;
@property (readonly) BOOL argumentsRetained;

@property (nullable, assign) id target;
@property SEL selector;

- (void)getReturnValue:(void *)retLoc;
- (void)setReturnValue:(void *)retLoc;

- (void)getArgument:(void *)argumentLocation atIndex:(NSInteger)idx;
- (void)setArgument:(void *)argumentLocation atIndex:(NSInteger)idx;

- (void)invoke;
- (void)invokeWithTarget:(id)target;

@end
  • 需要注意的是methodSignature必須要與函數(shù)的真實(shí)方法簽名一致日杈,否則不匹配將仍然找不到該函數(shù)

下面的代碼經(jīng)驗(yàn)證遣铝,確實(shí)仍然會(huì)提示找不到,因?yàn)椴黄ヅ?/p>

- (void)forwardInvocationTest:(NSString *)abc{
    NSLog(@"大家好莉擒,我是一個(gè)消息重定向的的實(shí)現(xiàn)函數(shù)酿炸,如果找不到eat函數(shù),就會(huì)執(zhí)行我");
}
  • methodSignatureForSelector是一定要寫的涨冀,需要先設(shè)置方法簽名

6.5 總結(jié)

  1. 消息轉(zhuǎn)發(fā)的流程不屬于消息發(fā)送填硕,只是在消息發(fā)送失敗后向消息接受者發(fā)送一個(gè)通知,我們在這個(gè)通知中改變消息的選擇器或接受者鹿鳖,以此來達(dá)到消息的二次發(fā)送扁眯。
  2. 消息接收者重定向只能修改消息接受者,轉(zhuǎn)發(fā)給另一個(gè)對象來執(zhí)行同名函數(shù)
  3. 消息重定向可以修改消息接收者和方法選擇器翅帜,也就是可以轉(zhuǎn)發(fā)給另一個(gè)對象的某個(gè)方法
  4. 消息接收者重定向必須要先通過methodSignatureForSelector設(shè)置方法簽名姻檀。

7、報(bào)錯(cuò)函數(shù)認(rèn)識(shí)

消息發(fā)送流程的lookUpImpOrForward函數(shù)中藕甩,我們看到如果快速查找施敢、慢速查找、動(dòng)態(tài)方法解析均沒有成功狭莱,則返回一個(gè)轉(zhuǎn)發(fā)函數(shù)僵娃,該函數(shù)為_objc_msgForward_impcache。它就是用來報(bào)錯(cuò)的腋妙,因此我們就開始分析它默怨,以此來查看消息發(fā)送失敗后最后的操作。

7.1 找到報(bào)錯(cuò)函數(shù)

這里看到報(bào)錯(cuò)函數(shù)為_objc_msgForward_impcache

//方法轉(zhuǎn)發(fā)(報(bào)錯(cuò)方法)
const IMP forward_imp = (IMP)_objc_msgForward_impcache;

7.2 匯編查找

  • 先全局搜索_objc_msgForward_impcache骤素,發(fā)現(xiàn)在匯編中
  • 執(zhí)行到__objc_msgForward
  • __objc_msgForward里執(zhí)行的是__objc_forward_handler匙睹,所以接下來找__objc_forward_handler
匯編查找.png

7.3 __objc_forward_handler的查找

全局搜索,沒有在匯編中查到济竹,猜想可能是在C語言中痕檬,因此在源碼中去掉一個(gè)下劃線進(jìn)行全局搜索_objc_forward_handler,發(fā)現(xiàn)最終的報(bào)錯(cuò)函數(shù)是objc_defaultForwardHandler送浊。

查找到源碼如下:

void *_objc_forward_handler = (void*)objc_defaultForwardHandler;

7.4 報(bào)錯(cuò)函數(shù)objc_defaultForwardHandler分析

查看報(bào)錯(cuò)函數(shù)的代碼梦谜,發(fā)現(xiàn)就是我們平常執(zhí)行方法時(shí)找不到函數(shù)所報(bào)的錯(cuò),終于找到頭了。消息發(fā)送結(jié)束唁桩。

objc_defaultForwardHandler源碼

// Default forward handler halts the process.
__attribute__((noreturn, cold)) void
objc_defaultForwardHandler(id self, SEL sel)
{
    _objc_fatal("%c[%s %s]: unrecognized selector sent to instance %p "
                "(no message forward handler is installed)", 
                class_isMetaClass(object_getClass(self)) ? '+' : '-', 
                object_getClassName(self), sel_getName(sel), self);
}

8闭树、總結(jié)

我們從上層方法調(diào)用,通過Clang查看底層實(shí)現(xiàn)荒澡,發(fā)現(xiàn)底層是通過objc_msgSend進(jìn)行消息發(fā)送报辱。之后通過objc_msgSend逐步探索,在匯編中查找cache的查找過程单山,在lookUpImpOrForward查找方法列表和動(dòng)態(tài)方法解析的過程碍现,之后通過官方文檔和打印日志發(fā)現(xiàn)了消息轉(zhuǎn)發(fā)對消息的二次挽救,最后找到了我們常見的報(bào)錯(cuò)函數(shù)饥侵,至此我們調(diào)用一次方法所經(jīng)歷的所有內(nèi)容宣告結(jié)束鸵赫。

09-方法調(diào)用流程.png
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市躏升,隨后出現(xiàn)的幾起案子辩棒,更是在濱河造成了極大的恐慌,老刑警劉巖膨疏,帶你破解...
    沈念sama閱讀 211,290評論 6 491
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件一睁,死亡現(xiàn)場離奇詭異,居然都是意外死亡佃却,警方通過查閱死者的電腦和手機(jī)者吁,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,107評論 2 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來饲帅,“玉大人复凳,你說我怎么就攤上這事≡畋茫” “怎么了育八?”我有些...
    開封第一講書人閱讀 156,872評論 0 347
  • 文/不壞的土叔 我叫張陵,是天一觀的道長赦邻。 經(jīng)常有香客問我,道長惶洲,這世上最難降的妖魔是什么按声? 我笑而不...
    開封第一講書人閱讀 56,415評論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮恬吕,結(jié)果婚禮上签则,老公的妹妹穿的比我還像新娘。我一直安慰自己铐料,他們只是感情好怀愧,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,453評論 6 385
  • 文/花漫 我一把揭開白布侨颈。 她就那樣靜靜地躺著,像睡著了一般芯义。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上妻柒,一...
    開封第一講書人閱讀 49,784評論 1 290
  • 那天扛拨,我揣著相機(jī)與錄音,去河邊找鬼举塔。 笑死绑警,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的央渣。 我是一名探鬼主播计盒,決...
    沈念sama閱讀 38,927評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼芽丹!你這毒婦竟也來了北启?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,691評論 0 266
  • 序言:老撾萬榮一對情侶失蹤拔第,失蹤者是張志新(化名)和其女友劉穎咕村,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體蚊俺,經(jīng)...
    沈念sama閱讀 44,137評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡懈涛,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,472評論 2 326
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了泳猬。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片批钠。...
    茶點(diǎn)故事閱讀 38,622評論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情揖盘,我是刑警寧澤砾层,帶...
    沈念sama閱讀 34,289評論 4 329
  • 正文 年R本政府宣布,位于F島的核電站势就,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜洋腮,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,887評論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望手形。 院中可真熱鬧啥供,春花似錦、人聲如沸库糠。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,741評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至贷屎,卻和暖如春罢防,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背唉侄。 一陣腳步聲響...
    開封第一講書人閱讀 31,977評論 1 265
  • 我被黑心中介騙來泰國打工咒吐, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人属划。 一個(gè)月前我還...
    沈念sama閱讀 46,316評論 2 360
  • 正文 我出身青樓恬叹,卻偏偏與公主長得像,于是被迫代替她去往敵國和親同眯。 傳聞我的和親對象是個(gè)殘疾皇子绽昼,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,490評論 2 348

推薦閱讀更多精彩內(nèi)容