前言
在前兩篇節我們了解了快速方法
查找如果找不到
,則會進入慢速查找流程
,其查找流程主要為在當前類
的方法列表中查找
,如果還是沒有找到
,則去父類鏈的緩存和方法列表
中查找。
- 快速查找
- 慢速查找
- 根據慢速查找找不到時,程序崩潰會出現
+[LGPerson sayNB]: unrecognized selector sent to class 0x1000022b0
像是這樣的提示,表示方法並未實現.
防止崩潰
- 為了提升用戶體驗,蘋果給予我們兩個建議
- 【建議一】:動態方法決議
- 【建議二】:消息轉發(快速轉發/慢速轉發)
【建議一】:動態方法決議
- 如果慢速查找沒有找到方法的實現地址,則會進入第一次動態方法決議,如下是
resolveMethod_locked
的源碼實現
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
- 針對實例方法調用時,如果快速/慢速都沒有找到實例方法的實現地址時,有一次的挽救機會,就是執行動態方法決議,由於不是元類,是一個實例方法,程式走到resolveInstanceMethod。
static void resolveInstanceMethod(id inst, SEL sel, Class cls)
{
runtimeLock.assertUnlocked();
ASSERT(cls->isRealized());
SEL resolve_sel = @selector(resolveInstanceMethod:);
// 查看是否有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源碼簡述
- 首先在發送resolveInstanceMethod消息前,需要查找cls類中是否有該方法的實現,即通過
lookUpImpOrNil
方法進入lookUpImpOrForward
慢速查找流程查找resolveInstanceMethod
方法。- 如果沒有查找到,則直接返回
- 如果查找到,則接下來進行發送
resolveInstanceMethod
消息
- 再次慢速查找實例方法的實現,即通過
lookUpImpOrNil
方法進入lookUpImpOrForward
慢速查找實例方法
避免崩潰
實例方法
- 我們知道了在沒有找到方法的實現地址前,程序會進到
resolveInstanceMethod
這裡,我們可以透過重寫類方法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 sayMMethod = class_getInstanceMethod(self, @selector(sayMaster));
//獲取sayMaster的方法簽名
const char *type = method_getTypeEncoding(sayMMethod);
//將sel的實現指向sayMaster
return class_addMethod(self, sel, imp, type);
}
return [super resolveInstanceMethod:sel];
}
- 打印結果如下
類方法
- 針對類方法的防止崩潰,與實例方法相似,一樣使用重寫
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
方法獲取類的元類
,因為類方法在元類中是實例方法
優化-避免崩潰
- 我們知道isa的走位圖,而其中方法查找流程如下
- 實例方法:
類→父類→根類→nil
- 類方法:
元類→根元類→根類→nil
- 實例方法:
- 上節兩種對於實例方法及類方法避免崩潰的方式,可以透過寫加寫
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;
}
- 這樣的統整也示範了,調用了類方法的動態決議,還會調用實例對象的動態方法決議,原因如上所述,因為類方法在
元類中是實例方法
。 - 但是上述這樣的寫法,如果經過
系統版次的更新
,系統方法是有可能會被修改
的,所以其實我們可以依照自定義的方法名,習慣在方法名加入自定義的前綴
,根據前綴判斷是否是自定義方法,統一處理,這樣處理屬於AOP(Aspect Oriented Programming)面向切面編程,比如說可以在檢測到崩潰時pop到首頁,進而提升用戶體驗。 - 在此我們在這個動態方法決議內,先不處理,而是流到下一個環節,消息轉發流程來處理。
消息轉發流程
- 如果
快速/慢速查找
流程與動態方法決議
,還是沒有
找到方法的實現地址
(imp),就進行的是消息轉發流程,我們透過以下方式來探討,找不到方法地址崩潰前,調用了哪些方法。 - 接著我們將使用以下兩種方式
- 通過
instrumentObjcMessageSends
方法查看log - 通過
hopper/IDA
反編譯
- 通過
通過instrumentObjcMessageSends
方法查看log
- 查找流程
lookUpImpOrForward
→log_and_fill_cache
→logMessageSend
並且在logMessageSend
下找到instrumentObjcMessageSends
- 如下為
instrumentObjcMessageSends
的源碼實現
void instrumentObjcMessageSends(BOOL flag)
{
bool enable = flag;
// Shortcut NOP
if (objcMsgLogEnabled == enable)
return;
// If enabling, flush all method caches so we get some traces
if (enable)
_objc_flush_caches(Nil);
// Sync our log file
if (objcMsgLogFD != -1)
fsync (objcMsgLogFD);
objcMsgLogEnabled = enable;
}
- 接下來,我們打算在
main
中調用instrumentObjcMessageSends
,打印出log信息,需要完成 下列動作。- 傳入flag為YES,也就是objcMsgLogEnabled,即調用
instrumentObjcMessageSends
,傳入參數為YES。 - 需要使用extern聲明instrumentObjcMessageSends方法。
- 傳入flag為YES,也就是objcMsgLogEnabled,即調用
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
目錄,發現有msgSends開頭的日誌文件,打開查看在程式崩潰前,執行了以下方法- 兩次動態方法決議:
resolveInstanceMethod
方法 - 兩次消息快速轉發:
forwardingTargetForSelector
方法 - 兩次消息慢速轉發:
methodSignatureForSelector
+resolveInstanceMethod
- 兩次動態方法決議:
通過反彙編-Hopper/IDA反編譯
- Hopper與IDA是逆向工程時靜態分析常用的工具,將可執行文件經由反彙編工具(Hopper/IDA)反彙編程彙編語言,透過工具還能將其轉換為偽代碼,控制流程圖。
程式碼編譯為彙編語言在編譯成機器語言(可執行文件),最後由計算器運行
- 可以看到
___forwarding___
來自CoreFoundation
- 利用
image list
調用讀取鏡相文件,然後搜索CoreFoundation
,查看執行路徑
- 通過文件路徑,找到
CoreFoundation
的可執行文件
/System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation
- 將可執行文件,拖入hopper開啟
- 通過左側的搜索框搜索_ forwarding_prep_0,然後選擇偽代碼
- 以下是
__forwarding_prep_0 ___
的彙編偽代碼,跳轉至___forwarding___
- 以下是
___forwarding___
的偽代碼實現,首先是查看是否實現forwardingTargetForSelector
方法,如果沒有響應.跳轉
偽代碼-forwardingTargetForSelector
- 如果沒有響應,跳轉至
loc_64b9b
,則直接報錯 - 如果獲取
methodSignatureForSelector
的方法签名
為nil(if (rax == 0x0) goto loc_6501c;)
,也是直接報錯if (strncmp(r13, "*NSZombie*", 0xa) == 0x0) goto loc_64fa1;
- 如果
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事務,程序都不會崩潰
問題探索
動態方法為何執行兩次?
- 以下使用兩種方式分析動態方法決議
- 通過偽代碼驗證
- 通過程式推導
通過偽代碼+調適驗證
- 首先LGPerson類裡面只有聲明,沒有say666的實現,通過慢速查找流程我們知道方法執行是通過lookUpImpOrForward --> resolveMethod_locked --> resolveInstanceMethod,來到resolveInstanceMethod源码,在源碼中通過發送resolve_sel消息觸發,如下所示
所以可以在
resolveInstanceMethod
方法中IMP imp = lookUpImpOrNil(inst, sel, cls);
處加一個斷點,通過bt打印堆棧信息來看到底發生了什麼在
resolveInstanceMethod
方法中IMP imp = lookUpImpOrNil(inst, sel, cls);
處加一個斷點,運行程序,直到第一次“来了”
,我們透過lldb調適輸入bt打印堆棧信息,此时的sel是say666
- 過掉斷點繼執行到第二次的
“来了”
,查看堆棧信息,在第二次中,我們可以看到是通過CoreFoundation
的[NSObject(NSObject) methodSignatureForSelector:]
方法,然後通過class_getInstanceMethod
再次進入動態方法決議。
- 通過上一步的堆棧信息,我們需要去看看CoreFoundation中到底做了什麼?通過
Hopper
反編譯彙編CoreFoundation
的可執行文件,查看methodSignatureForSelector
方法的偽代碼。
- 接著我們透過hopper反編譯
CoreFoundation
的可執行文件,開啟偽代碼模式,搜尋methodSignatureForSelector
。
- 通過
methodSignatureForSelector
偽代碼進入___methodDescriptionForSelector
的實現。
- 進入___methodDescriptionForSelector的偽代碼實現,結合彙編的堆棧打印,可以看到,在
___methodDescriptionForSelector
這個方法中調用了objc4-781
的class_getInstanceMethod
- 在objc中的源碼搜索
class_getInstanceMethod
,其源碼實現如下所示
- 這一點可以通過代碼調適來驗證,如下所示,在
class_getInstanceMethod
方法處加一個斷點,在執行了methodSignatureForSelector
方法後,返回了簽名.說明方法簽名是生效的,蘋果在走到invocation之前,給了開發者一次機會再去查詢,所以走到class_getInstanceMethod
這裏,又去走了一遍方法查詢say666,然後會再次走到動態方法決議
- 所以,上述的分析也印證了前文中
resolveInstanceMethod
方法執行了兩次的原因
通過程式推導
如果LGPerson類中重寫resolveInstanceMethod方法,並加上class_addMethod操作即賦值IMP,此時resolveInstanceMethod會走兩次嗎?
- 答案是只走一次,如果賦值了IMP,動態方法決議只會走一次,說明不是在這裡走第二次動態方法決議
- 排除掉
resolveInstanceMethod
方法中的賦值IMP,在LGPerson類中重寫forwardingTargetForSelector
,並指定返回值為[LGStudent alloc]
,重新運行,如果resolveInstanceMethod
打印兩次,說明是在forwardingTargetForSelector
方法之前執行了動態方法決議,反之在forwardingTargetForSelector
方法之後.
如圖可以看到 第二次動態法決議在
methodSignatureForSelector
和forwardInvocation
方法之間經過上面的論證,我們了解到其實在慢速消息轉發流程中,在
methodSignatureForSelector
和forwardInvocation
方法之間還有一次動態方法決議,即蘋果再次給的一個機會,如下圖所示
總結
當進行objc_msgSend發送消息時
- 首先進行
快速查找
流程,在類的緩存cache中查找指定方法的實現 - 如果緩存沒有查找到,則進行
慢速查找
流程,會先在類的方法列表中查找,如果還是沒有找到,則去父類鏈的緩存和方法列表中查找 - 如果慢速差找也沒有找到,第一次的機會是
動態方法決議
,也就是重寫resolveInstanceMethod
/resolveClassMethod
方法 - 如果動態方法決議還是沒有找到,則進行
消息轉發
,消息轉發有兩次機會:快速轉發
+慢速轉發
- 如果轉發後也沒有則報錯崩潰
unrecognized selector sent to instance