在前面兩篇文章iOS- 消息流程之快速查找和iOS- 消息流程之慢速查找中厅篓,分別分析了
objc_msgSend
的快速查找
和慢速查找
在這兩種都沒找到方法實(shí)現(xiàn)的情況下饲漾,蘋果給了兩個(gè)建議
-
動態(tài)方法決議
:慢速查找流程未找到后修陡,會執(zhí)行一次動態(tài)方法決議 -
消息轉(zhuǎn)發(fā)
:如果動態(tài)方法決議仍然沒有找到實(shí)現(xiàn)押桃,則進(jìn)行消息轉(zhuǎn)發(fā)
如果這兩個(gè)建議都沒有做任何操作兔乞,就會報(bào)我們?nèi)粘i_發(fā)中常見的方法未實(shí)現(xiàn)
的崩潰報(bào)錯(cuò)
半夷,其步驟如下
-
定義LGPerson類机杜,其中
say666
實(shí)例方法 和sayNB
類方法均沒有實(shí)現(xiàn) -
main
中 分別調(diào)用LGPerson的實(shí)例方法say666
和類方法sayNB
客情,運(yùn)行程序其弊,均會報(bào)錯(cuò)
,提示方法未實(shí)現(xiàn)膀斋,如下所示 -
調(diào)用
類方法sayNB
的報(bào)錯(cuò)結(jié)果image.png
方法未實(shí)現(xiàn)報(bào)錯(cuò)源碼
根據(jù)慢速查找
的源碼梭伐,我們發(fā)現(xiàn),其報(bào)錯(cuò)最后都是走到__objc_msgForward_impcache
方法仰担,以下是報(bào)錯(cuò)流程的源碼
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
- 匯編實(shí)現(xiàn)中查找
__objc_forward_handler
糊识,并沒有找到,在源碼中去掉一個(gè)下劃線
進(jìn)行全局搜索_objc_forward_handler
摔蓝,有如下實(shí)現(xiàn)赂苗,本質(zhì)是調(diào)用的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);
}
void *_objc_forward_handler = (void*)objc_defaultForwardHandler;
下面,我們來講講如何在崩潰前贮尉,如何操作拌滋,可以防止方法未實(shí)現(xiàn)的崩潰。
三次方法查找的挽救機(jī)會
根據(jù)蘋果的兩個(gè)建議绘盟,我們一共有三次挽救的機(jī)會:
【第一次機(jī)會】
動態(tài)方法決議
-
消息轉(zhuǎn)發(fā)流程
- 【第二次機(jī)會】
快速轉(zhuǎn)發(fā)
- 【第三次機(jī)會】
慢速轉(zhuǎn)發(fā)
- 【第二次機(jī)會】
【第一次機(jī)會】動態(tài)方法決議
在慢速查找
流程未找到
方法實(shí)現(xiàn)時(shí)鸠真,首先會嘗試一次動態(tài)方法決議
,其源碼實(shí)現(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
//如果方法解析中將其實(shí)現(xiàn)指向其他方法路操,則繼續(xù)走方法查找流程
return lookUpImpOrForward(inst, sel, cls, behavior | LOOKUP_CACHE);
}
主要分為以下幾步
- 判斷類是否是元類
- 如果是
類
疾渴,執(zhí)行實(shí)例方法的動態(tài)方法決議resolveInstanceMethod
- 如果是
元類
,執(zhí)行類方法的動態(tài)方法決議resolveClassMethod
屯仗,如果在元類中沒有找到
或者為空
搞坝,則在元類的實(shí)例方法的動態(tài)方法決議resolveInstanceMethod
中查找
,主要是因?yàn)?code>類方法在元類中是實(shí)例方法魁袜,所以還需要查找元類中實(shí)例方法的動態(tài)方法決議
- 如果是
- 如果
動態(tài)方法決議
中桩撮,將其實(shí)現(xiàn)指向了其他方法,則繼續(xù)查找指定的imp
峰弹,即繼續(xù)慢速查找lookUpImpOrForward
流程
實(shí)例方法
針對實(shí)例方法
調(diào)用店量,在快速-慢速查找
均沒有找到
實(shí)例方法的實(shí)現(xiàn)時(shí),我們有一次挽救的機(jī)會鞠呈,即嘗試一次動態(tài)方法決議
融师,由于是實(shí)例方法,所以會走到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ā)送消息前的容錯(cuò)處理
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));
}
}
}
主要分為以下幾個(gè)步驟:
- 在發(fā)送
resolveInstanceMethod
消息前旱爆,需要查找cls
類中是否有該方法的實(shí)現(xiàn),即通過lookUpImpOrNil
方法又會進(jìn)入lookUpImpOrForward
慢速查找流程查找resolveInstanceMethod
方法- 如果
沒有
灭将,則直接返回 - 如果
有
疼鸟,則發(fā)送resolveInstanceMethod
消息
- 如果
- 再次
慢速查找
實(shí)例方法的實(shí)現(xiàn),即通過lookUpImpOrNil
方法又會進(jìn)入lookUpImpOrForward
慢速查找流程查找實(shí)例方法
崩潰修改
所以庙曙,針對實(shí)例方法say666
未實(shí)現(xiàn)的報(bào)錯(cuò)崩潰空镜,可以通過在類中重寫resolveInstanceMethod
類方法,并將其指向其他方法
的實(shí)現(xiàn)捌朴,即在LGPerson中重寫resolveInstanceMethod
類方法吴攒,將實(shí)例方法say666
的實(shí)現(xiàn)指向sayMaster
方法實(shí)現(xiàn),如下所示
+ (BOOL)resolveInstanceMethod:(SEL)sel{
if (sel == @selector(say666)) {
NSLog(@"%@ 來了", NSStringFromSelector(sel));
//獲取sayMaster方法的imp
IMP imp = class_getMethodImplementation(self, @selector(sayMaster));
//獲取sayMaster的實(shí)例方法
Method sayMethod = class_getInstanceMethod(self, @selector(sayMaster));
//獲取sayMaster的豐富簽名
const char *type = method_getTypeEncoding(sayMethod);
//將sel的實(shí)現(xiàn)指向sayMaster
return class_addMethod(self, sel, imp, type);
}
return [super resolveInstanceMethod:sel];
}
重新運(yùn)行砂蔽,其打印結(jié)果如下- 【第一次動態(tài)決議】第一次的“來了”是在查找
say666
方法時(shí)會進(jìn)入動態(tài)方法決議
- 【第二次動態(tài)決議】第二次“來了”是在慢速轉(zhuǎn)發(fā)流程中調(diào)用了
CoreFoundation
框架中的NSObject(NSObject) methodSignatureForSelector:
后洼怔,會再次進(jìn)入動態(tài)決議
類方法
針對類方法,與實(shí)例方法類似左驾,同樣可以通過重寫resolveClassMethod
類方法來解決前文的崩潰問題镣隶,即在LGPerson類中重寫該方法极谊,并將sayNB
類方法的實(shí)現(xiàn)指向類方法lgClassMethod
+ (BOOL)resolveClassMethod:(SEL)sel{
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 [super resolveClassMethod:sel];
}
resolveClassMethod
類方法的重寫需要注意一點(diǎn),傳入的cls
不再是類
安岂,而是元類
轻猖,可以通過objc_getMetaClass
方法獲取類的元類,原因是因?yàn)轭惙椒ㄔ谠愔惺菍?shí)例方法
優(yōu)化
上面的這種方式是單獨(dú)在每個(gè)類中重寫域那,有沒有更好的咙边,一勞永逸的方法呢膀估?其實(shí)通過方法慢速查找流程可以發(fā)現(xiàn)其查找路徑有兩條
- 實(shí)例方法:
類 -- 父類 -- 根類 -- nil
- 類方法:
元類 -- 根元類 -- 根類 -- nil
它們的共同點(diǎn)是如果前面沒找到
揭措,都會來到根類即NSObject
中查找,所以我們是否可以將上述的兩個(gè)方法統(tǒng)一整合在一起呢砰嘁?答案是可以的淑蔚,可以通過NSObject
添加分類
的方式來實(shí)現(xiàn)統(tǒng)一處理市殷,而且由于類方法的查找,在其繼承鏈
刹衫,查找的也是實(shí)例方法
被丧,所以可以將實(shí)例方法
和類方法
的統(tǒng)一處理放在resolveInstanceMethod
方法中,如下所示
+ (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āng)然,上面這種寫法還是會有其他的問題绪妹,比如系統(tǒng)方法也會被更改
甥桂,針對這一點(diǎn),是可以優(yōu)化
的邮旷,即我們可以針對自定義類中方法統(tǒng)一方法名的前綴
黄选,根據(jù)前綴來判斷是否是自定義方法
,然后統(tǒng)一處理自定義方法婶肩,例如可以在崩潰前pop到首頁办陷,主要是用于app線上防崩潰的處理,提升用戶的體驗(yàn)
律歼。
消息轉(zhuǎn)發(fā)流程
在慢速查找
的流程中民镜,我們了解到,如果快速+慢速
沒有找到方法實(shí)現(xiàn)险毁,動態(tài)方法決議也不行制圈,就使用消息轉(zhuǎn)發(fā)
跷跪,但是,我們找遍了源碼也沒有發(fā)現(xiàn)消息轉(zhuǎn)發(fā)的相關(guān)源碼甘磨,可以通過以下方式來了解庵朝,方法調(diào)用崩潰前都走了哪些方法
通過
instrumentObjcMessageSends
方式打印發(fā)送消息的日志通過
hopper/IDA
反編譯
通過instrumentObjcMessageSends
通過lookUpImpOrForward --> log_and_fill_cache --> logMessageSend
,在logMessageSend
源碼下方找到instrumentObjcMessageSends
的源碼實(shí)現(xiàn)侄旬,所以汰蜘,在main
中調(diào)用
instrumentObjcMessageSends
打印方法調(diào)用的日志信息色难,有以下兩點(diǎn)準(zhǔn)備工作
1尺迂、打開 objcMsgLogEnabled
開關(guān)州疾,即調(diào)用instrumentObjcMessageSends
方法時(shí),傳入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ā)送打印信息存儲在目錄师溅,如下所示
/tmp/msgSends
image.png
運(yùn)行代碼窖维,并前往/tmp/msgSends
目錄判沟,發(fā)現(xiàn)有msgSends
開頭的日志文件挪哄,打開發(fā)現(xiàn)在崩潰前,執(zhí)行了以下方法
- 兩次動態(tài)方法決議:
resolveInstanceMethod
方法 - 兩次消息快速轉(zhuǎn)發(fā):
forwardingTargetForSelector
方法 - 兩次消息慢速轉(zhuǎn)發(fā):
methodSignatureForSelector + resolveInstanceMethod
消息轉(zhuǎn)發(fā)的處理主要分為兩部分:
-
【快速轉(zhuǎn)發(fā)】當(dāng)慢速查找,以及動態(tài)方法決議均沒有找到實(shí)現(xiàn)時(shí)证舟,進(jìn)行消息轉(zhuǎn)發(fā),首先是進(jìn)行
快速消息轉(zhuǎn)發(fā)
窗骑,即走到forwardingTargetForSelector
方法如果返回
消息接收者
抵知,在消息接收者中還是沒有找到,則進(jìn)入另一個(gè)方法的查找流程如果返回
nil
软族,則進(jìn)入慢速消息轉(zhuǎn)發(fā)
-
【慢速轉(zhuǎn)發(fā)】執(zhí)行到
methodSignatureForSelector
方法如果返回的方法簽名為
nil
刷喜,則直接崩潰報(bào)錯(cuò)
如果返回的方法簽名
不為nil
,走到forwardInvocation
方法中立砸,對invocation
事務(wù)進(jìn)行處理掖疮,如果不處理也不會報(bào)錯(cuò)
【第二次機(jī)會】快速轉(zhuǎn)發(fā)
針對前文的崩潰問題,如果動態(tài)方法決議也沒有找到實(shí)現(xiàn)颗祝,則需要在LGPerson中重寫forwardingTargetForSelector
方法浊闪,將LGPerson
的實(shí)例方法的接收者指定為LGStudent
的對象(LGStudent類中有say666的具體實(shí)現(xiàn)),如下所示
- (id)forwardingTargetForSelector:(SEL)aSelector{
NSLog(@"%s - %@",__func__,NSStringFromSelector(aSelector));
// runtime + aSelector + addMethod + imp
//將消息的接收者指定為LGStudent螺戳,在LGStudent中查找say666的實(shí)現(xiàn)
return [LGStudent alloc];
}
【第三次機(jī)會】慢速轉(zhuǎn)發(fā)
針對第二次機(jī)會即快速轉(zhuǎn)發(fā)中還是沒有找到搁宾,則進(jìn)入最后的一次
挽救機(jī)會,即在LGPerson中重寫methodSignatureForSelector
倔幼,如下所示
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
NSLog(@"%s - %@",__func__,NSStringFromSelector(aSelector));
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation{
NSLog(@"%s - %@",__func__,anInvocation);
}
forwardInvocation
也可以處理invocation
事務(wù)盖腿,如下所示,修改invocation
的target
為[LGStudent alloc]损同,調(diào)用[anInvocation invoke]
觸發(fā) 即LGPerson類的say666實(shí)例方法的調(diào)用會調(diào)用LGStudent的say666方法
- (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ā)送消息觸發(fā)富俄,如下所示resolve_sel
所以可以在resolveInstanceMethod
方法中IMP imp = lookUpImpOrNil(inst, sel, cls);
處加一個(gè)斷點(diǎn),通過bt打印堆棧信息來看到底發(fā)生了什么
-
運(yùn)行程序而咆,直到第一次“來了”霍比,通過
bt
查看第一次動態(tài)方法決議的堆棧信息,此時(shí)的sel是say666
-
繼續(xù)往下執(zhí)行暴备,直到第二次“來了”打印悠瞬,查看堆棧信息,在第二次中涯捻,我們可以看到是通過
CoreFoundation
的-[NSObject(NSObject) methodSignatureForSelector:]
方法浅妆,然后通過class_getInstanceMethod
再次進(jìn)入動態(tài)方法決議 -
通過上一步的堆棧信息,我們需要去看看
CoreFoundation
中到底做了什么障癌?通過Hopper
反匯編CoreFoundation
的可執(zhí)行文件凌外,查看methodSignatureForSelector
方法的偽代碼 -
通過
methodSignatureForSelector
偽代碼進(jìn)入___methodDescriptionForSelector
的實(shí)現(xiàn) -
進(jìn)入
___methodDescriptionForSelector
的偽代碼實(shí)現(xiàn),結(jié)合匯編的堆棧打印涛浙,可以看到康辑,在___methodDescriptionForSelecto
r這個(gè)方法中調(diào)用了objc4-781
的class_getInstanceMethod
-
在objc中的源碼中搜索
class_getInstanceMethod
,其源碼實(shí)現(xiàn)如下所示
這一點(diǎn)可以通過代碼調(diào)試來驗(yàn)證轿亮,如下所示疮薇,在class_getInstanceMethod
方法處加一個(gè)斷點(diǎn)
,在執(zhí)行了methodSignatureForSelector
方法后我注,返回了簽名惦辛,說明方法簽名是生效的,蘋果在走到invocation
之前仓手,給了開發(fā)者一次機(jī)會再去查詢
胖齐,所以走到class_getInstanceMethod
這里,又去走了一遍方法查詢say666,然后會再次走到動態(tài)方法決議
所以嗽冒,上述的分析也印證了前文中resolveInstanceMethod
方法執(zhí)行了兩次的原因
經(jīng)過上面的論證呀伙,我們了解到其實(shí)在慢速小子轉(zhuǎn)發(fā)流程中,在methodSignatureForSelector
和 forwardInvocation
方法之間還有一次動態(tài)方法決議
添坊,即蘋果再次給的一個(gè)機(jī)會剿另,如下圖所示
總結(jié)
到目前為止,objc_msgSend
發(fā)送消息的流程就分析完成了,在這里簡單總結(jié)下
【快速查找流程】
首先雨女,在類的緩存cache
中查找指定方法的實(shí)現(xiàn)【慢速查找流程】
如果緩存中沒有找到谚攒,則在類的方法列表
中查找,如果還是沒找到氛堕,則去父類鏈的緩存和方法列表
中查找【動態(tài)方法決議】
如果慢速查找還是沒有找到時(shí)馏臭,第一次補(bǔ)救機(jī)會
就是嘗試一次動態(tài)方法決議
,即重寫resolveInstanceMethod
/resolveClassMethod
方法【消息轉(zhuǎn)發(fā)】
如果動態(tài)方法決議還是沒有找到讼稚,則進(jìn)行消息轉(zhuǎn)發(fā)
括儒,消息轉(zhuǎn)發(fā)中有兩次補(bǔ)救機(jī)會:快速轉(zhuǎn)發(fā)+慢速轉(zhuǎn)發(fā)
如果轉(zhuǎn)發(fā)之后也沒有,則程序直接報(bào)錯(cuò)崩潰
unrecognized selector sent to instance