iOS 底层探索11——消息转发

前言

当我们在classcachemethodlist中都查找不到对应的方法时,会执行消息处理流程消息处理流程主要包括动态方法决议快速消息转发慢速消息转发;我们已经了解了动态方法决议,本文将分析下快速消息转发慢速消息转发相关的内容;

通过objc方法log查看信息

  1. 上一篇文章我们已经知道了oc底层调用方法时可以控制是否记录日志,通过以下代码我们开启了日志

// 慢速查找 
extern void instrumentObjcMessageSends(BOOL flag);

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        GCPerson *person = [GCPerson alloc];
        [GCPerson say];
        NSLog(@"Hello, World!");
    }
    return 0;
}
复制代码
  1. 由于我们并未在GCPerson中实现方法say:所以系统会因为方法找不到而抛出异常;

image.png
3. 但是在抛出异常前也是会记录调用日志的,我们已经知道路径日志的是/tmp/msgSends,通过cmd+shift+g到对应目录下查看日志

image.png
4. 最终发现在抛出异常前,调用了+ GCPerson NSObject forwardingTargetForSelector:+ GCPerson NSObject methodSignatureForSelector:

消息转发

快速转发forwardingTargetForSelector

  1. 通过查阅官方文档我们大概知道了forwardingTargetForSelector方法的作用,在当前类无法找到方法实现时,可以将方法转交给其他对象来进行处理;
If an object implements (or inherits) this method, and returns a non-nil (and non-self) result, that returned object is used as the new receiver object and the message dispatch resumes to that new object. (Obviously if you return self from this method, the code would just fall into an infinite loop.)
If you implement this method in a non-root class, if your class has nothing to return for the given selector then you should return the result of invoking super’s implementation.
This method gives an object a chance to redirect an unknown message sent to it before the much more expensive forwardInvocation: machinery takes over. This is useful when you simply want to redirect messages to another object and can be an order of magnitude faster than regular forwarding. It is not useful where the goal of the forwarding is to capture the NSInvocation, or manipulate the arguments or return value during the forwarding.
翻译
如果一个对象实现(或继承)此方法,并返回一个非nil(和非self)结果,则该返回的对象将用作新的接收方对象,消息分派将继续到该新对象。(显然,如果从这个方法返回self,代码将陷入无限循环。)
如果你在非根类中实现这个方法,如果你的类对于给定的选择器没有返回任何东西,那么你应该返回调用super的实现的结果。
该方法使对象有机会在昂贵得多的forwardInvocation:机制接管之前,重定向发送给它的未知消息。当您只想将消息重定向到另一个对象,并且比常规转发快一个数量级时,这很有用。如果转发的目标是捕获NSInvocation,或者在转发期间操作参数或返回值,那么它就没有用了。
复制代码
  1. forwardingTargetForSelector 支持类方法对象方法两种形式
  • 对象方法重定向到某个实例对象时,需要实现-forwardingTargetForSelector并返回对应的实例对象
  • 类方法重定向到某个类对象时,需要实现+forwardingTargetForSelector并返回对应的类对象
- (id)forwardingTargetForSelector:(SEL)aSelector {
    NSLog(@"%s-%@",__func__, NSStringFromSelector(aSelector));
    if (aSelector == @selector(sayHello)) {
        return [GCStudent alloc];//返回可以处理当前方法的实例对象
    }
    return [super forwardingTargetForSelector:aSelector];
}

+ (id)forwardingTargetForSelector:(SEL)aSelector {
    NSLog(@"%s-%@",__func__, NSStringFromSelector(aSelector));
    if (aSelector == @selector(say666)) {
        return [GCStudent class];//返回可以处理当前方法的类对象
    }
    return [super forwardingTargetForSelector:aSelector];
}
复制代码
  1. 快速转发的应用场景一般比较少,因为如果当前类找不到方法,就通过快速转发来转给其他类,这种场景下其他类也不太可能会实现当前缺失的方法,所以,被转发的类相当于是背锅侠

慢速转发 methodSignatureForSelector && forwardInvocation

  1. 快速转发找不到合适的类进行转发,即没有合适的背锅侠时候我们会进入消息慢速转发methodSignatureForSelectorforwardInvocation

image.png

  1. methodSignatureForSelectorforwardInvocation都有对象方法类方法两种形式,且methodSignatureForSelectorforwardInvocation必须成对出现;
  2. methodSignatureForSelector里面需要返回当前方法签名,由于对签名的要求并不严格,只要返回可识别的签名都可以往下执行,所以这里的签名可以不加参数长度的信息;注意只有返回了签名信息forwardInvocation才会执行
  3. 使用慢速转发的具体代码如下
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    if (aSelector == @selector(sayHello)) {
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }
    return [super methodSignatureForSelector:aSelector];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    NSLog(@"forwardInvocation");
}

+ (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    if (aSelector == @selector(say666)) {
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }
    return [super methodSignatureForSelector:aSelector];
}

+ (void)forwardInvocation:(NSInvocation *)anInvocation {
    NSLog(@"forwardInvocation");
}
复制代码
  1. 慢速消息转发触发时,我们可以在+ (void)forwardInvocation:做更多的事,进行更灵活的处理,让转发的消息尽可能的沿着正常的逻辑执行;
- (void)forwardInvocation:(NSInvocation *)anInvocation {
//根据实际需要进行灵活处理;
    GCStudent *student = [GCStudent alloc];
    if ([self respondsToSelector:anInvocation.selector]) {//如果当前类可以处理交给当前类
        [anInvocation invoke];
    }else if ([student respondsToSelector:anInvocation.selector]){
        [anInvocation invokeWithTarget:student];//如果student可以处理方法,交给student进行处理;
    }
    NSLog(@"forwardInvocation");
}
复制代码

消息转发使用场景

  1. 不管是快速消息转发还是慢速消息转发,都只能作为一种异常处理的手段,而不能作为最终解决办法;当代码执行到这里的时候即便通过消息转发可以让应用不崩溃,但是应用本身的业务将会处于混乱不可控的状态;
  2. 消息转发由于机制的特殊性需要不断的向父类查找,对性能也是一种浪费;
  3. 建议将消息机制作为一种容错手段不要过度依赖

doesNotRecognizeSelector

  1. doesNotRecognizeSelector作为最后抛出异常的一个方法,重写它可以在抛出异常前收到相关信息;
  2. doesNotRecognizeSelector也有对应的+方法-方法两种形式;
  3. doesNotRecognizeSelector但并不能够真正对异常做任何有效的处理,重写这个方法对实际的容错机制意义不大

消息转发三重防护流程图

动态决议消息快速转发消息慢速转发最终的流程图如下

消息转发流程.png

消息机制的探索过程分析理解

  1. 当方法无法找到发生崩溃时,通过查看堆栈信息,发现问题出在CoreFoundation___forwarding___CoreFoundation __forwarding_prep_0___方法中

image.png

  1. 通过使用hopper反汇编CoreFoundation库,搜索___forwarding_prep_0___查看对应信息;这里不再过多说明,有兴趣的可以从Xcode内置的corefoundation中查看对应的反汇编信息;

image.png
3.

  • 僵尸对象:内存已经被回收的对象;
  • 野指针:指向僵尸对象的指针;
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享