上一期在objc_msgSend()
的慢速查找
lookUpImpOrForward
流程中如果一直沒有找到方法,那流程會走向
resolveMethod_locked
-> resolveInstanceMethod
/ resolveClassMethod
-> resolveInstanceMethod:
/ resolveClassMethod:
也就是當方法一直無法找到的時候,會根據對象方法或者類方法的不同,走向最終對象方法或者類方法的動態方法決議
。
為了保持流程的完整性。我們研究一下 動態方法決議
動態方法決議
先用代碼測試一下。
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
FQPerson *person = [FQPerson alloc];
[person sayHelloWorld];
}
return 0;
}
在main
中我們調用sayHelloWorld
方法
在FQPerson.m
的中注釋掉 sayHelloWorld
方法的實現,同時添加
+ (BOOL)resolveInstanceMethod:(SEL)sel{
NSLog(@"沒找到 %@ 方法",NSStringFromSelector(sel));
return [super resolveInstanceMethod:sel];
}
運行
說明之前的流程確實如我們源碼看到的那樣,走到了resolveInstanceMethod
中。
動態方法決議
其實是蘋果在我們無法找到方法時給我們提供的補救流程,在這里,我們如果實現了方法,我們還是能避免崩潰。我們來嘗試一下。
先引入頭文件
#import <objc/message.h>
然后再resolveInstanceMethod
內部添加代碼
+ (BOOL)resolveInstanceMethod:(SEL)sel{
NSLog(@"沒找到 %@ 方法",NSStringFromSelector(sel));
if(sel == @selector(sayHelloWorld)){
IMP imp = class_getMethodImplementation(self, @selector(eat1));
Method eatMethod = class_getInstanceMethod(self, @selector(eat1));
const char *type = method_getTypeEncoding(eatMethod);
return class_addMethod(self, sel, imp, type);
}
return [super resolveInstanceMethod:sel];
}
在這里,我們將已經實現過的方法eat1
賦給了sayHelloWorld
。
運行
此時 并未崩潰,同時調用了eat1
方法。
繼續,我們注釋掉eat1
的實現
然后運行
此時,我們可以發現,在sayHelloWorld
的動態決議之后,進入了eat1
的動態方法決議,預估應該是在將eat1
賦給sayHelloWorld
后開始進入了eat1
的方法轉發流程。
此時有一個問題,為什么在第一張動態方法決議的打印圖中打印了兩次?
沒找到 sayHelloWorld 方法
沒找到 sayHelloWorld 方法
那么,我們來研究一下動態方法決議
之后,系統做了什么?
第二次 動態方法決議 的來源
我們在+ (BOOL)resolveInstanceMethod:(SEL)sel
中打個斷點,在第二次進入時 bt
打印棧信息
(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
* frame #0: 0x0000000100001885 KCObjc`+[FQPerson resolveInstanceMethod:](self=FQPerson, _cmd="resolveInstanceMethod:", sel="sayHelloWorld") at FQPerson.m:59:55 [opt]
frame #1: 0x00000001002fd3a7 libobjc.A.dylib`resolveInstanceMethod(inst=0x0000000000000000, sel="sayHelloWorld", cls=FQPerson) at objc-runtime-new.mm:6001:21
frame #2: 0x00000001002e8e73 libobjc.A.dylib`resolveMethod_locked(inst=0x0000000000000000, sel="sayHelloWorld", cls=FQPerson, behavior=0) at objc-runtime-new.mm:6043:9
frame #3: 0x00000001002e879c libobjc.A.dylib`lookUpImpOrForward(inst=0x0000000000000000, sel="sayHelloWorld", cls=FQPerson, behavior=0) at objc-runtime-new.mm:6192:16
frame #4: 0x00000001002c27c9 libobjc.A.dylib`class_getInstanceMethod(cls=FQPerson, sel="sayHelloWorld") at objc-runtime-new.mm:5922:5
frame #5: 0x00007fff2ddc8c68 CoreFoundation`__methodDescriptionForSelector + 282
frame #6: 0x00007fff2dde457c CoreFoundation`-[NSObject(NSObject) methodSignatureForSelector:] + 38
frame #7: 0x00007fff2ddb0fc0 CoreFoundation`___forwarding___ + 408
frame #8: 0x00007fff2ddb0d98 CoreFoundation`__forwarding_prep_0___ + 120
frame #9: 0x0000000100001b30 KCObjc`main(argc=<unavailable>, argv=<unavailable>) + 64 [opt]
frame #10: 0x00007fff67e65cc9 libdyld.dylib`start + 1
frame #11: 0x00007fff67e65cc9 libdyld.dylib`start + 1
通過打印的方法信息的反推,我們大概能看見流程
我們想研究這個流程詳細流程,但是CF的代碼并未開源,我們只能借助其他工具來研究。
- 通過
lldb
中image list
打印鏡像列表
(lldb) image list
[ 0] 02E8C081-F154-3A94-BF16-66811D081546 0x0000000100000000 /Users/fangqiang/Library/Developer/Xcode/DerivedData/objc-cmqgtagmrqfzeobzskdrohmygvsg/Build/Products/Debug/KCObjc
[ 1] F9D4DEDC-8296-3E3F-B517-9C8B89A4C094 0x000000010000b000 /usr/lib/dyld
[ 2] F9BB2E7A-E017-32C8-8DB8-5F748EE88EF9 0x00000001002bd000 /private/tmp/objc.dst/usr/lib/libobjc.A.dylib
[ 3] 7C69F845-F651-3193-8262-5938010EC67D 0x00007fff3040a000 /System/Library/Frameworks/Foundation.framework/Versions/C/Foundation
[ 4] C0C9872A-E730-37EA-954A-3CE087C15535 0x00007fff64e4a000 /usr/lib/libSystem.B.dylib
[ 5] C0D70026-EDBE-3CBD-B317-367CF4F1C92F 0x00007fff2dd4d000 /System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation
[ 6] E692F14F-C65E-303B-9921-BB7E97D77855 0x00007fff65183000 /usr/lib/libc++abi.dylib
[ 7] 59A8239F-C28A-3B59-B8FA-11340DC85EDC 0x00007fff65130000 /usr/lib/libc++.1.dylib
得到CF的鏡像地址
-
找到鏡像地址后 通過hopper我們來追蹤一下CF內部的實現流程
hop1.jpg -
使用其中的偽代碼模式,方便我們閱讀 搜索
__forwarding_prep_0___
參考棧方法流程往下走
hop2.jpg
- 然后跳轉
loc_649bb
hop3.jpg
-
其中調用了判斷了方法
forwardingTargetForSelector
是否實現,為空的話繼續跳轉loc_64a67
hop4.jpg -
判斷是否為
_NSZombie
對象,不是則繼續跳轉loc_64dc1
hop5.jpg -
繼續跳轉
loc_64dd7
hop6.jpg
*繼續跳轉loc_64e3c
上面大概是一個簡略的消息轉發失敗流程,似乎沒有找到答案。
我們回到上面的棧打印,其中有一個
CoreFoundation`-[NSObject(NSObject) methodSignatureForSelector:]
在流程
__forwarding_prep_0___
走完之后,如果沒有實現中間的forwardingTargetForSelector
的方法,那后續根據棧的打印,會走到methodSignatureForSelector
-
我們來搜索一下
methodSignatureForSelector
methodSingnatureForSelector.jpg 跳轉其中的
__methodDescriptionForSelector
-
再跳轉
loc_7c68b
hop10.jpg 其中得到
class_getInstanceMethod()
,再回到源碼
Method class_getInstanceMethod(Class cls, SEL sel)
{
if (!cls || !sel) return nil;
#warning fixme build and search caches
lookUpImpOrForward(nil, sel, cls, LOOKUP_RESOLVER);
#warning fixme build and search caches
return _class_getMethod(cls, sel);
}
在這里,我們再次進入lookUpImpOrForward
流程,會進行第二次動態方法決議的打印
消息轉發
在我們剛剛通過hopper
探索的過程中
我們還看到了其中一些其他處理
- 判斷是否響應
forwardingTargetForSelector
- 如果不響應,會跳轉判斷是否響應
methodSignatureForSelector
- 如果也不響應 則直接報錯
- 如果獲取
methodSignatureForSelector
的方法簽名
為nil
,也將直接報錯。 - 如果返回值
methodSignatureForSelector
不為空,則在forwardInvocation
中進行處理。
以上也就是我們消息轉發的流程。
我們通過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
源碼,我們定位到打印文件的位置
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;
}
得到文件結果
所以最終我們的消息轉發流程為
其實這里的消息轉發流程和動態決議是系統給予我們的三次補救機會,可以在這里避免程序崩潰。
但在實際使用過程中還會有一些坑點,還有一些實際的使用,我們有空再細說