1. instrumentObjcMessageSends辅助分析

objc源码中的logMessageSend函数,用于记录sel的执行日志,并输出文件到指定目录中

  1. bool logMessageSend(bool isClassMethod,
  2. const char *objectsClass,
  3. const char *implementingClass,
  4. SEL selector)
  5. {
  6. char buf[ 1024 ];
  7. // Create/open the log file
  8. if (objcMsgLogFD == (-1))
  9. {
  10. snprintf (buf, sizeof(buf), "/tmp/msgSends-%d", (int) getpid ());
  11. objcMsgLogFD = secure_open (buf, O_WRONLY | O_CREAT, geteuid());
  12. if (objcMsgLogFD < 0) {
  13. // no log file - disable logging
  14. objcMsgLogEnabled = false;
  15. objcMsgLogFD = -1;
  16. return true;
  17. }
  18. }
  19. // Make the log entry
  20. snprintf(buf, sizeof(buf), "%c %s %s %s\n",
  21. isClassMethod ? '+' : '-',
  22. objectsClass,
  23. implementingClass,
  24. sel_getName(selector));
  25. objcMsgLogLock.lock();
  26. write (objcMsgLogFD, buf, strlen(buf));
  27. objcMsgLogLock.unlock();
  28. // Tell caller to not cache the method
  29. return false;
  30. }

在写入缓存的log_and_fill_cache函数中,如果objcMsgLogEnabled为真,且implementer存在,调用logMessageSend函数

  1. static void
  2. log_and_fill_cache(Class cls, IMP imp, SEL sel, id receiver, Class implementer)
  3. {
  4. #if SUPPORT_MESSAGE_LOGGING
  5. if (slowpath(objcMsgLogEnabled && implementer)) {
  6. bool cacheIt = logMessageSend(implementer->isMetaClass(),
  7. cls->nameForLogging(),
  8. implementer->nameForLogging(),
  9. sel);
  10. if (!cacheIt) return;
  11. }
  12. #endif
  13. cls->cache.insert(sel, imp, receiver);
  14. }
  • 参数implementer在正常情况下一定存在,所以进入logMessageSend函数的关键条件取决于objcMsgLogEnabled的值

objc源码中,找到objcMsgLogEnabled的赋值

  1. void instrumentObjcMessageSends(BOOL flag)
  2. {
  3. bool enable = flag;
  4. // Shortcut NOP
  5. if (objcMsgLogEnabled == enable)
  6. return;
  7. // If enabling, flush all method caches so we get some traces
  8. if (enable)
  9. _objc_flush_caches(Nil);
  10. // Sync our log file
  11. if (objcMsgLogFD != -1)
  12. fsync (objcMsgLogFD);
  13. objcMsgLogEnabled = enable;
  14. }
  • 将参数flag赋值给objcMsgLogEnabled

不难看出,调用objc中的instrumentObjcMessageSends函数,将入参flag传入true,即可记录sel的调用日志,并输出文件到指定目录中

1.1 导出日志

搭建测试工程,在LGPerson中定义say666实例方法,但不实现该方法

main.m文件中,写入以下代码:

  1. #import <Foundation/Foundation.h>
  2. #import "LGPerson.h"
  3. extern void instrumentObjcMessageSends(BOOL flag);
  4. int main(int argc, const char * argv[]) {
  5. @autoreleasepool {
  6. LGPerson *per = [LGPerson alloc];
  7. instrumentObjcMessageSends(YES);
  8. [per say666];
  9. instrumentObjcMessageSends(NO);
  10. }
  11. return 0;
  12. }
  13. -------------------------
  14. //输出结果:
  15. -[LGPerson say666]: unrecognized selector sent to instance 0x10071b990

1.2 辅助分析

