1. instrumentObjcMessageSends辅助分析
objc
源码中的logMessageSend
函数,用于记录sel
的执行日志,并输出文件到指定目录中
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;
}
在写入缓存的log_and_fill_cache
函数中,如果objcMsgLogEnabled
为真,且implementer
存在,调用logMessageSend
函数
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
cls->cache.insert(sel, imp, receiver);
}
- 参数
implementer
在正常情况下一定存在,所以进入logMessageSend
函数的关键条件取决于objcMsgLogEnabled
的值
objc
源码中,找到objcMsgLogEnabled
的赋值
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;
}
- 将参数
flag
赋值给objcMsgLogEnabled
不难看出,调用objc
中的instrumentObjcMessageSends
函数,将入参flag
传入true
,即可记录sel
的调用日志,并输出文件到指定目录中
1.1 导出日志
搭建测试工程,在LGPerson
中定义say666
实例方法,但不实现该方法
在main.m
文件中,写入以下代码:
#import <Foundation/Foundation.h>
#import "LGPerson.h"
extern void instrumentObjcMessageSends(BOOL flag);
int main(int argc, const char * argv[]) {
@autoreleasepool {
LGPerson *per = [LGPerson alloc];
instrumentObjcMessageSends(YES);
[per say666];
instrumentObjcMessageSends(NO);
}
return 0;
}
-------------------------
//输出结果:
-[LGPerson say666]: unrecognized selector sent to instance 0x10071b990
1.2 辅助分析
打开/tmp
目录,找到msgSends
开头的文件
+ LGPerson NSObject resolveInstanceMethod:
+ LGPerson NSObject resolveInstanceMethod:
- LGPerson NSObject forwardingTargetForSelector:
- LGPerson NSObject forwardingTargetForSelector:
- LGPerson NSObject methodSignatureForSelector:
- LGPerson NSObject methodSignatureForSelector:
+ LGPerson NSObject resolveInstanceMethod:
+ LGPerson NSObject resolveInstanceMethod:
- LGPerson NSObject doesNotRecognizeSelector:
- LGPerson NSObject doesNotRecognizeSelector:
- 日志中的每一个方法都会打印两次,此问题暂且无视
日志中,当调用的方法找不到时,在方法动态决议流程之后,系统还调用了消息转发流程,通过快速转发和慢速转发,给开发者另外两次挽救机会
2. 快速转发
forwardingTargetForSelector
:返回未找到的消息首选重定向的对象
如果一个对象实现或继承此方法,并返回一个非nil
和非self
结果,则该返回的对象将用作新的接收方对象,消息将发送给该新对象。如果从这个方法返回self
,代码将陷入死循环
如果你在非根类中实现这个方法,如果你的类对于给定的选择器没有返回任何东西,那么你应该返回调用super
的实现的结果
该方法在慢速转发forwardInvocation:
机制触发之前,重定向发送给它的未知消息。如果只想将消息重定向到另一个对象,并且比常规转发快一个数量级时,这是最好的选择。如果转发的目标是捕获NSInvocation
,或者在转发期间操作参数或返回值,那么它就没有用了
在LGStudent
中,实现say666
实例方法
#import "LGStudent.h"
@implementation LGStudent
- (void)say666{
NSLog(@"%s",__func__);
}
@end
在LGPerson
中,实现forwardingTargetForSelector
方法,并重定向给LGStudent
实例对象
#import "LGPerson.h"
#import "LGStudent.h"
@implementation LGPerson
- (id)forwardingTargetForSelector:(SEL)aSelector{
NSLog(@"forwardingTargetForSelector:%@,%@", self, NSStringFromSelector(aSelector));
return [LGStudent alloc];
}
@end
-------------------------
//输出结果:
forwardingTargetForSelector:<LGPerson: 0x280078090>,say666
-[LGStudent say666]
3. 慢速转发
3.1 methodSignatureForSelector
返回一个NSMethodSignature
对象,该对象包含指定SEL
的方法签名
该方法用于协议的实现。此方法配合resolveInstanceMethod:
一起使用,在消息转发期间必须创建NSInvocation
对象。如果您的对象维护一个委托或能够处理它没有直接实现的消息,您应该重写此方法以返回适当的方法签名
案例
在LGPerson
中,实现methodSignatureForSelector
方法
@implementation LGPerson
- (id)forwardingTargetForSelector:(SEL)aSelector{
NSLog(@"forwardingTargetForSelector:%@,%@", self, NSStringFromSelector(aSelector));
// return [LGStudent alloc];
return [super forwardingTargetForSelector:aSelector];
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
NSLog(@"methodSignatureForSelector:%@,%@", self, NSStringFromSelector(aSelector));
return [super methodSignatureForSelector:aSelector];
}
@end
-------------------------
//输出结果:
forwardingTargetForSelector:<LGPerson: 0x28182c090>,say666
methodSignatureForSelector:<LGPerson: 0x28182c090>,say666
-[LGPerson say666]: unrecognized selector sent to instance 0x28182c090
- 如果想在
LGPerson
中触发methodSignatureForSelector
,必须保证在forwardingTargetForSelector
中,不能重定向到其他对象。即使重定向的对象未实现该方法,也会进入该对象的消息处理流程中 - 没有执行成功,需要返回方法签名,并配合
forwardInvocation:
一起使用
3.2 forwardInvocation
被子类重写用于将消息转发到其他对象
当向一个对象发送了一条它没有相应方法的消息时,运行时系统给接收方一个机会将消息委托给另一个接收方。它通过创建一个代表该消息的NSInvocation
对象,并向接收者发送一个包含该NSInvocation
对象作为参数的forwardInvocation:
消息来委托该消息。然后,接收方的forwardInvocation:
方法可以选择将消息转发到另一个对象。如果该对象也不能响应消息,它也将有机会转发它
forwardInvocation:
方法的实现有两个任务:
- 定位可以响应调用中编码的消息的对象。该对象不必对所有消息都相同
- 使用
invocation
将消息发送到该对象。anInvocation
将保存结果,运行时系统将提取该结果并将其交付给原始发送方
在LGPerson
中,返回方法签名,并实现forwardInvocation
方法
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
NSLog(@"methodSignatureForSelector:%@,%@", self, NSStringFromSelector(aSelector));
if(aSelector==@selector(say666)){
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
return [super methodSignatureForSelector:aSelector];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation{
NSLog(@"forwardInvocation:%@,%@,%@", self, anInvocation.target, NSStringFromSelector(anInvocation.selector));
}
-------------------------
//输出结果:
forwardingTargetForSelector:<LGPerson: 0x280ab0090>,say666
methodSignatureForSelector:<LGPerson: 0x280ab0090>,say666
forwardInvocation:<LGPerson: 0x280ab0090>,<LGPerson: 0x280ab0090>,say666
- 解决系统崩溃的问题,也打印出指定方法中的
Log
,但此时并没有对say666
方法进行处理
在系统层面,所有的方法和函数统称系统消息,也可称之为事务。对事务来说,我们可以立即处理,也可暂不处理。但anInvocation
会保留,我们可以在后面适当的时机,继续用来消息的处理
案例
慢速转发机制,通过methodSignatureForSelector:
获得方法签名,创建要转发的NSInvocation
对象。所以我们需要预先提供正确的方法签名
在LGStudent
中,实现say:
方法,传入NSString
参数
#import "LGStudent.h"
@implementation LGStudent
- (void)say:(NSString *)str{
NSLog(@"%s,say:%@", __func__, str);
}
@end
在methodSignatureForSelector
方法中,返回正确的方法签名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
NSLog(@"methodSignatureForSelector:%@,%@", self, NSStringFromSelector(aSelector));
if(aSelector==@selector(say666)){
return [NSMethodSignature signatureWithObjCTypes:"v@:@"];
}
return [super methodSignatureForSelector:aSelector];
}
在forwardInvocation
方法中,对消息进行转发处理
- (void)forwardInvocation:(NSInvocation *)anInvocation{
if(anInvocation.selector==@selector(say666)){
LGStudent *s = [LGStudent alloc];
anInvocation.target=s;
anInvocation.selector=@selector(say:);
NSString *str = @"hahaha~";
[anInvocation setArgument:&str atIndex:2];
[anInvocation invoke];
return;
}
NSLog(@"forwardInvocation:%@,%@,%@", self, anInvocation.target, NSStringFromSelector(anInvocation.selector));
}
-------------------------
//输出结果:
forwardingTargetForSelector:<LGPerson: 0x283918110>,say666
methodSignatureForSelector:<LGPerson: 0x283918110>,say666
-[LGStudent say:],say:hahaha~
- 传入参数的
index
为2
,因为0
和1
被objc_msgSend
的self
和_cmd
占用
forwardInvocation:
的实现可以做的不仅仅是转发消息,还可以用于合并响应各种不同消息的代码,从而避免为每个选择器编写单独的方法的必要性。在对给定消息的响应中,forwardInvocation:
方法还可能涉及多个其他对象,而不是将其转发给一个对象
NSObject
的forwardInvocation
实现:内部调用doesNotRecognizeSelector:
方法,它不转发任何信息。因此,如果不实现forwardInvocation:
,那么向对象发送无法识别的消息将引发异常
3.3 doesNotRecognizeSelector
处理接收者不能识别的消息
当对象接收到无法响应或转发的aSelector
消息时,运行时系统就会调用此方法。这个方法反过来引发NSInvalidArgumentException
,并生成一个错误消息
案例
在LGPerson
中,定义并实现sayNB
方法
- (void)sayNB {
NSLog(@"如果不覆盖此方法,将会得到一个异常");
[self doesNotRecognizeSelector:_cmd];
}
LGStudent
继承自LGPerson
,如果子类没有重写父类方法,调用该方法,此时就会抛出异常
LGStudent *s = [LGStudent alloc];
[s sayNB];
-------------------------
//输出结果:
如果不覆盖此方法,将会得到一个异常
-[LGStudent sayNB]: unrecognized selector sent to instance 0x280f48070
4. 反汇编探索
4.1 CoreFoundation
如何查看消息转发的调用流程?
在forwardingTargetForSelector
中设置断点,查看函数调用栈
消息转发流程:_CF_forwarding_prep_0
→___forwarding___
→forwardingTargetForSelector:
消息转发在CoreFoundation
框架中调用,但CF
框架并没有完全开源,我们只能通过反汇编的方式进行探索
先使用真机或模拟器运行项目,得到CF
框架可执行文件的路径
image list
-------------------------
[ 5] F80FCA31-BF76-3293-8BC6-1729588AE8B6 0x000000018c052000 /Users/zang/Library/Developer/Xcode/iOS DeviceSupport/14.0 (18A373)/Symbols/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation
使用Hopper
打开CoreFoundation
可执行文件,搜索forwarding
找到_CF_forwarding_prep_0
函数,使用伪代码查看
- 方法内部调用
___forwarding___
函数
进入___forwarding___
函数
- 如果
forwardingTargetForSelector
方法未实现,继续执行代码。否则,跳转至loc_64a67
- 进入快速转发流程,调用
forwardingTargetForSelector
方法,如果返回nil
,跳转至loc_64a67
。否则,表示问题已解决,继续向下执行
进入loc_64a67
流程
- 进入此流程,表示
forwardingTargetForSelector
未实现,或者快速转发流程未能解决问题 - 如果
methodSignatureForSelector
方法已实现,继续执行代码 - 进入慢速转发流程,调用
methodSignatureForSelector
方法,如果返回值不为nil
,继续向下执行
继续向下执行,进入loc_64ad5
流程
_forwardStackInvocation
方法为CF
中的内部方法,外部无法调用- 如果
_forwardStackInvocation
方法未实现,跳转至loc_64c19
进入loc_64c19
流程
- 进入快速转发流程的第二步,如果
forwardInvocation
方法已实现,调用该方法
4.2 resolveInstanceMethod两次调用
resolveInstanceMethod
方法的第一次调用,属于方法动态决议的正常流程。而第二次调用,在慢速转发流程的第一步,methodSignatureForSelector
方法调用之后出发
查看第二次resolveInstanceMethod
方法的函数调用栈:
CoreFoundation
框架:___forwarding___
→-[NSObject(NSObject) methodSignatureForSelector:]
→__methodDescriptionForSelector
→objc
:class_getInstanceMethod
→resolveMethod_locked
→resolveInstanceMethod
使用Hopper
打开CoreFoundation
可执行文件,顺着___forwarding___
流程,代码向下执行,找到methodSignatureForSelector:
方法
双击methodSignatureForSelector:
方法,选择-[NSObject(NSObject) methodSignatureForSelector:]
进入methodSignatureForSelector:
方法
进入___methodDescriptionForSelector
方法,代码向下执行,内部会调用objc
中的class_getInstanceMethod
函数
来到objc
源码,class_getInstanceMethod
函数中,内部调用了方法慢速查找的lookUpImpOrForward
函数
如果方法找不到,第二次进入方法动态决议流程,系统再一次给出挽救机会
影响方法决议第二次调用的因素
快速转发流程返回对象
- (id)forwardingTargetForSelector:(SEL)aSelector{
return [LGStudent alloc];
}
-------------------------
//输出结果:
resolveInstanceMethod:LGPerson,sayNB
forwardingTargetForSelector:<LGPerson: 0x28176c0e0>,sayNB
-[LGStudent sayNB]
- 不会进入二次调用流程
慢速转发methodSignatureForSelector
中,返回方法签名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
if(aSelector==@selector(sayNB)){
return [NSMethodSignature signatureWithObjCTypes:"v@:@"];
}
return [super methodSignatureForSelector:aSelector];
}
-------------------------
//输出结果:
resolveInstanceMethod:LGPerson,sayNB
forwardingTargetForSelector:<LGPerson: 0x281590070>,sayNB
methodSignatureForSelector:<LGPerson: 0x281590070>,sayNB
resolveInstanceMethod:LGPerson,_forwardStackInvocation:
forwardInvocation:<LGPerson: 0x281590070>,<LGPerson: 0x281590070>,sayNB
-[LGPerson sayNB]: unrecognized selector sent to instance 0x281590070
- 进入二次调用流程,查找
_forwardStackInvocation
方法,然后进入forwardInvocation
流程
慢速转发methodSignatureForSelector
中,返回nil
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
return [super methodSignatureForSelector:aSelector];
}
-------------------------
//输出结果:
resolveInstanceMethod:LGPerson,sayNB
forwardingTargetForSelector:<LGPerson: 0x280e2c020>,sayNB
methodSignatureForSelector:<LGPerson: 0x280e2c020>,sayNB
resolveInstanceMethod:LGPerson,sayNB
-[LGPerson sayNB]: unrecognized selector sent to instance 0x280e2c020
- 进入二次调用流程,再次查找之前的
sel
在消息转发流程中,对sel
进行imp
修复
- (void)say666{
NSLog(@"%@ - %s",self , __func__);
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
NSLog(@"methodSignatureForSelector:%@,%@", self, NSStringFromSelector(aSelector));
if(aSelector==@selector(sayNB)){
Class cls = objc_getClass(NSStringFromClass(self.class).UTF8String);
IMP sayNBImp = class_getMethodImplementation(cls, @selector(say666));
Method method = class_getInstanceMethod(cls, @selector(say666));
const char *type = method_getTypeEncoding(method);
class_addMethod(cls, aSelector, sayNBImp, type);
}
return [super methodSignatureForSelector:aSelector];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation{
NSLog(@"forwardInvocation:%@,%@,%@", self, anInvocation.target, NSStringFromSelector(anInvocation.selector));
if(anInvocation.selector==@selector(sayNB)){
[anInvocation invoke];
}
}
-------------------------
//输出结果:
resolveInstanceMethod:LGPerson,sayNB
forwardingTargetForSelector:<LGPerson: 0x2816e8040>,sayNB
methodSignatureForSelector:<LGPerson: 0x2816e8040>,sayNB
resolveInstanceMethod:LGPerson,_forwardStackInvocation:
forwardInvocation:<LGPerson: 0x2816e8040>,<LGPerson: 0x2816e8040>,sayNB
<LGPerson: 0x2816e8040> - -[LGPerson say666]
- 不会进入二次调用流程
- 一旦
sel
被修复,无论methodSignatureForSelector
方法是否实现,是否返回方法签名,都会进入forwardInvocation
流程
总结:
- 如果快速转发流程,
forwardingTargetForSelector
方法返回对象,不会再次进入方法动态决议流程 - 如果慢速转发流程,
methodSignatureForSelector
方法返回签名,再次进入方法动态决议流程,查找_forwardStackInvocation
方法,然后进入forwardInvocation
流程 - 如果慢速转发流程,
methodSignatureForSelector
方法返回nil
,再次进入方法动态决议流程,查找sayNB
方法 - 如果消息转发流程中,对
sel
进行修复,不会再次进入方法动态决议流程。一旦sel
被修复,无论methodSignatureForSelector
方法是否实现,是否返回方法签名,都会进入forwardInvocation
流程
总结
instrumentObjcMessageSends
辅助分析:
- 用于记录
sel
的执行日志,并输出文件到指定目录中 - 输出位置:
/tmp
目录下msgSends
开头的文件
快速转发:
forwardingTargetForSelector
:返回未找到的消息首选重定向的对象- 如果返回
self
,代码将陷入死循环
慢速转发:
methodSignatureForSelector
:返回一个NSMethodSignature
对象,该对象包含指定SEL
的方法签名
◦ 此方法配合resolveInstanceMethod:
一起使用,在消息转发期间必须创建NSInvocation
对象
resolveInstanceMethod
:被子类重写用于将消息转发到其他对象
◦ 创建一个代表该消息的NSInvocation
对象,可以选择将消息转发到另一个对象
◦ resolveInstanceMethod
可以暂不处理消息,但anInvocation
会保留。我们可以在后面适当的时机,继续用来消息的处理
doesNotRecognizeSelector
:处理接收者不能识别的消息
◦ 当对象接收到无法响应或转发的aSelector
消息时,运行时系统就会调用此方法。这个方法反过来引发NSInvalidArgumentException
,并生成一个错误消息
反汇编CoreFoundation
:
- 消息转发流程:
_CF_forwarding_prep_0
→___forwarding___
→forwardingTargetForSelector:
- 消息转发在
CoreFoundation
框架中调用,但CF
框架并没有完全开源,只能通过反汇编的方式进行探索
反汇编resolveInstanceMethod
两次调用:
resolveInstanceMethod
方法的第一次调用,属于方法动态决议的正常流程- 第二次调用,在慢速转发流程的第一步,
methodSignatureForSelector
方法调用之后出发
第二次resolveInstanceMethod
方法的函数调用栈:
CoreFoundation
框架:___forwarding___
→-[NSObject(NSObject) methodSignatureForSelector:]
→__methodDescriptionForSelector
→objc
:class_getInstanceMethod
→resolveMethod_locked
→resolveInstanceMethod
影响方法决议第二次调用的因素:
- 如果快速转发流程,
forwardingTargetForSelector
方法返回对象,不会再次进入方法动态决议流程 - 如果慢速转发流程,
methodSignatureForSelector
方法返回签名,再次进入方法动态决议流程,查找_forwardStackInvocation
方法,然后进入forwardInvocation
流程 - 如果慢速转发流程,
methodSignatureForSelector
方法返回nil
,再次进入方法动态决议流程,查找sayNB
方法 - 如果消息转发流程中,对
sel
进行修复,不会再次进入方法动态决议流程。一旦sel
被修复,无论methodSignatureForSelector
方法是否实现,是否返回方法签名,都会进入forwardInvocation
流程