在前面兩篇文章iOS- 消息流程之快速查找和iOS- 消息流程之慢速查找中,分別分析了
objc_msgSend
的快速查找
和慢速查找
在這兩種都沒找到方法實現的情況下,蘋果給了兩個建議
-
動態方法決議
:慢速查找流程未找到后,會執行一次動態方法決議 -
消息轉發
:如果動態方法決議仍然沒有找到實現,則進行消息轉發
如果這兩個建議都沒有做任何操作,就會報我們日常開發中常見的方法未實現
的崩潰報錯
,其步驟如下
-
定義LGPerson類,其中
say666
實例方法 和sayNB
類方法均沒有實現 -
main
中 分別調用LGPerson的實例方法say666
和類方法sayNB
,運行程序,均會報錯
,提示方法未實現,如下所示 -
調用
類方法sayNB
的報錯結果image.png
方法未實現報錯源碼
根據慢速查找
的源碼,我們發現,其報錯最后都是走到__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
- 匯編實現中查找
__objc_forward_handler
,并沒有找到,在源碼中去掉一個下劃線
進行全局搜索_objc_forward_handler
,有如下實現,本質是調用的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;
下面,我們來講講如何在崩潰前,如何操作,可以防止方法未實現的崩潰。
三次方法查找的挽救機會
根據蘋果的兩個建議,我們一共有三次挽救的機會:
【第一次機會】
動態方法決議
-
消息轉發流程
- 【第二次機會】
快速轉發
- 【第三次機會】
慢速轉發
- 【第二次機會】
【第一次機會】動態方法決議
在慢速查找
流程未找到
方法實現時,首先會嘗試一次動態方法決議
,其源碼實現如下:
static NEVER_INLINE IMP
resolveMethod_locked(id inst, SEL sel, Class cls, int behavior)
{
runtimeLock.assertLocked();
ASSERT(cls->isRealized());
runtimeLock.unlock();
//對象 -- 類
if (! cls->isMetaClass()) { //類不是元類,調用對象的解析方法
// try [cls resolveInstanceMethod:sel]
resolveInstanceMethod(inst, sel, cls);
}
else {//如果是元類,調用類的解析方法, 類 -- 元類
// try [nonMetaClass resolveClassMethod:sel]
// and [cls resolveInstanceMethod:sel]
resolveClassMethod(inst, sel, cls);
//為什么要有這行代碼? -- 類方法在元類中是對象方法,所以還是需要查詢元類中對象方法的動態方法決議
if (!lookUpImpOrNil(inst, sel, cls)) { //如果沒有找到或者為空,在元類的對象方法解析方法中查找
resolveInstanceMethod(inst, sel, cls);
}
}
// chances are that calling the resolver have populated the cache
// so attempt using it
//如果方法解析中將其實現指向其他方法,則繼續走方法查找流程
return lookUpImpOrForward(inst, sel, cls, behavior | LOOKUP_CACHE);
}
主要分為以下幾步
- 判斷類是否是元類
- 如果是
類
,執行實例方法的動態方法決議resolveInstanceMethod
- 如果是
元類
,執行類方法的動態方法決議resolveClassMethod
,如果在元類中沒有找到
或者為空
,則在元類的實例方法的動態方法決議resolveInstanceMethod
中查找
,主要是因為類方法在元類中是實例方法,所以還需要查找元類中實例方法的動態方法決議
- 如果是
- 如果
動態方法決議
中,將其實現指向了其他方法,則繼續查找指定的imp
,即繼續慢速查找lookUpImpOrForward
流程
實例方法
針對實例方法
調用,在快速-慢速查找
均沒有找到
實例方法的實現時,我們有一次挽救的機會,即嘗試一次動態方法決議
,由于是實例方法,所以會走到resolveInstanceMethod
方法,其源碼如下
static void resolveInstanceMethod(id inst, SEL sel, Class cls)
{
runtimeLock.assertUnlocked();
ASSERT(cls->isRealized());
SEL resolve_sel = @selector(resolveInstanceMethod:);
// look的是 resolveInstanceMethod --相當于是發送消息前的容錯處理
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); //發送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));
}
}
}
主要分為以下幾個步驟:
- 在發送
resolveInstanceMethod
消息前,需要查找cls
類中是否有該方法的實現,即通過lookUpImpOrNil
方法又會進入lookUpImpOrForward
慢速查找流程查找resolveInstanceMethod
方法- 如果
沒有
,則直接返回 - 如果
有
,則發送resolveInstanceMethod
消息
- 如果
- 再次
慢速查找
實例方法的實現,即通過lookUpImpOrNil
方法又會進入lookUpImpOrForward
慢速查找流程查找實例方法
崩潰修改
所以,針對實例方法say666
未實現的報錯崩潰,可以通過在類中重寫resolveInstanceMethod
類方法,并將其指向其他方法
的實現,即在LGPerson中重寫resolveInstanceMethod
類方法,將實例方法say666
的實現指向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的實現指向sayMaster
return class_addMethod(self, sel, imp, type);
}
return [super resolveInstanceMethod:sel];
}
重新運行,其打印結果如下- 【第一次動態決議】第一次的“來了”是在查找
say666
方法時會進入動態方法決議
- 【第二次動態決議】第二次“來了”是在慢速轉發流程中調用了
CoreFoundation
框架中的NSObject(NSObject) methodSignatureForSelector:
后,會再次進入動態決議
類方法
針對類方法,與實例方法類似,同樣可以通過重寫resolveClassMethod
類方法來解決前文的崩潰問題,即在LGPerson類中重寫該方法,并將sayNB
類方法的實現指向類方法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
類方法的重寫需要注意一點,傳入的cls
不再是類
,而是元類
,可以通過objc_getMetaClass
方法獲取類的元類,原因是因為類方法在元類中是實例方法
優化
上面的這種方式是單獨在每個類中重寫,有沒有更好的,一勞永逸的方法呢?其實通過方法慢速查找流程可以發現其查找路徑有兩條
- 實例方法:
類 -- 父類 -- 根類 -- nil
- 類方法:
元類 -- 根元類 -- 根類 -- nil
它們的共同點是如果前面沒找到
,都會來到根類即NSObject
中查找,所以我們是否可以將上述的兩個方法統一整合在一起呢?答案是可以的,可以通過NSObject
添加分類
的方式來實現統一處理,而且由于類方法的查找,在其繼承鏈
,查找的也是實例方法
,所以可以將實例方法
和類方法
的統一處理放在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;
}
當然,上面這種寫法還是會有其他的問題,比如系統方法也會被更改
,針對這一點,是可以優化
的,即我們可以針對自定義類中方法統一方法名的前綴
,根據前綴來判斷是否是自定義方法
,然后統一處理自定義方法,例如可以在崩潰前pop到首頁,主要是用于app線上防崩潰的處理,提升用戶的體驗
。
消息轉發流程
在慢速查找
的流程中,我們了解到,如果快速+慢速
沒有找到方法實現,動態方法決議也不行,就使用消息轉發
,但是,我們找遍了源碼也沒有發現消息轉發的相關源碼,可以通過以下方式來了解,方法調用崩潰前都走了哪些方法
通過
instrumentObjcMessageSends
方式打印發送消息的日志通過
hopper/IDA
反編譯
通過instrumentObjcMessageSends
通過lookUpImpOrForward --> log_and_fill_cache --> logMessageSend
,在logMessageSend
源碼下方找到instrumentObjcMessageSends
的源碼實現,所以,在main
中調用
instrumentObjcMessageSends
打印方法調用的日志信息,有以下兩點準備工作
1、打開 objcMsgLogEnabled
開關,即調用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
源碼,了解到消息發送打印信息存儲在目錄,如下所示
/tmp/msgSends
image.png
運行代碼,并前往/tmp/msgSends
目錄,發現有msgSends
開頭的日志文件,打開發現在崩潰前,執行了以下方法
- 兩次動態方法決議:
resolveInstanceMethod
方法 - 兩次消息快速轉發:
forwardingTargetForSelector
方法 - 兩次消息慢速轉發:
methodSignatureForSelector + resolveInstanceMethod
消息轉發的處理主要分為兩部分:
-
【快速轉發】當慢速查找,以及動態方法決議均沒有找到實現時,進行消息轉發,首先是進行
快速消息轉發
,即走到forwardingTargetForSelector
方法如果返回
消息接收者
,在消息接收者中還是沒有找到,則進入另一個方法的查找流程如果返回
nil
,則進入慢速消息轉發
-
【慢速轉發】執行到
methodSignatureForSelector
方法如果返回的方法簽名為
nil
,則直接崩潰報錯
如果返回的方法簽名
不為nil
,走到forwardInvocation
方法中,對invocation
事務進行處理,如果不處理也不會報錯
【第二次機會】快速轉發
針對前文的崩潰問題,如果動態方法決議也沒有找到實現,則需要在LGPerson中重寫forwardingTargetForSelector
方法,將LGPerson
的實例方法的接收者指定為LGStudent
的對象(LGStudent類中有say666的具體實現),如下所示
- (id)forwardingTargetForSelector:(SEL)aSelector{
NSLog(@"%s - %@",__func__,NSStringFromSelector(aSelector));
// runtime + aSelector + addMethod + imp
//將消息的接收者指定為LGStudent,在LGStudent中查找say666的實現
return [LGStudent alloc];
}
【第三次機會】慢速轉發
針對第二次機會即快速轉發中還是沒有找到,則進入最后的一次
挽救機會,即在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
事務,如下所示,修改invocation
的target
為[LGStudent alloc],調用[anInvocation invoke]
觸發 即LGPerson類的say666實例方法的調用會調用LGStudent的say666方法
- (void)forwardInvocation:(NSInvocation *)anInvocation{
NSLog(@"%s - %@",__func__,anInvocation);
anInvocation.target = [LGStudent alloc];
[anInvocation invoke];
}
所以,由上述可知,無論在forwardInvocation
方法中是否處理invocation
事務,程序都不會崩潰。
動態方法決議為什么執行兩次?
上帝視角的探索
在慢速查找流程中,我們了解到resolveInstanceMethod
方法的執行是通過lookUpImpOrForward --> resolveMethod_locked --> resolveInstanceMethod
來到resolveInstanceMethod
源碼,在源碼中通過發送消息觸發,如下所示resolve_sel
所以可以在resolveInstanceMethod
方法中IMP imp = lookUpImpOrNil(inst, sel, cls);
處加一個斷點,通過bt打印堆棧信息來看到底發生了什么
-
運行程序,直到第一次“來了”,通過
bt
查看第一次動態方法決議的堆棧信息,此時的sel是say666
-
繼續往下執行,直到第二次“來了”打印,查看堆棧信息,在第二次中,我們可以看到是通過
CoreFoundation
的-[NSObject(NSObject) methodSignatureForSelector:]
方法,然后通過class_getInstanceMethod
再次進入動態方法決議 -
通過上一步的堆棧信息,我們需要去看看
CoreFoundation
中到底做了什么?通過Hopper
反匯編CoreFoundation
的可執行文件,查看methodSignatureForSelector
方法的偽代碼 -
通過
methodSignatureForSelector
偽代碼進入___methodDescriptionForSelector
的實現 -
進入
___methodDescriptionForSelector
的偽代碼實現,結合匯編的堆棧打印,可以看到,在___methodDescriptionForSelecto
r這個方法中調用了objc4-781
的class_getInstanceMethod
-
在objc中的源碼中搜索
class_getInstanceMethod
,其源碼實現如下所示
這一點可以通過代碼調試來驗證,如下所示,在class_getInstanceMethod
方法處加一個斷點
,在執行了methodSignatureForSelector
方法后,返回了簽名,說明方法簽名是生效的,蘋果在走到invocation
之前,給了開發者一次機會再去查詢
,所以走到class_getInstanceMethod
這里,又去走了一遍方法查詢say666,然后會再次走到動態方法決議
所以,上述的分析也印證了前文中resolveInstanceMethod
方法執行了兩次的原因
經過上面的論證,我們了解到其實在慢速小子轉發流程中,在methodSignatureForSelector
和 forwardInvocation
方法之間還有一次動態方法決議
,即蘋果再次給的一個機會,如下圖所示
總結
到目前為止,objc_msgSend
發送消息的流程就分析完成了,在這里簡單總結下
【快速查找流程】
首先,在類的緩存cache
中查找指定方法的實現【慢速查找流程】
如果緩存中沒有找到,則在類的方法列表
中查找,如果還是沒找到,則去父類鏈的緩存和方法列表
中查找【動態方法決議】
如果慢速查找還是沒有找到時,第一次補救機會
就是嘗試一次動態方法決議
,即重寫resolveInstanceMethod
/resolveClassMethod
方法【消息轉發】
如果動態方法決議還是沒有找到,則進行消息轉發
,消息轉發中有兩次補救機會:快速轉發+慢速轉發
如果轉發之后也沒有,則程序直接報錯崩潰
unrecognized selector sent to instance