打开/tmp目录,找到msgSends开头的文件

  1. + LGPerson NSObject resolveInstanceMethod:
  2. + LGPerson NSObject resolveInstanceMethod:
  3. - LGPerson NSObject forwardingTargetForSelector:
  4. - LGPerson NSObject forwardingTargetForSelector:
  5. - LGPerson NSObject methodSignatureForSelector:
  6. - LGPerson NSObject methodSignatureForSelector:
  7. + LGPerson NSObject resolveInstanceMethod:
  8. + LGPerson NSObject resolveInstanceMethod:
  9. - LGPerson NSObject doesNotRecognizeSelector:
  10. - LGPerson NSObject doesNotRecognizeSelector:
  • 日志中的每一个方法都会打印两次,此问题暂且无视

日志中,当调用的方法找不到时,在方法动态决议流程之后,系统还调用了消息转发流程,通过快速转发和慢速转发,给开发者另外两次挽救机会

2. 快速转发

forwardingTargetForSelector:返回未找到的消息首选重定向的对象

如果一个对象实现或继承此方法,并返回一个非nil和非self结果,则该返回的对象将用作新的接收方对象,消息将发送给该新对象。如果从这个方法返回self,代码将陷入死循环

如果你在非根类中实现这个方法,如果你的类对于给定的选择器没有返回任何东西,那么你应该返回调用super的实现的结果

该方法在慢速转发forwardInvocation:机制触发之前,重定向发送给它的未知消息。如果只想将消息重定向到另一个对象,并且比常规转发快一个数量级时,这是最好的选择。如果转发的目标是捕获NSInvocation,或者在转发期间操作参数或返回值,那么它就没有用了

LGStudent中,实现say666实例方法

  1. #import "LGStudent.h"
  2. @implementation LGStudent
  3. - (void)say666{
  4. NSLog(@"%s",__func__);
  5. }
  6. @end

LGPerson中,实现forwardingTargetForSelector方法,并重定向给LGStudent实例对象

  1. #import "LGPerson.h"
  2. #import "LGStudent.h"
  3. @implementation LGPerson
  4. - (id)forwardingTargetForSelector:(SEL)aSelector{
  5. NSLog(@"forwardingTargetForSelector:%@,%@", self, NSStringFromSelector(aSelector));
  6. return [LGStudent alloc];
  7. }
  8. @end
  9. -------------------------
  10. //输出结果:
  11. forwardingTargetForSelector:<LGPerson: 0x280078090>,say666
  12. -[LGStudent say666]

3. 慢速转发

3.1 methodSignatureForSelector

返回一个NSMethodSignature对象,该对象包含指定SEL的方法签名

该方法用于协议的实现。此方法配合resolveInstanceMethod:一起使用,在消息转发期间必须创建NSInvocation对象。如果您的对象维护一个委托或能够处理它没有直接实现的消息,您应该重写此方法以返回适当的方法签名

案例

LGPerson中,实现methodSignatureForSelector方法

  1. @implementation LGPerson
  2. - (id)forwardingTargetForSelector:(SEL)aSelector{
  3. NSLog(@"forwardingTargetForSelector:%@,%@", self, NSStringFromSelector(aSelector));
  4. // return [LGStudent alloc];
  5. return [super forwardingTargetForSelector:aSelector];
  6. }
  7. - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
  8. NSLog(@"methodSignatureForSelector:%@,%@", self, NSStringFromSelector(aSelector));
  9. return [super methodSignatureForSelector:aSelector];
  10. }
  11. @end
  12. -------------------------
  13. //输出结果:
  14. forwardingTargetForSelector:<LGPerson: 0x28182c090>,say666
  15. methodSignatureForSelector:<LGPerson: 0x28182c090>,say666
  16. -[LGPerson say666]: unrecognized selector sent to instance 0x28182c090
  • 如果想在LGPerson中触发methodSignatureForSelector,必须保证在forwardingTargetForSelector中,不能重定向到其他对象。即使重定向的对象未实现该方法,也会进入该对象的消息处理流程中
  • 没有执行成功,需要返回方法签名,并配合forwardInvocation:一起使用

3.2 forwardInvocation

被子类重写用于将消息转发到其他对象

当向一个对象发送了一条它没有相应方法的消息时,运行时系统给接收方一个机会将消息委托给另一个接收方。它通过创建一个代表该消息的NSInvocation对象,并向接收者发送一个包含该NSInvocation对象作为参数的forwardInvocation:消息来委托该消息。然后,接收方的forwardInvocation:方法可以选择将消息转发到另一个对象。如果该对象也不能响应消息,它也将有机会转发它

