OC消息转发机制
在上一篇文章中,我们探索了objc_msgSend慢速转发的流程;那么我们知道在进行慢速查找过程中,如果meth
有值的话,就能获得imp
,接着就会执行代码goto done;
代码
done
中的源码只有两行:
log_and_fill_cache(cls, imp, sel, inst, curClass); runtimeLock.unlock();
重点在第一行的log_and_fill_cache
方法中,下面附上源码:
static void
log_and_fill_cache(Class cls, IMP imp, SEL sel, id receiver, Class implementer)
{
#if SUPPORT_MESSAGE_LOGGING
if (slowpath(objcMsgLogEnabled && implementer)) {
bool cacheIt = logMessageSend(implementer->isMetaClass(),
cls->nameForLogging(),
implementer->nameForLogging(),
sel);
if (!cacheIt) return;
}
#endif
cache_fill(cls, sel, imp, receiver);
}
在源码中,当你满足SUPPORT_MESSAGE_LOGGING
条件之后,他就会执行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;
}
但是,这个方法它是进不来的,为什么呢?
因为objcMsgLogEnabled
这个是默认关闭的,在logMessageSend
函数下面的方法就是打开这个函数的开关,这个函数是void instrumentObjcMessageSends(BOOL flag)
,具体源码就不贴出来了,只要将这个方法打开,就能将执行的方法打印到一个文件中,下面我们来尝试一下:
首先创建一个工程,在工程中创建一个类,具体代码,其中sayHello
方法只有声明,没有实现:
#import <Foundation/Foundation.h>
#import "Person.h"
extern void instrumentObjcMessageSends(BOOL flag);
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *person = [Person alloc];
instrumentObjcMessageSends(YES);
[person sayHello];
instrumentObjcMessageSends(NO);
NSLog(@"Hello, World!");
}
return 0;
}
接着我们先来看一下上面方法中出现的文件路径/tmp/msgSends/
:
没有执行程序时,是这样的;现在我们来执行一下上面的代码,执行之后,文件中多了一个msgSends-11018
文件:
我们打开一下这个文件,下面截取一部分内容显示出来:
16009290557511.png
在里面的内容中,resolveInstanceMethod
是进行动态方法决议,在进行动态方法决议之后,又执行了forwardingTargetForSelector
和methodSignatureForSelector
两个方法。
既然系统会调用这两个方法,那我们去看看这两个方法到底是什么:
首先我们以上面创建的项目在Person.m中实现一个- (id)forwardingTargetForSelector:(SEL)aSelector
方法,具体实现代码:
- (id)forwardingTargetForSelector:(SEL)aSelector{
NSLog(@"%s - %@",__func__,NSStringFromSelector(aSelector));
return [super forwardingTargetForSelector:aSelector];
}
执行程序之后,程序崩溃,打印信息为:
-[Person forwardingTargetForSelector:] - sayHello
-[Person sayHello]: unrecognized selector sent to instance 0x1007457e0
我们先来看一下官方的解释:Returns the object to which unrecognized messages should first be directed.
意思是:当消息者没有接收者时,返回它的第一接收者;也就是说,如果消息没有人接收,那就可以找一个人接收。
那么我们再修改一下代码:
在工程中继续创建一个Student类,类中声明和实现一下sayHello
这个方法,接着在Person.m中修改代码:
- (id)forwardingTargetForSelector:(SEL)aSelector{
NSLog(@"%s - %@",__func__,NSStringFromSelector(aSelector));
return [Student alloc];
}
在执行程序之后,打印结果为(省略了前面的时间和工程名):
-[Person forwardingTargetForSelector:] - sayHello
-[Student sayHello]
Hello, World!
可以看到,程序可以正常执行,可以看到sayHello
方法能够执行,执行的类为Student
。类似帅锅一样的,这就是所谓的快速转发
过程。
那既然有快速转发,那是否存在慢速转发呢?
下面我们来看一下methodSignatureForSelector
这个方法的作用:
首先看一下官方解释:对那些没有转发和处理的方法,系统这边还会有一层处理,返回一个方法的签名,搭配一个方法的使用;进行方法签名时,不能返回为空。
下面我们继续通过代码来实现一下:
在Person.m中另外添加一个方法,首先先测试一下方法会不会执行:
- (id)forwardingTargetForSelector:(SEL)aSelector{
NSLog(@"%s - %@",__func__,NSStringFromSelector(aSelector));
return [super forwardingTargetForSelector:aSelector];
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
NSLog(@"%s - %@",__func__,NSStringFromSelector(aSelector));
return [super methodSignatureForSelector:aSelector];
}
执行结果,程序崩溃,执行了两次:
-[Person forwardingTargetForSelector:] - sayHello
-[Person methodSignatureForSelector:] - sayHello
-[Person sayHello]: unrecognized selector sent to instance 0x10044c0e0
(lldb)
同样的,程序都会执行两个方法,下面根据官方的提示,来实现一下methodSignatureForSelector
的正确过程:
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
NSLog(@"%s - %@",__func__,NSStringFromSelector(aSelector));
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation{
NSLog(@"%s - %@",__func__,anInvocation);
}
这个方法签名在Type Encodings中有详细介绍。
执行结果:
-[Person forwardingTargetForSelector:] - sayHello
-[Person methodSignatureForSelector:] - sayHello
-[Person forwardInvocation:] - <NSInvocation: 0x102808e10>
Hello, World!
Program ended with exit code: 0
程序正确执行;首先快速转发流程来了,然后方法签名和forwardInvocation
都来了,没有崩溃。
下面我们来看一下forwardInvocation
这个方法,这个方法中有一个NSInvocation
类,我们点进去看一下他的结构:
@interface NSInvocation : NSObject
+ (NSInvocation *)invocationWithMethodSignature:(NSMethodSignature *)sig;
@property (readonly, retain) NSMethodSignature *methodSignature;
- (void)retainArguments;
@property (readonly) BOOL argumentsRetained;
@property (nullable, assign) id target;
@property SEL selector;
可以看到他有很多属性,我们在forwardInvocation
方法处打上断点,然后在控制它输出他的属性看一下:
可以看到里面属性的一些内容;由此可见,它是一个消息慢速转发过程,它类似于一个漂流瓶一样的,谁爱处理谁处理。
因此,我们可以自己设定谁来处理,例如将它的target
进行修改,然后invoke
保存:
- (void)forwardInvocation:(NSInvocation *)anInvocation{
NSLog(@"%s - %@",__func__,anInvocation);
anInvocation.target = [Student alloc];
// anInvocation 保存 - 方法
[anInvocation invoke];
}
他的执行结果,可以看到是Student
进行输出sayHello
:
-[Person forwardingTargetForSelector:] - sayHello
-[Person methodSignatureForSelector:] - sayHello
-[Person forwardInvocation:] - <NSInvocation: 0x10060d1e0>
-[Student sayHello]
Hello, World!
Program ended with exit code: 0
消息的慢速转发是一个很灵活的方法,它可以随意指定任何对象进行消息转发,并随时进行保存。
下面总结一下整个过程:
当慢速查找流程没有找到IMP,首先判断当前是否为类,是类就执行resolveClassMethod
方法,不是类就执行resolveInstanceMethod
,但是,resolveClassMethod
方法最后还会走到resolveInstanceMethod
处;
接着进行快速转发执行forwardingTargetForSelector
,当快速转发执行成功,进行消息转发,当执行不成功,进行慢速转发methodSignatureForSelector
,慢速转发返回签名forwardInvocation
,最后进行消息转发。
以上所有流程皆来自上帝视角,那么如果没有上帝视角,那如何得到消息转发的流程呢?
例如我们将Person中的方法注释掉,在main函数中执行sayHello
方法。
结果必然是崩溃的,那么我们通过堆栈信息打印一下看:
16009328233391.png
在样的内容看起来很繁琐,当我们去查看___forwarding___
的汇编信息,可以得知它所存在的库在CoreFoundation中。
但是经过在库中搜索__forwarding_prep_0___
,却找不到我们想要的内容。
那么只有通过反汇编去hopper。首先通过image list
去打印库文件信息,找到CoreFoundation
所在路径:/System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation
,将可执行文件拖入hopper。
拖入进去后的界面:
16009337594264.png
搜索__forwarding_prep_0___
,通过点击伪代码来查看源码:
int ___forwarding_prep_0___(int arg0, int arg1, int arg2, int arg3, int arg4, int arg5) {
*(rsp + 0xa0) = zero_extend_64(xmm7);
*(rsp + 0x90) = zero_extend_64(xmm6);
*(rsp + 0x80) = zero_extend_64(xmm5);
*(rsp + 0x70) = zero_extend_64(xmm4);
*(rsp + 0x60) = zero_extend_64(xmm3);
*(rsp + 0x50) = zero_extend_64(xmm2);
*(rsp + 0x40) = zero_extend_64(xmm1);
*(rsp + 0x30) = zero_extend_64(xmm0);
stack[2021] = arg0;
rax = ____forwarding___(rsp, 0x0);
if (rax != 0x0) {
rax = *rax;
}
else {
rax = objc_msgSend(stack[2021], stack[2021]);
}
return rax;
}
在上面的伪代码中,点击____forwarding___
方法,去查看它的源码:
在这个方法中,找到了一段代码,里面就有在上面探索的forwardingTargetForSelector
方法。
当if
处成立,goto loc_64a67
:
loc_64a67:
var_138 = rbx;
if (strncmp(r13, "_NSZombie_", 0xa) == 0x0) goto loc_64dc1;
继续goto loc_64dc1
:
loc_64dc1:
____forwarding___.cold.1(var_138, r13, var_140, rcx, r8);
goto loc_64dd7;
}
loc_64dd7:
rbx = class_getSuperclass(r12);
r14 = object_getClassName(r14);
if (rbx == 0x0) {
_CFLog(0x4, @"*** NSForwarding: warning: object %p of class '%s' does not implement methodSignatureForSelector: -- did you forget to declare the superclass of '%s'?", var_138, r14, object_getClassName(var_138), r9, stack[2003]);
}
else {
_CFLog(0x4, @"*** NSForwarding: warning: object %p of class '%s' does not implement methodSignatureForSelector: -- trouble ahead", var_138, r14, r8, r9, stack[2003]);
}
goto loc_64e3c;
方法没有实现,报错。
伪代码的流程,我就不详细介绍了,接着继续查看伪代码:
接着又找到了methodSignatureForSelector
方法和forwardInvocation方法
:
loc_64a8a:
rbx = @selector(methodSignatureForSelector:);
r14 = var_138;
var_148 = r15;
if (class_respondsToSelector(r12, rbx) == 0x0) goto loc_64dd7;
loc_64c19:
r15 = @selector(forwardInvocation:);
if (class_respondsToSelector(object_getClass(r14), r15) == 0x0) goto loc_64ec2;
接着又返回到loc_64dd7
,这些伪代码同样的跟上面拥有上帝视角的流程差不太多。
这就是反汇编的整个消息转发流程。