上一期在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内部的实现流程
-
使用其中的伪代码模式,方便我们阅读 搜索
__forwarding_prep_0___
参考栈方法流程往下走
- 然后跳转
loc_649bb
-
其中调用了判断了方法
forwardingTargetForSelector
是否实现,为空的话继续跳转loc_64a67
-
判断是否为
_NSZombie
对象,不是则继续跳转loc_64dc1
-
继续跳转
loc_64dd7
*继续跳转loc_64e3c
上面大概是一个简略的消息转发失败流程,似乎没有找到答案。
我们回到上面的栈打印,其中有一个
CoreFoundation`-[NSObject(NSObject) methodSignatureForSelector:]
在流程
__forwarding_prep_0___
走完之后,如果没有实现中间的forwardingTargetForSelector
的方法,那后续根据栈的打印,会走到methodSignatureForSelector
-
我们来搜索一下
methodSignatureForSelector
跳转其中的
__methodDescriptionForSelector
-
再跳转
loc_7c68b
其中得到
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;
}
得到文件结果
所以最终我们的消息转发流程为
其实这里的消息转发流程和动态决议是系统给予我们的三次补救机会,可以在这里避免程序崩溃。
但在实际使用过程中还会有一些坑点,还有一些实际的使用,我们有空再细说