forwardInvocation:方法的实现有两个任务:

  • 定位可以响应调用中编码的消息的对象。该对象不必对所有消息都相同
  • 使用invocation将消息发送到该对象。anInvocation将保存结果,运行时系统将提取该结果并将其交付给原始发送方

LGPerson中,返回方法签名,并实现forwardInvocation方法

  1. - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
  2. NSLog(@"methodSignatureForSelector:%@,%@", self, NSStringFromSelector(aSelector));
  3. if(aSelector==@selector(say666)){
  4. return [NSMethodSignature signatureWithObjCTypes:"v@:"];
  5. }
  6. return [super methodSignatureForSelector:aSelector];
  7. }
  8. - (void)forwardInvocation:(NSInvocation *)anInvocation{
  9. NSLog(@"forwardInvocation:%@,%@,%@", self, anInvocation.target, NSStringFromSelector(anInvocation.selector));
  10. }
  11. -------------------------
  12. //输出结果:
  13. forwardingTargetForSelector:<LGPerson: 0x280ab0090>,say666
  14. methodSignatureForSelector:<LGPerson: 0x280ab0090>,say666
  15. forwardInvocation:<LGPerson: 0x280ab0090>,<LGPerson: 0x280ab0090>,say666
  • 解决系统崩溃的问题,也打印出指定方法中的Log,但此时并没有对say666方法进行处理

在系统层面,所有的方法和函数统称系统消息,也可称之为事务。对事务来说,我们可以立即处理,也可暂不处理。但anInvocation会保留,我们可以在后面适当的时机,继续用来消息的处理

案例

慢速转发机制,通过methodSignatureForSelector:获得方法签名,创建要转发的NSInvocation对象。所以我们需要预先提供正确的方法签名

LGStudent中,实现say:方法,传入NSString参数

  1. #import "LGStudent.h"
  2. @implementation LGStudent
  3. - (void)say:(NSString *)str{
  4. NSLog(@"%s,say:%@", __func__, str);
  5. }
  6. @end

methodSignatureForSelector方法中,返回正确的方法签名

  1. - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
  2. NSLog(@"methodSignatureForSelector:%@,%@", self, NSStringFromSelector(aSelector));
  3. if(aSelector==@selector(say666)){
  4. return [NSMethodSignature signatureWithObjCTypes:"v@:@"];
  5. }
  6. return [super methodSignatureForSelector:aSelector];
  7. }

forwardInvocation方法中,对消息进行转发处理

  1. - (void)forwardInvocation:(NSInvocation *)anInvocation{
  2. if(anInvocation.selector==@selector(say666)){
  3. LGStudent *s = [LGStudent alloc];
  4. anInvocation.target=s;
  5. anInvocation.selector=@selector(say:);
  6. NSString *str = @"hahaha~";
  7. [anInvocation setArgument:&str atIndex:2];
  8. [anInvocation invoke];
  9. return;
  10. }
  11. NSLog(@"forwardInvocation:%@,%@,%@", self, anInvocation.target, NSStringFromSelector(anInvocation.selector));
  12. }
  13. -------------------------
  14. //输出结果:
  15. forwardingTargetForSelector:<LGPerson: 0x283918110>,say666
  16. methodSignatureForSelector:<LGPerson: 0x283918110>,say666
  17. -[LGStudent say:],sayhahaha~
  • 传入参数的index2,因为01objc_msgSendself_cmd占用

forwardInvocation:的实现可以做的不仅仅是转发消息,还可以用于合并响应各种不同消息的代码,从而避免为每个选择器编写单独的方法的必要性。在对给定消息的响应中,forwardInvocation:方法还可能涉及多个其他对象,而不是将其转发给一个对象

NSObjectforwardInvocation实现:内部调用doesNotRecognizeSelector:方法,它不转发任何信息。因此,如果不实现forwardInvocation:,那么向对象发送无法识别的消息将引发异常

