在前兩篇文章objc_msgSend流程之快速查找和objc_msgSend流程之慢速查找分析了objc_msgSend
的快速查找
和慢速查找
,當(dāng)前面這兩種方式都沒找到對應(yīng)的方法實現(xiàn)
時焊虏,我們可以通過操作下面兩個方法來避免方法未實現(xiàn)
的奔潰報錯
-
動態(tài)方法決議
:在慢速查找流程未找到后澎嚣,會執(zhí)行一次 -
消息轉(zhuǎn)發(fā)
:如果動態(tài)方法決議沒有找到實現(xiàn)褥琐,則進行消息轉(zhuǎn)發(fā)- 快速轉(zhuǎn)發(fā)
- 慢速轉(zhuǎn)發(fā)
實例方法報錯
類方法報錯
方法未實現(xiàn)報錯源碼
匯編__objc_msgForward_impcache
方法
STATIC_ENTRY __objc_msgForward_impcache
// No stret specialization.
b __objc_msgForward
END_ENTRY __objc_msgForward_impcache
//??
ENTRY __objc_msgForward
adrp x17, __objc_forward_handler@PAGE
ldr p17, [x17, __objc_forward_handler@PAGEOFF]
TailCallFunctionPointer x17
END_ENTRY __objc_msgForward
在匯編實現(xiàn)中查找_objc_forward_handler
方法
// 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);
}
void *_objc_forward_handler = (void*)objc_defaultForwardHandler;
objc_defaultForwardHandler
方法就是我們?nèi)粘i_發(fā)中常見錯誤沒有實現(xiàn)函數(shù)磕洪,運行程序,崩潰時報的錯誤提示
防止方法未實現(xiàn)崩潰的三次機會
- 【第一次機會】
動態(tài)方法決議
-
消息轉(zhuǎn)發(fā)流程
- 【第二次機會】
快速轉(zhuǎn)發(fā)
- 【第三次機會】
慢速轉(zhuǎn)發(fā)
- 【第二次機會】
【第一次機會】動態(tài)方法決議
在慢速查找
流程未找到
方法實現(xiàn)時谷异,首先嘗試一次動態(tài)方法決議
,源碼實現(xiàn)如下
static NEVER_INLINE IMP
resolveMethod_locked(id inst, SEL sel, Class cls, int behavior)
{
runtimeLock.assertLocked();
ASSERT(cls->isRealized());
runtimeLock.unlock();
//對象 -- 類
if (! cls->isMetaClass()) { //類不是元類尺上,調(diào)用對象的解析方法
// try [cls resolveInstanceMethod:sel]
resolveInstanceMethod(inst, sel, cls);
}
else {//如果是元類,調(diào)用類的解析方法构资, 類 -- 元類
// try [nonMetaClass resolveClassMethod:sel]
// and [cls resolveInstanceMethod:sel]
resolveClassMethod(inst, sel, cls);
//為什么要有這行代碼? -- 類方法在元類中是對象方法唉窃,所以還是需要查詢元類中對象方法的動態(tài)方法決議
if (!lookUpImpOrNil(inst, sel, cls)) { //如果沒有找到或者為空,在元類的對象方法解析方法中查找
resolveInstanceMethod(inst, sel, cls);
}
}
// chances are that calling the resolver have populated the cache
// so attempt using it
//如果方法解析中將其實現(xiàn)指向其他方法件已,則繼續(xù)走方法查找流程
return lookUpImpOrForward(inst, sel, cls, behavior | LOOKUP_CACHE);
}
- 判斷是否是元類
- 如果是
類
,執(zhí)行實例方法
的動態(tài)決議resolveInstanceMethod
- 如果是
元類
,執(zhí)行類方法
的動態(tài)方法決議resolveClassMethod
铜秆,如果元類中沒有找到
或者為空
,則在元類
的實例方法
的動態(tài)方法決議resolveInstanceMethod
中查找梅屉,是因為類方法在元類中是實例方法
,所以還需要查找元類中的實例方法的動態(tài)決議
- 如果是
- 如果
動態(tài)方法決議
中,將其實現(xiàn)指向了其他方法
搓幌,則繼續(xù)查找指定的imp
,即繼續(xù)慢速查找lookUpImpOrForward
流程
動態(tài)方法決議流程
實例方法的動態(tài)決議
實例方法
的調(diào)用拐揭,在快速查找和慢速查找均未找到實例方法
的實現(xiàn)時盟猖,我們還有一次挽救的機會值依,即嘗試動態(tài)方法決議
颇蜡,由于是實例方法
,所以會走到resolveInstanceMethod
方法领迈,源碼如下:
static void resolveInstanceMethod(id inst, SEL sel, Class cls)
{
runtimeLock.assertUnlocked();
ASSERT(cls->isRealized());
SEL resolve_sel = @selector(resolveInstanceMethod:);
// look的是 resolveInstanceMethod --相當(dāng)于是發(fā)送消息前的容錯處理
if (!lookUpImpOrNil(cls, resolve_sel, cls->ISA())) {
// Resolver not implemented.
return;
}
BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
bool resolved = msg(cls, resolve_sel, sel); //發(fā)送resolve_sel消息
// Cache the result (good or bad) so the resolver doesn't fire next time.
// +resolveInstanceMethod adds to self a.k.a. cls
//查找say666
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 {
// 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));
}
}
}
主要分為以下幾個步驟
- 在
發(fā)送resolveInstanceMethod 消息
前尘喝,需要查找cls類
中是否有改方法的實現(xiàn)
,即通過lookUpImpOrNil
方法又會進入lookUpImpOrForward
慢速查找流程查找resolveInstanceMethod
方法- 如果沒有,直接返回
- 如果有友题,則發(fā)送
resolveInstanceMethod
消息
- 再次慢速查找實例方法的實現(xiàn)嗤堰,即通過
lookUpImpOrNil
方法又會進入lookUpImpOrForward
慢速查找流程查找對應(yīng)的實例方法
奔潰修改
針對實例方法未實現(xiàn)
的奔潰報錯,我們可以通過在類
中重寫resolveInstanceMethod 類方法
咆爽,并將其指向其他方法的實現(xiàn)梁棠,即將實例方法say666
的實現(xiàn)指向sayMaster
+ (BOOL)resolveInstanceMethod:(SEL)sel{
if (sel == @selector(say666)) {
NSLog(@"%@ 來了", NSStringFromSelector(sel));
//獲取sayMaster方法的imp
IMP imp = class_getMethodImplementation(self, @selector(sayMaster));
//獲取sayMaster的實例方法
Method sayMethod = class_getInstanceMethod(self, @selector(sayMaster));
//獲取sayMaster的豐富簽名
const char *type = method_getTypeEncoding(sayMethod);
//將sel的實現(xiàn)指向sayMaster
return class_addMethod(self, sel, imp, type);
}
return [super resolveInstanceMethod:sel];
}
重新運行,并打印堆棧信息
- 【第一次動態(tài)決議】查找
say666
方法時進入動態(tài)方法決議
- 【第二次動態(tài)決議】在慢速轉(zhuǎn)發(fā)流程中調(diào)用了
CoreFoundation
框架中的NSObject(NSObject) methodSignatureForSelector:
后斗埂,會再次進入動態(tài)方法決議
第二次動態(tài)決議流程分析請看文末的問題探索
類方法的動態(tài)決議
針對類方法
的重寫resolveClassMethod
,需要注意傳入的cls
不再是類
,而是元類
实夹,因為類方法在元類中是實例方法
,可以通過objc_getMetaClass
方法獲取元類
優(yōu)化
- 實例方法:
類 -- 父類 -- 根類 -- nil
- 類方法:
元類 -- 根元類 -- 根類 -- nil
通過上面方法的查找路徑可以發(fā)現(xiàn)沦辙,都會來到根類(NSObject)
中查找
+ (BOOL)resolveInstanceMethod:(SEL)sel{
if (sel == @selector(say666)) {
NSLog(@"%@ 來了", NSStringFromSelector(sel));
IMP imp = class_getMethodImplementation(self, @selector(sayMaster));
Method sayMethod = class_getInstanceMethod(self, @selector(sayMaster));
const char *type = method_getTypeEncoding(sayMethod);
return class_addMethod(self, sel, imp, type);
}else if (sel == @selector(sayNB)) {
NSLog(@"%@ 來了", NSStringFromSelector(sel));
IMP imp = class_getMethodImplementation(objc_getMetaClass("LGPerson"), @selector(lgClassMethod));
Method lgClassMethod = class_getInstanceMethod(objc_getMetaClass("LGPerson"), @selector(lgClassMethod));
const char *type = method_getTypeEncoding(lgClassMethod);
return class_addMethod(objc_getMetaClass("LGPerson"), sel, imp, type);
}
return NO;
}
上面這種寫法會導(dǎo)致系統(tǒng)方法也會被更改
狞玛,針對這一點戴已,我們可以通過自定義類中方法的統(tǒng)一方法名前綴
拴疤,根據(jù)前綴來判斷是否是自定義方法荞膘,然后統(tǒng)一處理自定義方法
狭郑,例如可以在崩潰前pop到首頁黄绩,主要是用于app線上防崩潰的處理
袒餐,提升用戶的體驗
消息轉(zhuǎn)發(fā)流程
在源碼中找不到消息轉(zhuǎn)發(fā)的源碼匕积,但是我們可以通過下面方式來了解,方法調(diào)用奔潰前都走了那些方法
- 通過
instrumentObjcMessageSends
方式打印發(fā)送消息的日志 - 痛過
hopper/IDA反編譯
通過instrumentObjcMessageSends
- 通過
lookUpImpOrForward --> log_and_fill_cache --> logMessageSend
精居,在logMessageSend
源碼中找到instrumentObjcMessageSends
的源碼實現(xiàn)沃但,然后再main函數(shù)
中調(diào)用instrumentObjcMessageSends
打印方法調(diào)用的日志信息佛吓,
- 1灼伤、打開objcMsgLogEnabled
開關(guān),即調(diào)用instrumentObjcMessageSends
方法時,傳入YES
- 2可柿、在main
中通過extern
聲明instrumentObjcMessageSends
方法
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;
}
-
通過
logMessageSend
源碼鸠踪,可以發(fā)現(xiàn)消息發(fā)送打印信息存儲在/tmp/msgSends
目錄
消息發(fā)送日志路徑 -
運行代碼,并前往
/tmp/msgSends
目錄复斥,發(fā)現(xiàn)有msgSends
開頭的日志文件营密,可以發(fā)現(xiàn)在奔潰前,執(zhí)行了以下方法- 兩次
動態(tài)方法決議
:resolveInstanceMethod
方法 - 兩次
消息快速轉(zhuǎn)發(fā)
:forwardingTargetForSelector
方法 - 兩次
消息慢速轉(zhuǎn)發(fā)
:methodSignatureForSelector
+resolveInvocation
消息發(fā)送日志詳情
- 兩次
通過hopper/IDA反編譯
Hopper和IDA是一個可以幫助我們靜態(tài)分析可視性文件的工具目锭,可以將可執(zhí)行文件反匯編成偽代碼评汰,控制流程圖等纷捞,下面以Hopper為例
-
運行程序奔潰,查看堆棧信息
查看堆棧打印信息 -
發(fā)現(xiàn)
___forwarding___
來自CoreFoundation
框架
___forwarding___源碼定位 -
通過
image list
命令被去,讀取整個鏡像文件主儡,然后搜索CoreFoundation
,可以查看其可執(zhí)行文件的路徑
查找CoreFoundation -
通過文件路徑编振,找到
CoreFoundation
的執(zhí)行文件
CoreFoundation執(zhí)行文件路徑 -
打開
hopper
缀辩,選擇Try the Demo
,然后將CoreFoundation
的可執(zhí)行文件拖入hopper進行反編譯踪央,選擇x86(64 bits)
hopper反編譯
hoperr反編譯 -
反匯編后的界面
hopper界面 -
通過左側(cè)的搜索框搜索
__forwarding_prep_0___
后臀玄,選擇偽代碼
-
以下是
__forwarding_prep_0___
的匯編偽代碼,跳轉(zhuǎn)至___forwarding___
偽代碼___forwarding___ -
以下是
___forwarding___
的偽代碼實現(xiàn)畅蹂,首先查看是否實現(xiàn)forwardingTargetForSelector
方法健无,如果沒有響應(yīng),跳轉(zhuǎn)至loc_6459b
即快速轉(zhuǎn)發(fā)沒有響應(yīng)液斜,進入慢速轉(zhuǎn)發(fā)
流程
偽代碼-forwardingTargetForSelector -
跳轉(zhuǎn)至
loc_6459b
累贤,在其下方判斷是否響應(yīng)methodSignatureForSelector
方法
偽代碼-methodSignatureForSelector 如果
沒有響應(yīng)
,跳轉(zhuǎn)至loc_6490b
少漆,則直接報錯-
如果獲取
methodSignatureForSelector
的方法簽名
為nil臼膏,也是直接報錯
偽代碼-methodSignatureForSelector為nil時報錯
-
-
如果
methodSignatureForSelector
返回值不為空,則在forwardInvocation
方法中對invocation
進行處理
偽代碼-forwardInvocation
通過上面兩種查找方式可以驗證示损,消息轉(zhuǎn)發(fā)的方法有3個
【快速轉(zhuǎn)發(fā)】
forwardingTargetForSelector
-
【慢速轉(zhuǎn)發(fā)】
-methodSignatureForSelector
-forwardInvocation
所以消息轉(zhuǎn)發(fā)的整體流程圖如下
消息轉(zhuǎn)發(fā)流程圖 【快速轉(zhuǎn)發(fā)】當(dāng)慢速查找以及動態(tài)方法決議都沒找到渗磅,首先進行
快速消息轉(zhuǎn)發(fā)
,forwardingTargetForSelector
方法
- 如果返回消息接收者
检访,在消息接收者中還是沒有找到始鱼,則進入另一個方法的查找流程
- 如果返回nil
,則進入慢速消息轉(zhuǎn)發(fā)
-【慢速轉(zhuǎn)發(fā)】 執(zhí)行到methodSignatureForSelector
- 如果返回的方法簽名
為nil
脆贵,則直接崩潰報錯
- 如果返回的方法簽名不為nil
医清,走到forwardInvocation
方法中,對invocation
事務(wù)進行處理卖氨,不處理也不會報錯
【第二次機會】快速轉(zhuǎn)發(fā)
在LGPerson中重寫forwardingTargetForSelector
方法会烙,將LGPerson的實例方法的接收者指定為LGStudent
的對象(LGStudent類中有say666的具體實現(xiàn)),如下所示
- (id)forwardingTargetForSelector:(SEL)aSelector{
NSLog(@"%s - %@",__func__,NSStringFromSelector(aSelector));
// runtime + aSelector + addMethod + imp
//將消息的接收者指定為LGStudent双泪,在LGStudent中查找say666的實現(xiàn)
return [LGStudent alloc];
}
也可以直接不指定消息接收者持搜,直接調(diào)用父類的該方法
,如果還是沒有找到焙矛,則直接報錯
- (id)forwardingTargetForSelector:(SEL)aSelector{
NSLog(@"%s - %@",__func__,NSStringFromSelector(aSelector));
// runtime + aSelector + addMethod + imp
return [super forwardingTargetForSelector:aSelector];
}
【第三次機會】慢速轉(zhuǎn)發(fā)
如果消息快速轉(zhuǎn)發(fā)
還是沒有找到葫盼,則還有最后一次機會,即在LGPerson
中重寫methodSignatureForSelector
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
NSLog(@"%s - %@",__func__,NSStringFromSelector(aSelector));
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation{
NSLog(@"%s - %@",__func__,anInvocation);
}
也可以處理invocation事務(wù)
- (void)forwardInvocation:(NSInvocation *)anInvocation{
NSLog(@"%s - %@",__func__,anInvocation);
anInvocation.target = [LGStudent alloc];
[anInvocation invoke];
}
我們在forwardInvocation
方法中處理和不處理invocation事務(wù)
村斟,都不會報錯
“動態(tài)方法決議為什么執(zhí)行兩次贫导?” 探索
在慢速查找流程中抛猫,我們了解到resolveInstanceMethod
方法的執(zhí)行是通過lookUpImpOrForward --> resolveMethod_locked --> resolveInstanceMethod
來到resolveInstanceMethod
源碼,在源碼中通過發(fā)送resolve_sel
消息觸發(fā)
-
可以在
resolveInstanceMethod
方法中IMP imp = lookUpImpOrNil(inst, sel, cls)
;處加一個斷點孩灯,通過bt打印堆棧信息來看到底發(fā)生了什么
第一次動態(tài)方法決議堆棧信息 -
繼續(xù)往下執(zhí)行闺金,直到第二次“來了”打印,查看堆棧信息峰档,在第二次中败匹,我們可以看到是通過
CoreFoundation
的-[NSObject(NSObject) methodSignatureForSelector:]
方法,然后通過`class_getInstanceMethod再次進入動態(tài)方法決議
第二次動態(tài)方法決議堆棧信息 -
通過
Hopper
反匯編CoreFoundation
的可執(zhí)行文件讥巡,查看methodSignatureForSelector
方法的偽代碼
methodSignatureForSelector偽代碼進入方式 -
通過
methodSignatureForSelector
偽代碼進入___methodDescriptionForSelector
的實現(xiàn)
methodDescriptionForSelector方法的偽代碼 -
進入
___methodDescriptionForSelector
的偽代碼實現(xiàn)掀亩,結(jié)合匯編的堆棧打印,可以看到在___methodDescriptionForSelector
這個方法中調(diào)用了objc4-781
的class_getInstanceMethod
___methodDescriptionForSelector方法的偽代碼調(diào)用了class_getInstanceMethod -
在objc中的源碼中搜索
class_getInstanceMethod
欢顷,其源碼實現(xiàn)如下所示
class_getInstanceMethod方法源碼
如下所示槽棍,在class_getInstanceMethod
方法處加一個斷點,在執(zhí)行了methodSignatureForSelector
方法后抬驴,返回了簽名炼七,說明方法簽名是生效的,蘋果在走到invocation
之前布持,給了開發(fā)者一次機會再去查詢豌拙,所以走到class_getInstanceMethod
這里,又去走了一遍方法查詢say666,然后會再次走到動態(tài)方法決議
所以题暖,上述的分析也印證了前文中resolveInstanceMethod
方法執(zhí)行了兩次的原因
通過代碼來推導(dǎo)
-
LGPerson中重寫
resolveInstanceMethod
方法姆蘸,并加上class_addMethod
操作即賦值IMP
,此時resolveInstanceMethod
會走兩次嗎
resolveInstanceMethod方法調(diào)試驗證
【結(jié)論】:通過運行發(fā)現(xiàn)芙委,如果賦值了IMP,動態(tài)方法決議只會走一次
狂秦,說明不是在這里走第二次動態(tài)方法決議灌侣, -
去掉
resolveInstanceMethod
方法中的賦值IMP
,在LGPerson類中重寫forwardingTargetForSelector
方法裂问,并指定返回值為[LGStudent alloc]侧啼,重新運行,如果resolveInstanceMethod
打印了兩次堪簿,說明是在forwardingTargetForSelector
方法之前執(zhí)行了動態(tài)方法決議
痊乾,反之,在forwardingTargetForSelector
方法之后
forwardingTargetForSelector方法調(diào)試驗證
【結(jié)論】:發(fā)現(xiàn)resolveInstanceMethod
中的打印還是只打印了一次椭更,數(shù)排名第二次動態(tài)方法決議 在forwardingTargetForSelector
方法后
- 在LGPerson中重寫
methodSignatureForSelector
和forwardInvocation
哪审,運行
methodSignatureForSelector+forwardInvocation方法調(diào)試驗證
【結(jié)論】:第二次動態(tài)方法決議
在methodSignatureForSelector
和forwardInvocation
方法之間
經(jīng)過上面的論證,我們了解到其實在慢速轉(zhuǎn)發(fā)流程中虑瀑,在methodSignatureForSelector
和 forwardInvocation
方法之間還有一次動態(tài)方法決議
總結(jié)
objc_msgSend發(fā)送消息的流程
【快速查找流程】首先湿滓,在類的
緩存cache
中查找指定方法的實現(xiàn)【慢速查找流程】如果緩存中沒有找到滴须,則在
類的方法列表
中查找,如果還是沒找到叽奥,則去父類鏈的緩存和方法列表
中查找【動態(tài)方法決議】如果慢速查找還是沒有找到時扔水,
第一次
補救機會就是嘗試一次動態(tài)方法決議
,即重寫resolveInstanceMethod/resolveClassMethod
方法【消息轉(zhuǎn)發(fā)】如果動態(tài)方法決議還是沒有找到朝氓,則進行消息轉(zhuǎn)發(fā)魔市,消息轉(zhuǎn)發(fā)中有
兩次
補救機會:快速轉(zhuǎn)發(fā)+慢速轉(zhuǎn)發(fā)
如果轉(zhuǎn)發(fā)之后也沒有,則程序直接報錯崩潰
unrecognized selector sent to instance