1. instrumentObjcMessageSends辅助分析
objc源码中的logMessageSend函数,用于记录sel的执行日志,并输出文件到指定目录中
bool logMessageSend(bool isClassMethod,const char *objectsClass,const char *implementingClass,SEL selector){char buf[ 1024 ];// Create/open the log fileif (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 loggingobjcMsgLogEnabled = false;objcMsgLogFD = -1;return true;}}// Make the log entrysnprintf(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 methodreturn false;}
在写入缓存的log_and_fill_cache函数中,如果objcMsgLogEnabled为真,且implementer存在,调用logMessageSend函数
static voidlog_and_fill_cache(Class cls, IMP imp, SEL sel, id receiver, Class implementer){#if SUPPORT_MESSAGE_LOGGINGif (slowpath(objcMsgLogEnabled && implementer)) {bool cacheIt = logMessageSend(implementer->isMetaClass(),cls->nameForLogging(),implementer->nameForLogging(),sel);if (!cacheIt) return;}#endifcls->cache.insert(sel, imp, receiver);}
- 参数
implementer在正常情况下一定存在,所以进入logMessageSend函数的关键条件取决于objcMsgLogEnabled的值
objc源码中,找到objcMsgLogEnabled的赋值
void instrumentObjcMessageSends(BOOL flag){bool enable = flag;// Shortcut NOPif (objcMsgLogEnabled == enable)return;// If enabling, flush all method caches so we get some tracesif (enable)_objc_flush_caches(Nil);// Sync our log fileif (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>,say666methodSignatureForSelector:<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>,say666methodSignatureForSelector:<LGPerson: 0x280ab0090>,say666forwardInvocation:<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>,say666methodSignatureForSelector:<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,sayNBforwardingTargetForSelector:<LGPerson: 0x28176c0e0>,sayNB-[LGStudent sayNB]
- 不会进入二次调用流程
慢速转发methodSignatureForSelector中,返回方法签名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{if(aSelector==@selector(sayNB)){return [NSMethodSignature signatureWithObjCTypes:"v@:@"];}return [super methodSignatureForSelector:aSelector];}-------------------------//输出结果:resolveInstanceMethod:LGPerson,sayNBforwardingTargetForSelector:<LGPerson: 0x281590070>,sayNBmethodSignatureForSelector:<LGPerson: 0x281590070>,sayNBresolveInstanceMethod: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,sayNBforwardingTargetForSelector:<LGPerson: 0x280e2c020>,sayNBmethodSignatureForSelector:<LGPerson: 0x280e2c020>,sayNBresolveInstanceMethod: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,sayNBforwardingTargetForSelector:<LGPerson: 0x2816e8040>,sayNBmethodSignatureForSelector:<LGPerson: 0x2816e8040>,sayNBresolveInstanceMethod: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流程