3.3 doesNotRecognizeSelector

处理接收者不能识别的消息

当对象接收到无法响应或转发的aSelector消息时,运行时系统就会调用此方法。这个方法反过来引发NSInvalidArgumentException,并生成一个错误消息

案例

LGPerson中,定义并实现sayNB方法

  1. - (void)sayNB {
  2. NSLog(@"如果不覆盖此方法,将会得到一个异常");
  3. [self doesNotRecognizeSelector:_cmd];
  4. }

LGStudent继承自LGPerson,如果子类没有重写父类方法,调用该方法,此时就会抛出异常

  1. LGStudent *s = [LGStudent alloc];
  2. [s sayNB];
  3. -------------------------
  4. //输出结果:
  5. 如果不覆盖此方法,将会得到一个异常
  6. -[LGStudent sayNB]: unrecognized selector sent to instance 0x280f48070

4. 反汇编探索

4.1 CoreFoundation

如何查看消息转发的调用流程?

forwardingTargetForSelector中设置断点,查看函数调用栈
image.png

消息转发流程:_CF_forwarding_prep_0___forwarding___forwardingTargetForSelector:

消息转发在CoreFoundation框架中调用,但CF框架并没有完全开源,我们只能通过反汇编的方式进行探索

先使用真机或模拟器运行项目,得到CF框架可执行文件的路径

  1. image list
  2. -------------------------
  3. [ 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
image.png

找到_CF_forwarding_prep_0函数,使用伪代码查看
image.png

  • 方法内部调用___forwarding___函数

进入___forwarding___函数
image.png

  • 如果forwardingTargetForSelector方法未实现,继续执行代码。否则,跳转至loc_64a67
  • 进入快速转发流程,调用forwardingTargetForSelector方法,如果返回nil,跳转至loc_64a67。否则,表示问题已解决,继续向下执行

进入loc_64a67流程
image.png

  • 进入此流程,表示forwardingTargetForSelector未实现,或者快速转发流程未能解决问题
  • 如果methodSignatureForSelector方法已实现,继续执行代码
  • 进入慢速转发流程,调用methodSignatureForSelector方法,如果返回值不为nil,继续向下执行

继续向下执行,进入loc_64ad5流程
image.png

  • _forwardStackInvocation方法为CF中的内部方法,外部无法调用
  • 如果_forwardStackInvocation方法未实现,跳转至loc_64c19

进入loc_64c19流程
image.png

  • 进入快速转发流程的第二步,如果forwardInvocation方法已实现,调用该方法

4.2 resolveInstanceMethod两次调用

resolveInstanceMethod方法的第一次调用,属于方法动态决议的正常流程。而第二次调用,在慢速转发流程的第一步,methodSignatureForSelector方法调用之后出发

查看第二次resolveInstanceMethod方法的函数调用栈:

  • CoreFoundation框架:___forwarding___-[NSObject(NSObject) methodSignatureForSelector:]__methodDescriptionForSelector
  • objcclass_getInstanceMethodresolveMethod_lockedresolveInstanceMethod

使用Hopper打开CoreFoundation可执行文件,顺着___forwarding___流程,代码向下执行,找到methodSignatureForSelector:方法

双击methodSignatureForSelector:方法,选择-[NSObject(NSObject) methodSignatureForSelector:]
image.png

进入methodSignatureForSelector:方法
image.png

进入___methodDescriptionForSelector方法,代码向下执行,内部会调用objc中的class_getInstanceMethod函数
image.png

来到objc源码,class_getInstanceMethod函数中,内部调用了方法慢速查找的lookUpImpOrForward函数
image.png

如果方法找不到,第二次进入方法动态决议流程,系统再一次给出挽救机会

影响方法决议第二次调用的因素

快速转发流程返回对象

  1. - (id)forwardingTargetForSelector:(SEL)aSelector{
  2. return [LGStudent alloc];
  3. }
  4. -------------------------
  5. //输出结果:
  6. resolveInstanceMethodLGPersonsayNB
  7. forwardingTargetForSelector:<LGPerson: 0x28176c0e0>,sayNB
  8. -[LGStudent sayNB]
  • 不会进入二次调用流程

慢速转发methodSignatureForSelector中,返回方法签名

  1. - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
  2. if(aSelector==@selector(sayNB)){
  3. return [NSMethodSignature signatureWithObjCTypes:"v@:@"];
  4. }
  5. return [super methodSignatureForSelector:aSelector];
  6. }
  7. -------------------------
  8. //输出结果:
  9. resolveInstanceMethodLGPersonsayNB
  10. forwardingTargetForSelector:<LGPerson: 0x281590070>,sayNB
  11. methodSignatureForSelector:<LGPerson: 0x281590070>,sayNB
  12. resolveInstanceMethodLGPerson_forwardStackInvocation:
  13. forwardInvocation:<LGPerson: 0x281590070>,<LGPerson: 0x281590070>,sayNB
  14. -[LGPerson sayNB]: unrecognized selector sent to instance 0x281590070
  • 进入二次调用流程,查找_forwardStackInvocation方法,然后进入forwardInvocation流程

