前倆篇objc_msgSend快速查找和objc_msgSend慢速查找的流程,主要分析了經過彙編流程快速查找緩存,經過類的方法列表慢速查找,本章着重接着上倆章深刻分析沒有找到方法的狀況下,
蘋果
給開發者提供了二個建議。c++
動態方法解析
: 在慢速查找過程當中,未找到IMP
,會執行一次動態方法解析
消息轉發
: 若是動態方法決議仍是沒有找到IMP
,則開始消息轉發
若是以上倆步都沒有作相應的操做,就會報平常開發常見的錯誤
方法未實現的崩潰報錯
objective-c
以下示例代碼:windows
@interface Student : NSObject
@property (nonatomic, copy) NSString *lgName;
@property (nonatomic, strong) NSString *nickName;
- (void)sayNB;
- (void)sayMaster;
- (void)say666;
- (void)sayHello;
+ (void)sayNB;
+ (void)lgClassMethod;
@end
@implements Student
- (void)sayHello{
NSLog(@"%s",__func__);
}
- (void)sayNB{
NSLog(@"%s",__func__);
}
- (void)sayMaster{
NSLog(@"%s",__func__);
}
+ (void)lgClassMethod{
NSLog(@"%s",__func__);
}
@end
複製代碼
int main(int argc, const char * argv[]) {
@autoreleasepool {
Student *stu = [Student alloc];
[stu say666];
//[Student performSelector:@selector(sayNB)];
}
return 0;
}
複製代碼
在main
方法中分別調用實例方法
和類方法
,緩存
慢速查找
的源碼中,IMP
未找到,會賦值稱爲forward_imp=(IMP)_objc_msgForward_impcache;
,經過搜索_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
,根據以前總結的規則, 去掉一個下劃線來搜索。markdown
// 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
,這就是咱們平常中常常見到的崩潰錯誤。 下面深刻分析崩潰發生以前的補救方法
架構
在
lookUpImpOrForward
方法裏,方法慢速
查找走完以後,會開始走方法動態解析
流程,給開發者提供第一次
機會,來處理找不到消息
的錯誤。函數
// No implementation found. Try method resolver once.
if (slowpath(behavior & LOOKUP_RESOLVER)) {
behavior ^= LOOKUP_RESOLVER;
return resolveMethod_locked(inst, sel, cls, behavior);
}
複製代碼
經過註釋
也能夠得知, 這個實在IMP
沒有找到的時候,會走這裏解決,而且只走一次。工具
/*********************************************************************** * resolveMethod_locked * Call +resolveClassMethod or +resolveInstanceMethod. * * Called with the runtimeLock held to avoid pressure in the caller * Tail calls into lookUpImpOrForward, also to avoid pressure in the callerb **********************************************************************/
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);
}
複製代碼
主要分如下幾步:oop
先是判斷cls
是不是元類
?post
類
,調用對象方法
的動態解析resolveInstanceMethod
元類
,調用類方法
的動態解析resolveClassMethod
來處理,而後判斷是否能找到sel
,找不到接着再調用一次resolveInstanceMethod
,由於類方法,即帶+號的方法
相對於元類
來講也是實例方法, 調用resolveInstanceMethod
,參數第一個是inst=類
,第二個查找是sel方法名字
,第三個cls=元類
,if (!lookUpImpOrNil(cls, resolve_sel, cls->ISA())) {
// Resolver not implemented.
return;
}
複製代碼
若是這裏查找的是類方法, 是在cls->ISA
根元類裏找這個解析方法的實現, 找到就去發送消息, 找不到返回默認實現。
+ (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
,消息崩潰以前, 會執行一次實例方法動態解析
,在這個方法裏,經過runtime
把沒找到的sel
指向一個存在的imp
上,打印結果
這裏會看到這個方法打印裏倆次
,這個問題留在文章末尾分析。
發送類方法消息
找不到imp
致使的崩潰修復,與實例方法
相似方法修復, 重寫resolveClassMethod
來解決,在該方法中, 把崩潰的sel
指向一個能夠找到的imp
。
+ (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];
}
複製代碼
⚠️ 這裏要注意獲取類方法是要到
元類
,添加類方法
也要到元類中
,可使用objc_getMetaClass
獲取元類。
經過上邊的方法的動態解析
分析, 獲得這樣的結論
類 -> 父類 -> 根類 -> nil
類方法(resolveClassMethod) 元類 -> 父元類 -> 根元類 -> 根類 -> nil
類方法(resolveInstanceMethod) 根元類 -> 根類 -> nil
以前的修復崩潰都是在對應的類中重寫resolveInstanceMethod
或者resolveClassMethod
,經過上邊這三條路線,能夠根類NSObject
中重寫resolveInstanceMethod
統一處理實例方法
和類方法
的崩潰處理。
resolveInstanceMethod
在NSObject
有默認實現
+ (BOOL)resolveClassMethod:(SEL)sel {
return NO;
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
return NO;
}
複製代碼
以下,建立一個NSObject
的分類,統一處理以下,由於有默認實現,因此返回NO
,不能調用[super resolveInstanceMethod:sel]
:
+ (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;
}
複製代碼
固然這種統一處理的方式,仍是會有一些問題, 一些系統的方法會走進這裏, 能夠針對類中的方法名統一前綴
,根據前綴
判斷對應的模塊來處理,好比mine
模塊, 屬於這個模塊的崩潰統一跳轉到mine
模塊首頁, 也能夠作一些錯誤上報的操做。
在
快速查找
+慢速查找
沒有找到以及動態消息解析
也未處理,就會進入消息轉發過程
在
lookUpImpOrForward
的函數末尾, 在log_and_fill_cache
有這麼一個控制條件objcMsgLogEnabled
,經過它能夠控制日誌保存到本地,經過日誌能夠看到調用流程
控制這個objcMsgLogEnabled
的是這個函數instrumentObjcMessageSends
,給它傳入true
,控制開啓本地日誌保存
經過lookUpImpOrForward -> log_and_fill_cache -> logMessageSend
找到如下源碼實現
bool objcMsgLogEnabled = false;
static int objcMsgLogFD = -1;
bool logMessageSend(bool isClassMethod, const char *objectsClass, const char *implementingClass, SEL selector) {
char buf[ 1024 ];
// Create/open the log file
if (objcMsgLogFD == (-1))
{
snprintf (buf, sizeof(buf), "/tmp/msgSends-%d", (int) getpid ());
objcMsgLogFD = secure_open (buf, O_WRONLY | O_CREAT, geteuid());
if (objcMsgLogFD < 0) {
// no log file - disable logging
objcMsgLogEnabled = false;
objcMsgLogFD = -1;
return true;
}
}
// Make the log entry
snprintf(buf, sizeof(buf), "%c %s %s %s\n",
isClassMethod ? '+' : '-',
objectsClass,
implementingClass,
sel_getName(selector));
objcMsgLogLock.lock();
write (objcMsgLogFD, buf, strlen(buf));
objcMsgLogLock.unlock();
// Tell caller to not cache the method
return false;
}
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;
}
複製代碼
由於這個instrumentObjcMessageSends
是內部函數,在外部使用須要使用extern
外部聲明
extern void instrumentObjcMessageSends(BOOL flag);
int main(int argc, const char * argv[]) {
@autoreleasepool {
LGPerson *person = [LGPerson alloc];
instrumentObjcMessageSends(YES);
[person sayHello];
instrumentObjcMessageSends(NO);
}
return 0;
}
複製代碼
經過以上源碼瞭解到日誌的保存路徑在/tmp/msgSends
目錄中,運行代碼,就能夠看到以下內容
在目錄中打開msgSends
開頭的文件, 調用完resolveInstanceMethod
方法,並無在方法動態解析
處理,因此來到forwardingTargetForSelector
的快速轉發
以及後續 的慢速轉發
Hopper
和IDA
是一個能夠幫助咱們靜態分析的反彙編工具,將可執行文件反彙編爲僞代碼 和流程圖形式,幫助咱們去分析,因爲IDA
在mac上不穩定,能夠在windows
系統上測試, 如下使用Hopper
來分析。
運行崩潰後,經過bt
看堆棧信息,
經過彙編查看,__forwarding___
也是在CoreFoundation
中。
經過image list
調試命令查看CoreFoundation image
的位置
找到CoreFoundation
後,用Hopper
打開它
打開Hopper
, 選擇Try the Demo
,將CoreFoundation
拖入裏邊
點擊OK
默認點擊Next
等待加載完成,
搜索__forwarding_prep_0___
,查看僞代碼, 跳轉到___forwarding___
裏邊的僞代碼
首先判斷是否實現forwardingTargetForSelector
loc_64a67
loc_649fc
,經過forwardingTargetForSelector
獲取接受對象給rax
, 再對rax
做容錯處理,有錯誤跳到loc_64e3c
loc_64a67
僞代碼跳到這裏後,首先判斷是否爲殭屍對象
,在下邊繼續判斷是否響應 methodSignatureForSelector
,
loc_64dd7
, 直接報錯loc_64e3c
loc_64dd7
僞代碼和loc_64e3c
僞代碼經過獲取methodSignatureForSelector
方法簽名爲nil
也直接報錯
上邊的流程獲取到方法簽名
,開始在forwardInvocation
方法中進行處理
因此經過以上分析, 消息轉發有倆種
forwardingTargetForSelector
methodSignatureForSelector
+forwardingTargetForSelector
實現在lookUpImpOrForward
中,慢速也沒有找到imp
,
方法的動態解析
處理,這步未處理, 即走消息轉發
消息轉發
第一步開始forwardingTargetForSelector
,即快速消息轉發
,將消息轉發給別等對象處理,這步未處理,交給慢速轉發
慢速轉發
使用methodSignatureForSelector
返回方法簽名,不能夠返回nil
或者簽名內容爲空
,使用方法簽名生成NSInvocation
對象, 因此須要重寫forwardInvocation
進行消息轉發。resolveInstanceMethod
爲何執行倆次?解決以前遺留的問題, 在實例動態方法解析的時候, 只重寫了, 並未對未找到的
sel
做處理, 會調用倆次
在實例動態方法解析
的時候, 會走到lookUpImpOrForward
-> resolveMethod_locked
-> resolveInstanceMethod
,是經過這裏觸發
在IMP imp = lookUpImpOrNil(inst, sel, cls);
加個斷點, 當sel
是say666
停下來,打印了了say66 來了
經過bt
查看堆棧,
第一次打印的信息, 經過堆棧能夠看出是第一次經過方法動態解析
執行打印的。
經過第二次打印, 經過[NSObject(NSObject) methodSignatureForSelector:]
-> __methodDescriptionForSelector
-> class_getInstanceMethod
再次來到方法的動態解析
並打印了第二次,經過堆棧分析, 能夠經過Hopper
反彙編CoreFoundation
文件,查看methodSignatureForSelector
的僞代碼
在跳進到___methodDescriptionForSelector
看它的實現
結合以前的堆棧信查看, 這裏調用了objc 的方法 class_getInstanceMethod
,在源碼工程查看
/*********************************************************************** * class_getInstanceMethod. Return the instance method for the * specified class and selector. **********************************************************************/
Method class_getInstanceMethod(Class cls, SEL sel) {
if (!cls || !sel) return nil;
// This deliberately avoids +initialize because it historically did so.
// This implementation is a bit weird because it's the only place that
// wants a Method instead of an IMP.
#warning fixme build and search caches
// Search method lists, try method resolver, etc.
lookUpImpOrForward(nil, sel, cls, LOOKUP_RESOLVER);
#warning fixme build and search caches
return _class_getMethod(cls, sel);
}
複製代碼
經過源碼查看,這裏又調用了lookUpImpOrForward
, 又走了一次方法動態解析
,系統在調用完methodSignatureForSelector
,返回方法簽名,在調用invocation
以前,又去調用class_getInstanceMethod
,因此又走了一遍lookUpImpOrForward
,查詢一遍sel
,沒查到再走方法動態解析
和消息轉發
流程。
由於在源碼工程裏探索, 因此有上帝視角, 若是沒有環境, 如何驗證上邊的流程?
在普通工程
裏重寫resolveInstanceMethod
,在方法裏解決sel
找不到的錯誤,使用class_addMethod
添加一個IMP
, 看看這個方法是否會走倆次?
+ (BOOL)resolveInstanceMethod:(SEL)sel{
if (sel == @selector(say666)) {
NSLog(@"%@ 來了", NSStringFromSelector(sel));
//獲取sayMaster方法的imp
IMP imp = class_getMethodImplementation(self, @selector(sayHello));
//獲取sayMaster的實例方法
Method sayMethod = class_getInstanceMethod(self, @selector(sayHello));
//獲取sayMaster的方法簽名
const char *type = method_getTypeEncoding(sayMethod);
//將sel的實現指向sayMaster
return class_addMethod(self, sel, imp, type);
}
return [super resolveInstanceMethod:sel];
}
複製代碼
經過結果看,經過動態方法解析
,賦值了IMP
, 只執行了一次,說明第二次不在這裏。按照消息轉發流程
, 把resolveInstanceMethod
裏的imp
去掉,重寫forwardingTargetForSelector
,並指定[LGStudent alloc]
,從新運行, 看是否resolveInstanceMethod
打印倆次, 打印倆次,說明在forwardingTargetForSelector
以前執行了方法動態解析
,反之,則在以後執行的方法動態解析
。
+ (BOOL)resolveInstanceMethod:(SEL)sel{
if (sel == @selector(say666)) {
NSLog(@"%s -- %@ 來了",__func__, NSStringFromSelector(sel));
// //獲取sayMaster方法的imp
// IMP imp = class_getMethodImplementation(self, @selector(sayHello));
// //獲取sayMaster的實例方法
// Method sayMethod = class_getInstanceMethod(self, @selector(sayHello));
// //獲取sayMaster的方法簽名
// const char *type = method_getTypeEncoding(sayMethod);
// //將sel的實現指向sayMaster
// return class_addMethod(self, sel, imp, type);
}
return [super resolveInstanceMethod:sel];
}
- (id)forwardingTargetForSelector:(SEL)aSelector{
NSLog(@"%s - %@",__func__,NSStringFromSelector(aSelector));
// runtime + aSelector + addMethod + imp
return [LGStudent alloc];
}
複製代碼
經過運行結果看, 並無在以前答應倆次, 說明在forwardingTargetForSelector
以後執行的方法動態解析
接着根據流程,重寫methodSignatureForSelector
和forwardInvocation
+ (BOOL)resolveInstanceMethod:(SEL)sel{
NSLog(@"%s -- %@ 來了",__func__, NSStringFromSelector(sel));
if (sel == @selector(say666)) {
// //獲取sayMaster方法的imp
// IMP imp = class_getMethodImplementation(self, @selector(sayHello));
// //獲取sayMaster的實例方法
// Method sayMethod = class_getInstanceMethod(self, @selector(sayHello));
// //獲取sayMaster的方法簽名
// const char *type = method_getTypeEncoding(sayMethod);
// //將sel的實現指向sayMaster
// return class_addMethod(self, sel, imp, type);
}
return [super resolveInstanceMethod:sel];
}
- (id)forwardingTargetForSelector:(SEL)aSelector{
NSLog(@"%s - %@",__func__,NSStringFromSelector(aSelector));
return [super forwardingTargetForSelector:aSelector];
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
NSLog(@"%s - %@",__func__,NSStringFromSelector(aSelector));
NSLog(@"%p", [NSMethodSignature signatureWithObjCTypes:"v@:@"]);
return [NSMethodSignature signatureWithObjCTypes:"v@"];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation{
NSLog(@"%s - %@",__func__,anInvocation);
// GM sayHello - anInvocation - 漂流瓶 - anInvocation
anInvocation.target = [LGStudent alloc];
// anInvocation 保存 - 方法
[anInvocation invoke];
}
複製代碼
通過上邊的分析,第二次動態決議是在methodSignatureForSelector
和forwardInvocation
之間調用的,第二種分析方法驗證結果和第一種反彙編
的結果是同樣的。獲得以下的圖
本篇是消息流程分析
,方法動態解析
,消息轉發
的最後一篇
消息
經過彙編流程
快速查找,沒有找到跳到lookupImpOrForward
開始慢速查找慢速查找消息
也沒有找到,開始方法動態決議
方法動態決議
根據消息是類方法
仍是實例方法
重寫resolveInstanceMethod
和resolveClassMethod
方法,開始第一次補救方法動態決議
也沒有處理, 開始進行消息轉發即【快速轉發】快速轉發
, 即重寫forwardingTargetForSelector
方法, 將消息甩給能夠處理的對象
,進行第二次補救慢速轉發
使用methodSignatureForSelector
返回方法簽名,不能夠返回nil
或者簽名內容爲空
,使用方法簽名生成NSInvocation
對象, 因此須要重寫forwardInvocation
進行消息轉發。iOS開發·runtime原理與實踐: 消息轉發篇(Message Forwarding) (消息機制,方法未實現+API不兼容奔潰,模擬多繼承)