引子
在前面兩篇文章iOS-底層原理 12:objc_msgSend流程分析之快速查找和iOS-底層原理 13:objc_msgSend流程分析之慢速查找中,分別分析了objc_msgSend
的快速查找
和慢速查找
,在這兩種都沒找到方法實現的情況下,蘋果給了兩個建議
-
動態方法決議
:慢速查找流程未找到后,會執行一次動態方法決議 -
消息轉發
:如果動態方法決議仍然沒有找到實現,則進行消息轉發
如果這兩個建議都沒有做任何操作,就會報我們日常開發中常見的方法未實現
的崩潰報錯
,其步驟如下
-
定義
LGPerson
類,其中say666
實例方法 和sayNB
類方法均沒有實現
自定義LGPerson類 -
main中 分別調用
LGPerson
的實例方法say666
和類方法sayNB
,運行程序,均會報錯
,提示方法未實現
,如下所示-
調用實例方法say666的報錯結果
實例方法報錯 -
調用類方法sayNB的報錯結果
類方法報錯
-
方法未實現報錯源碼
根據慢速查找
的源碼,我們發現,其報錯最后都是走到__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;
看著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];
}
重新運行,其打印結果如下
從結果中可以發現,resolveInstanceMethod
動態決議方法中“來了”打印了兩次,這是為什么呢?通過堆棧信息可以看出
- 【第一次動態決議】第一次的“來了”是在查找
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
目錄,如下所示
消息發送日志路徑 -
運行代碼,并前往
/tmp/msgSends
目錄,發現有msgSends
開頭的日志文件,打開發現在崩潰前,執行了以下方法- 兩次
動態方法決議
:resolveInstanceMethod
方法 - 兩次
消息快速轉發
:forwardingTargetForSelector
方法 - 兩次
消息慢速轉發
:methodSignatureForSelector
+resolveInvocation
消息發送日志詳情
- 兩次
通過hopper/IDA反編譯
Hopper和IDA是一個可以幫助我們靜態分析可視性文件的工具,可以將可執行文件反匯編成偽代碼、控制流程圖等,下面以Hopper為例(注:hopper高級版本是一款收費軟件,針對比較簡單的反匯編需求來說,demo版本足夠使用了)
-
運行程序崩潰,查看堆棧信息
查看堆棧打印信息 -
發現
___forwarding___
來自CoreFoundation
___forwarding___源碼定位 -
通過
image list
,讀取整個鏡像文件,然后搜索CoreFoundation
,查看其可執行文件的路徑
從鏡像文件中查找CoreFoundation -
通過文件路徑,找到
CoreFoundation
的可執行文件
查找CoreFoundation的可執行文件 -
打開
hopper
,選擇Try the Demo
,然后將上一步的可執行文件拖入hopper進行反匯編,選擇x86(64 bits)
hopper選擇Demo版本
hopper反匯編 -
以下是反匯編后的界面,主要使用上面的三個功能,分別是 匯編、流程圖、偽代碼
hoppper主要使用的三個功能 -
通過左側的搜索框搜索
__forwarding_prep_0___
,然后選擇偽代碼
-
以下是
__forwarding_prep_0___
的匯編偽代碼,跳轉至___forwarding___
偽代碼-___forwarding___ -
以下是
___forwarding___
的偽代碼實現,首先是查看是否實現forwardingTargetForSelector
方法,如果沒有響應,跳轉至loc_6459b
即快速轉發沒有響應,進入慢速轉發
流程,
偽代碼-forwardingTargetForSelector -
跳轉至
loc_6459b
,在其下方判斷是否響應methodSignatureForSelector
方法,
偽代碼-methodSignatureForSelector如果
沒有響應
,跳轉至loc_6490b
,則直接報錯-
如果獲取
methodSignatureForSelector
的方法簽名
為nil,也是直接報錯
偽代碼-methodSignatureForSelector為nil時報錯
-
-
如果
methodSignatureForSelector
返回值不為空,則在forwardInvocation
方法中對invocation
進行處理
偽代碼-forwardInvocation
所以,通過上面兩種查找方式可以驗證,消息轉發的方法有3個
- 【快速轉發】
forwardingTargetForSelector
- 【慢速轉發】
methodSignatureForSelector
forwardInvocation
所以,綜上所述,消息轉發整體的流程如下
消息轉發的處理主要分為兩部分:
- 【快速轉發】當慢速查找,以及動態方法決議均沒有找到實現時,進行消息轉發,首先是進行
快速消息轉發
,即走到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事務
,如下所示,修改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
打印堆棧信息
來看到底發生了什么
-
在
resolveInstanceMethod
方法中IMP imp = lookUpImpOrNil(inst, sel, cls);
處加一個斷點,運行程序,直到第一次“來了”
,通過bt查看第一次動態方法決議
的堆棧信息,此時的sel是say666
第一次動態方法決議堆棧信息 -
繼續往下執行,直到第二次
“來了”打印
,查看堆棧信息,在第二次中,我們可以看到是通過CoreFoundation
的-[NSObject(NSObject) methodSignatureForSelector:]
方法,然后通過class_getInstanceMethod
再次進入動態方法決議,
第二次動態方法決議堆棧信息 -
通過上一步的堆棧信息,我們需要去看看CoreFoundation中到底做了什么?通過
Hopper
反匯編CoreFoundation
的可執行文件,查看methodSignatureForSelector
方法的偽代碼
methodSignatureForSelector偽代碼進入方式 -
通過
methodSignatureForSelector
偽代碼進入___methodDescriptionForSelector
的實現
methodDescriptionForSelector方法的偽代碼 -
進入
___methodDescriptionForSelector
的偽代碼實現,結合匯編的堆棧打印,可以看到,在___methodDescriptionForSelector
這個方法中調用了objc4-781
的class_getInstanceMethod
___methodDescriptionForSelector方法的偽代碼調用了class_getInstanceMethod -
在objc中的源碼中搜索
class_getInstanceMethod
,其源碼實現如下所示
class_getInstanceMethod方法源碼
這一點可以通過代碼調試
來驗證,如下所示,在class_getInstanceMethod
方法處加一個斷點,在執行了methodSignatureForSelector
方法后,返回了簽名,說明方法簽名是生效的,蘋果在走到invocation
之前,給了開發者一次機會再去查詢
,所以走到class_getInstanceMethod
這里,又去走了一遍方法查詢say666
,然后會再次走到動態方法決議
所以,上述的分析也印證了前文中resolveInstanceMethod
方法執行了兩次的原因
無上帝視角的探索
如果在沒有上帝視角的情況下,我們也可以通過代碼
來推導
在哪里再次調用了動態方法決議
- LGPerson中重寫
resolveInstanceMethod
方法,并加上class_addMethod
操作即賦值IMP
,此時resolveInstanceMethod
會走兩次嗎?
resolveInstanceMethod方法調試驗證
【結論】:通過運行發現,如果賦值了IMP,動態方法決議只會走一次
,說明不是在這里走第二次動態方法決議,
繼續往下探索
-
去掉
resolveInstanceMethod
方法中的賦值IMP,在LGPerson
類中重寫forwardingTargetForSelector
方法,并指定返回值為[LGStudent alloc]
,重新運行,如果resolveInstanceMethod
打印了兩次,說明是在forwardingTargetForSelector
方法之前執行了 動態方法決議,反之,在forwardingTargetForSelector
方法之后
forwardingTargetForSelector方法調試驗證
【結論】:發現resolveInstanceMethod
中的打印還是只打印了一次,數排名第二次動態方法決議 在forwardingTargetForSelector
方法后 -
在LGPerson中重寫
methodSignatureForSelector
和forwardInvocation
,運行
methodSignatureForSelector+forwardInvocation方法調試驗證
【結論】:第二次動態方法決議
在methodSignatureForSelector
和forwardInvocation
方法之間
第二種分析同樣可以論證前文中resolveInstanceMethod
執行了兩次的原因
經過上面的論證,我們了解到其實在慢速小子轉發流程中,在methodSignatureForSelector
和 forwardInvocation
方法之間還有一次動態方法決議,即蘋果再次給的一個機會,如下圖所示
總結
到目前為止,objc_msgSend發送消息的流程就分析完成了,在這里簡單總結下
【快速查找流程】
首先,在類的緩存cache
中查找指定方法的實現【慢速查找流程】
如果緩存中沒有找到,則在類的方法列表
中查找,如果還是沒找到,則去父類鏈的緩存和方法列表
中查找【動態方法決議】
如果慢速查找還是沒有找到時,第一次補救機會
就是嘗試一次動態方法決議
,即重寫resolveInstanceMethod
/resolveClassMethod
方法【消息轉發】
如果動態方法決議還是沒有找到,則進行消息轉發
,消息轉發中有兩次補救機會:快速轉發+慢速轉發
如果轉發之后也沒有,則程序直接報錯崩潰
unrecognized selector sent to instance