慢速转发methodSignatureForSelector中,返回nil

  1. - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
  2. return [super methodSignatureForSelector:aSelector];
  3. }
  4. -------------------------
  5. //输出结果:
  6. resolveInstanceMethodLGPersonsayNB
  7. forwardingTargetForSelector:<LGPerson: 0x280e2c020>,sayNB
  8. methodSignatureForSelector:<LGPerson: 0x280e2c020>,sayNB
  9. resolveInstanceMethodLGPersonsayNB
  10. -[LGPerson sayNB]: unrecognized selector sent to instance 0x280e2c020
  • 进入二次调用流程,再次查找之前的sel

在消息转发流程中,对sel进行imp修复

  1. - (void)say666{
  2. NSLog(@"%@ - %s",self , __func__);
  3. }
  4. - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
  5. NSLog(@"methodSignatureForSelector:%@,%@", self, NSStringFromSelector(aSelector));
  6. if(aSelector==@selector(sayNB)){
  7. Class cls = objc_getClass(NSStringFromClass(self.class).UTF8String);
  8. IMP sayNBImp = class_getMethodImplementation(cls, @selector(say666));
  9. Method method = class_getInstanceMethod(cls, @selector(say666));
  10. const char *type = method_getTypeEncoding(method);
  11. class_addMethod(cls, aSelector, sayNBImp, type);
  12. }
  13. return [super methodSignatureForSelector:aSelector];
  14. }
  15. - (void)forwardInvocation:(NSInvocation *)anInvocation{
  16. NSLog(@"forwardInvocation:%@,%@,%@", self, anInvocation.target, NSStringFromSelector(anInvocation.selector));
  17. if(anInvocation.selector==@selector(sayNB)){
  18. [anInvocation invoke];
  19. }
  20. }
  21. -------------------------
  22. //输出结果:
  23. resolveInstanceMethodLGPersonsayNB
  24. forwardingTargetForSelector:<LGPerson: 0x2816e8040>,sayNB
  25. methodSignatureForSelector:<LGPerson: 0x2816e8040>,sayNB
  26. resolveInstanceMethodLGPerson_forwardStackInvocation:
  27. forwardInvocation:<LGPerson: 0x2816e8040>,<LGPerson: 0x2816e8040>,sayNB
  28. <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
  • objcclass_getInstanceMethodresolveMethod_lockedresolveInstanceMethod

影响方法决议第二次调用的因素:

  • 如果快速转发流程,forwardingTargetForSelector方法返回对象,不会再次进入方法动态决议流程
  • 如果慢速转发流程,methodSignatureForSelector方法返回签名,再次进入方法动态决议流程,查找_forwardStackInvocation方法,然后进入forwardInvocation流程
  • 如果慢速转发流程,methodSignatureForSelector方法返回nil,再次进入方法动态决议流程,查找sayNB方法
  • 如果消息转发流程中,对sel进行修复,不会再次进入方法动态决议流程。一旦sel被修复,无论methodSignatureForSelector方法是否实现,是否返回方法签名,都会进入forwardInvocation流程