分析對象進(jìn)行方法調(diào)用在底層的執(zhí)行過程州邢,包括快速查找流程儡陨、慢速查找流程、動(dòng)態(tài)方法解析量淌、消息轉(zhuǎn)發(fā)骗村,以及最后查詢失敗的報(bào)錯(cuò)方法。
主要內(nèi)容:
- 快速查找流程
- 慢速查找流程
- 動(dòng)態(tài)方法解析
- 消息轉(zhuǎn)發(fā)(消息接收者重定向呀枢,消息重定向)
- 查詢失敗提示
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)方法解析只說是這樣的,但是為什么是這樣的并沒有說殴穴。
- 先從方法上層調(diào)用入手劲够,通過Clang查看底層是如何實(shí)現(xiàn)的震桶。經(jīng)查看發(fā)現(xiàn)是objc_msgSend來實(shí)現(xiàn)的
- 通過objc_msgSend在源碼中查看啊鸭,在匯編中找到了objc_msgSend的實(shí)現(xiàn)過程岳掐,探索匯編發(fā)現(xiàn)是進(jìn)行cache的查找杜跷。
- cache查找結(jié)束后會(huì)進(jìn)入到一個(gè)lookUpImpOrForward方法样屠,之后在源碼C語言中查找到lookUpImpOrForward的實(shí)現(xiàn)汪拥,探索發(fā)現(xiàn)其內(nèi)部是在進(jìn)行方法列表的查找鸭蛙。
- 方法的查找后如果沒有查找到茴扁,發(fā)現(xiàn)會(huì)進(jìn)入到一個(gè)resolveMethod_locked方法翩概,由此開啟動(dòng)態(tài)方法解析的過程凫岖。
- 方法查找失敗后江咳,會(huì)得到一個(gè)報(bào)錯(cuò)函數(shù)_objc_msgForward_impcache,繼續(xù)探索發(fā)現(xiàn)就是我們常見的方法調(diào)用失敗后的報(bào)錯(cuò)信息哥放。
- 在動(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ā)送惰赋。
發(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荆姆,找到了入口
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)、寄存器的變化過程矫渔,后面有看的不好理解的地方可以對照這幅圖來看彤蔽。
- 開始查詢,得到Class
- 通過self的isa獲取到該對象的Class
- 得到buckets
- 通過Class獲取到類中的cache
- 得到cache中的buckets和mask
- 哈希算法查找
- 通過mask和方法選擇_cmd進(jìn)行哈希計(jì)算得到bucket所在的地址值
- 取出bucket中的imp
- 哈希沖突算法查找
- 如果發(fā)生沖突庙洼,開始哈希沖突算法
- 先以當(dāng)前的bucket不斷向前查找顿痪,判斷是否是我們需要的bucket
- 如果一直找到第一個(gè)bucket,仍然沒有找到油够,則跳轉(zhuǎn)到最后一個(gè)bucket蚁袭,繼續(xù)向前查找
- 一直查找到第一個(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
匯編語句解讀:
- ENTRY _objc_msgSend
- 此處開始進(jìn)入到msgSend流程
- 以后看到ENTRY就說明了一個(gè)流程的開始
- UNWIND _objc_msgSend, NoFrame
- 表示沒有視圖
- cmp p0, #0
- 判斷p0的值是否為空
- cmp表示比較
- p0是self,此處存儲(chǔ)的是消息接受者。這是因?yàn)楫?dāng)傳入的時(shí)候只恨,第一個(gè)參數(shù)就是消息接受者
- 當(dāng)函數(shù)返回時(shí)译仗,p0是返回值
- if SUPPORT_TAGGED_POINTERS
- 是否支持小對象類型
- b.le LNilOrTagged
- b表示進(jìn)行跳轉(zhuǎn)
- b.le是小于判斷,也就是小于的時(shí)候開始執(zhí)行LNilOrTagged
- b.eq LReturnZero
- b.eq判斷是等于的是執(zhí)行
- 如果為0官觅,則直接退出
- ldr p13, [x0]
- 將isa保存到了p13中
- 剛才所看到的p0是消息接受者纵菌,x0就是這個(gè)p1寄存器所存儲(chǔ)的內(nèi)容,類的第一個(gè)屬性就是isa
- ldr表示將x0值傳遞到p13上
- GetClassFromIsa_p16 p13
- 將Class信息保存到p16
- 它的作用是取出isa中保存的Class 信息并保存到p16寄存器中
- 也就是p16 = isa(p13) & ISA_MASK
- 此處是相當(dāng)于函數(shù)調(diào)用休涤,在匯編中是一個(gè)宏定義
- CacheLookup NORMAL, _objc_msgSend
- 開始緩存查找流程
代碼邏輯:
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
匯編語句解讀:
- .macro CacheLookup
- 此處表示CacheLookup的定義
- 以后方法的定義可以通過.macro來查找
- ldr p11, [x16, #CACHE]
- 將Class信息偏移16個(gè)字節(jié)税朴,就獲取到了cache
- 將緩存信息cache存入到p11寄存器中,p11 = mask|buckets家制,p11就是cache
- 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 )
- and p10, p11, #0x0000ffffffffffff
- 得到buckets,存儲(chǔ)到p10
- p10 = mask|buckets & #0x0000ffffffffffff
- buckets占有后48位(真正數(shù)據(jù)是44位)
- and表示與運(yùn)算,將與后的值保存到p10寄存器
- and p12, p1, p11, LSR #48
- p12是哈希地址涵但,通過哈希算法得到杈绸,x12 = _cmd & mask
- LSR表示邏輯向右偏移,p11, LSR #48表示maskAndBuckets向左平移48位矮瘟,也就是得到mask
- and p12,p1,p11,LSR #48表示_cmd &mask并保存到p12
- add p12, p10, p12, LSL #(1+PTRSHIFT)
- 得到計(jì)算出的bucket并存放到p12中
- PTRSHIFT經(jīng)全局搜索發(fā)現(xiàn)是3瞳脓,LSL #(1+PTRSHIFT)表示邏輯左移4位,也就是*16
- p10此時(shí)是第一個(gè)bucket的地址澈侠,需要通過地址平移來得到下標(biāo)所在的bucket劫侧。
- 而平移的地址大小就是坐標(biāo)*16(因?yàn)閎ucket包含sel和imp,所以是16個(gè)字節(jié),所以需要乘以16)
- 所以此處是將首地址移動(dòng)下標(biāo)*16的位數(shù)就得到了計(jì)算出的bucket烧栋,并且放到p12中
- ldp p17, p9, [x12]
- 將imp和sel分別存儲(chǔ)到p17和p9
- cmp p9, p1
- 比較bucket中的sel與傳入的_cmd是否相等
- cmp p12, p10
- 比較bucket與buckets第一個(gè)bucket是否一致写妥,也就是是否是第一個(gè)bucket
- ldp p17, p9, [x12, #-BUCKET_SIZE]!
- 將bucket的地址向前平移一個(gè)bucket的大小
- 也就是得到前一個(gè)bucket,并且繼續(xù)將sel和imp存儲(chǔ)到p17和p9中
- BUCKET_SIZE經(jīng)全局搜索發(fā)現(xiàn)是一個(gè)bucket的大小
- add p12, p12, p11, LSR #(48 - (1+PTRSHIFT))
- 如果判斷是第一個(gè)bucket就挪到最后一個(gè)位置
- 將cache向右平移44位审姓,也就是mask向左平移4位耳标,也就是mask*16
- 移動(dòng)mask個(gè)bucket,所以是移動(dòng)到了最后一位邑跪,這是因?yàn)閙ask=容量-1,所以mask就是最后一個(gè)位置次坡,再乘上16,就是最后一個(gè)位置到第一個(gè)位置的地址大小了
- 經(jīng)查詢PTRSHIFT為4
- 這里也就是說明了為什么要把maskAndBuckets中間留下4位的0画畅,就是為了在此處更好的計(jì)算
代碼邏輯:
- 將Class從首地址內(nèi)存平移16位得到cache砸琅,并存入到p11中
- 將cache進(jìn)行掩碼計(jì)算得到buckets,并存入到p10中
- 將maskAndBuckets右移48位得到mask與上_cmd得到哈希地址,并存入到p12中(哈希算法)
- 通過buckets平移到這個(gè)下標(biāo)地址得到目標(biāo)bucket轴踱,并存入到p12中
- 將目標(biāo)bucket的imp和sel分別存入到p17和p9中
- 判斷當(dāng)前的bucket->sel 症脂!= _cmd
- 如果查找到,則獲取到imp返回
- 如果查找不到淫僻,就開始第一輪循環(huán)遍歷(哈希沖突算法)
- 判斷bucket是否是第一個(gè)元素诱篷,如果不是,則向前移動(dòng)一位雳灵,再進(jìn)行比較
- 如果是第一個(gè)元素棕所,則跳轉(zhuǎn)到最后一個(gè)元素,開啟第二輪循環(huán)循環(huán)向前移動(dòng)一位進(jìn)行比較悯辙。
- 如果第二輪循環(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
3.3 總結(jié)分析
代碼執(zhí)行流程:
寄存器的存儲(chǔ)示意圖:
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
這里的后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é)
- 在消息發(fā)送時(shí),先在本類的cache中查找是否存在附井,謂之快速查找
- 先通過接受者對象得到Class讨越,再查看Class中的cache,之后拿到cache中的bucket的sel與傳入的_cmd進(jìn)行比較永毅,如果存在則返回imp把跨。
- 這里查找bucket的過程涉及到哈希算法,先通過哈希算法_cmd &mask得到哈希地址的bucket
- 如果不存在沼死,可能會(huì)是哈希沖突之后的地址着逐,就先向前查找,如果找到第一個(gè)位置還沒找到,就從最后一個(gè)向前查找耸别,一直找到第一個(gè)元素健芭,最終沒找到就開始慢速查找了
- 簡單的邏輯就是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;
}
代碼分析:
- 前期準(zhǔn)備
- 判斷cache_t中是否已經(jīng)有該sel和imp
- 判斷是否是一個(gè)已知的類:判斷當(dāng)前類是否是已經(jīng)被認(rèn)可的類午绳,即已經(jīng)加載的類(類加載時(shí)會(huì)分析)
- 將該類的繼承鏈和該類的元類繼承鏈的相關(guān)類都實(shí)現(xiàn)一遍置侍,方便后續(xù)進(jìn)行父類的查找、類方法的查找(類加載時(shí)會(huì)分析)
- 判斷該類是否初始化拦焚,如果沒有則進(jìn)行初始化蜡坊, 包括該類的繼承鏈和該類的元類的繼承鏈都進(jìn)行初始化
- 方法查詢
- 得到sel對應(yīng)的Method
- 如果存在,直接拿到IMP
- 判斷父類如果為空赎败,則開始報(bào)錯(cuò)秕衙,如果父類存在,則查找父類的方法
- 父類方法查詢
- 此處又開始在匯編中查找父類的cache僵刮,需要在匯編中查找
- 父類的報(bào)錯(cuò)方法
- 查找到IMP存在据忘,則繼續(xù)執(zhí)行,存儲(chǔ)到當(dāng)前類的cache中
- 方法動(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)的
示意圖:
小結(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)
- 比較的只是中間位置瓶堕,前后位置不需要比較隘道,而中間位置是通過起始位置和總數(shù)來進(jìn)行計(jì)算的
- 總數(shù)的計(jì)算,總數(shù)每次都要變郎笆,也就是除以2谭梗,向右位移1位
- 起始位置的計(jì)算
- 如果實(shí)際位置在中間位置右側(cè),則需要改變起始位置题画,也就是中間位置probe+1,因?yàn)榘裵robe也減去了默辨,所以總數(shù)還要先減一,后面再去除以2.
- 如果實(shí)際位置在中間位置左側(cè)苍息,就不需要改變
- 這里還有一個(gè)比較不好想象的小細(xì)節(jié)缩幸,就是如果沒有中間位置,比如中間只有偶位數(shù)竞思,中間位置何去何從呢
- count>>1,這個(gè)是簡單計(jì)算一下就會(huì)發(fā)現(xiàn)最后一位是會(huì)抹去的表谊,也就是說會(huì)向下取整
- 也就是如果是偶位數(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;
}
}
說明:
- 簡單的循環(huán)流程
- 先查找當(dāng)前類的methodList
- 再查找父類的cache
- 再查找父類的methodList
- cache_getImp方法是通過匯編_cache_getImp實(shí)現(xiàn)荧琼,傳入的$0 是 GETIMP
- 如果父類緩存中找到了方法實(shí)現(xiàn),則跳轉(zhuǎn)至CacheHit即命中,則直接返回imp
- 如果在父類緩存中命锄,沒有找到方法實(shí)現(xiàn)堰乔,則跳轉(zhuǎn)至CheckMiss 或者 JumpMiss,通過判斷$0 跳轉(zhuǎn)至LGetImpMiss累舷,直接返回nil
流程示意圖:
小結(jié):
- 當(dāng)前類查詢methodList沒有找到后浩考,會(huì)先查找父類的cache
- 如果父類的cache沒有找到夹孔,就查找父類的方法列表
- 如果父類仍然沒有找到被盈,繼續(xù)找父類,一直到父類為nil搭伤,當(dāng)父類為nil時(shí)只怎,說明當(dāng)前類是NSObject類。也就是找到頭了怜俐,此時(shí)會(huì)退出開始開始進(jìn)行動(dòng)態(tài)方法解析身堡。
- 父類查找到方法后會(huì)存儲(chǔ)在當(dāng)前類的cache中。
4.4 總結(jié)
- 慢速查找流程先查找類的方法列表拍鲤,再查找父類的cache和方法列表贴谎,一直找到NSObject類仍然沒有找到就開始動(dòng)態(tài)方法解析,解析完成后會(huì)再次查找方法列表季稳。如果仍然沒有找到擅这,就返回forward_imp,也就是報(bào)錯(cuò)函數(shù)景鼠。如果找到就存儲(chǔ)到當(dāng)前類的cache中仲翎。
- 方法列表的查找是通過二分查找法來實(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)方法解析。(
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);
}
說明:
- 上邊代碼中如果通過break退出循環(huán)時(shí),就會(huì)開始進(jìn)入到動(dòng)態(tài)方法解析五辽。上文已經(jīng)分析過眯牧,如果是沒有查找到方法就會(huì)break蹋岩。所以是當(dāng)沒有找到方法后開始進(jìn)入動(dòng)態(tài)方法解析
- 一個(gè)類每次進(jìn)行慢速查找后只會(huì)執(zhí)行一次動(dòng)態(tài)方法解析
- 通過behavior來判斷,避免動(dòng)態(tài)方法解析之后去查詢imp時(shí)再次進(jìn)入学少,造成死循環(huán)
- 這里的behavior異或完之后變成了xx0x剪个,之后作為參數(shù)傳入,而在resolveMethod_locked中會(huì)再次調(diào)用lookUpImpOrForward中將behavior作為參數(shù)傳入版确,所以再進(jìn)入到該方法中不會(huì)再進(jìn)入到動(dòng)態(tài)解析了扣囊。
- 最終通過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);
}
說明:
- 如果是傳入的類不是元類樱衷,則調(diào)用resolveInstanceMethod()來對實(shí)例方法進(jìn)行解析
- 如果是傳入的類是元類,則調(diào)用resolveClassMethod()來對類方法進(jìn)行解析
- 解析完成后酒唉,會(huì)再次調(diào)用lookUpImpOrForward再次進(jìn)行查詢方法矩桂。可以看到這里的behavior為xx0x痪伦,所以本次的查詢是不會(huì)進(jìn)入到動(dòng)態(tài)方法解析中了侄榴。
- 使用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
說明:
- 根據(jù)源碼注釋滥搭,也通過resolveMethod_locked函數(shù)中均可以看到resolveInstanceMethod傳入的可能是類酸纲,也可能是元類,也就是它也可以進(jìn)行類方法的動(dòng)態(tài)解析论熙。
- 會(huì)先執(zhí)行一下resolveInstanceMethod方法福青。
- 執(zhí)行后就再通過lookUpImpOrNil查詢一遍imp,注意此時(shí)直接就查詢脓诡,而不會(huì)看resolveInstanceMethod執(zhí)行返回的是否是YES无午。
- resolveInstanceMethod方法的返回值只是為了打印,并沒有其他作用祝谚,真正起作用的就是看到底有沒有給這個(gè)sel增加imp
- lookUpImpOrNil函數(shù)中調(diào)用lookUpImpOrForward函數(shù)傳入的behavior是0011宪迟,所以并不會(huì)再次進(jìn)行動(dòng)態(tài)方法解析了。
- 因此我們只需要在這個(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
說明:
- 這里傳入的cls只是元類捐寥,不會(huì)是類
- 針對類方法進(jìn)行動(dòng)態(tài)方法解析
- 內(nèi)部會(huì)執(zhí)行resolveClassMethod方法
- 通過nonmeta = getMaybeUnrealizedNonMetaClass(cls, inst);獲取到元類的初始類笤昨。
- 可以看到調(diào)用類方法,objc_msgSend函數(shù)中傳遞的接受者參數(shù)也必須是類握恳,而不能是元類瞒窒,雖然方法存儲(chǔ)在元類中,但是消息接受者仍然是類乡洼。
- 執(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é)
- 慢速查找會(huì)查找當(dāng)前類的方法列表泉褐,如果方法列表也不存在,則開始查詢父類的cache和方法列表
- 慢速查找過程:先查找當(dāng)前類的方法列表鸟蜡,之后再查找父類的cache和父類的方法列表膜赃,一直到NSObject還沒有查找到就開始進(jìn)行動(dòng)態(tài)方法解析,解析完成后會(huì)再次查找方法列表揉忘。如果仍然沒有找到跳座,就返回forward_imp,也就是報(bào)錯(cuò)函數(shù)
- 在方法列表中通過sel查找imp是通過二分查找來獲取的
- 動(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指令可以獲取到所有的鏡像文件路徑
-
在路徑中獲取到鏡像文件
【第二步】:使用Hopper Disassembler打開鏡像文件
- 全局搜索一下自己想要查找的方法佳恬,會(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 目錄
- 通過logMessageSend源碼际度,了解到消息發(fā)送打印信息存儲(chǔ)在/tmp/msgSends 目錄
- 一次在運(yùn)行后葵袭,就可以前往這個(gè)目錄查找日志文件
6.2.1 查看日志文件
- 兩次動(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é)
- 消息轉(zhuǎn)發(fā)的流程不屬于消息發(fā)送填硕,只是在消息發(fā)送失敗后向消息接受者發(fā)送一個(gè)通知,我們在這個(gè)通知中改變消息的選擇器或接受者鹿鳖,以此來達(dá)到消息的二次發(fā)送扁眯。
- 消息接收者重定向只能修改消息接受者,轉(zhuǎn)發(fā)給另一個(gè)對象來執(zhí)行同名函數(shù)
- 消息重定向可以修改消息接收者和方法選擇器翅帜,也就是可以轉(zhuǎn)發(fā)給另一個(gè)對象的某個(gè)方法
- 消息接收者重定向必須要先通過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
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é)束鸵赫。