底层原理-11-objc_msgSend之消息转发

1.instrumentObjcMessageSends分析

之前我们在慢速查找中没有找到imp会进行方法决议,但是如果方法决议中还是没有处理怎么办?
我们在慢速查找中会把找到的方法进行缓存,log_and_fill_cache

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);
}
复制代码

里面有一个objcMsgLogEnabled 的判断,但是bool objcMsgLogEnabled = false。全局搜索objcMsgLogEnabled的赋值情况找到了instrumentObjcMessageSends里面对它进行了改变

void instrumentObjcMessageSends(BOOL flag)
{
    bool enable = flag;

    // Shortcut NOP
    if (objcMsgLogEnabled == enable)//objcMsgLogEnabled默认false
        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);//写入log日志

    objcMsgLogEnabled = enable;//赋值
}
复制代码

我们要暴露出方法的缓存日志的话把instrumentObjcMessageSends设置为yes就能得到方法的缓存日志。我们直接使用找不到这个方法,我们这个时候就要暴露出这个方法

extern void instrumentObjcMessageSends(BOOL flag);
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        
        KBStudent *student = [KBStudent alloc];
        instrumentObjcMessageSends(YES);
        [student sayHello];
        instrumentObjcMessageSends(NO);
    }
    return 0;
}
复制代码

我们只要关注这个方法即可,调用完在进行关闭。在方法logMessageSend 说了日志的路径

  snprintf (buf, sizeof(buf), "/tmp/msgSends-%d", (int) getpid ());
复制代码

截屏2021-07-03 上午10.58.13.png
得到

截屏2021-07-03 上午10.59.45.png
我们只要关注KBStudent就好,resolveInstanceMethod->forwardingTargetForSelector->methodSignatureForSelector->resolveInstanceMethod->doesNotRecognizeSelector这里也可以解释为社么之前resolveInstanceMethod走了次

2.forwardingTargetForSelector分析

源码搜索没有发现介绍,只有NSObject中实现了。我们Command + shift + 0(是数字0)查询官方文档关于这个方法的介绍

截屏2021-07-03 上午11.15.41.png
返回一个对象,对于原对象没有实现这个消息,让这个对象试试,也就是方法重定向。实例对象A没实现这个方法,那么我让实例对象B试试,返回一个实例对象B。

#import "KBPerson.h"
@implementation KBStudent

-(id)forwardingTargetForSelector:(SEL)aSelector
{
    return [KBPerson alloc];
}
@end
复制代码
2021-07-03 11:23:03.232618+0800 消息转发[19642:2342779] -[KBPerson sayHello]
复制代码

KBPerson中实现了sayHello方法,没有崩溃。这就行方法快速转发.类方法一样

+ (id)forwardingTargetForSelector:(SEL)aSelector
{
    return KBPerson.class;
}
复制代码

但是实际情况,我们没办法判断哪个类实现了,哪个类没有实现。我们可以创建一个类,给这个类添加未实现的方法,统一处理。可以创建一个类,添加统一处理的类方法实例方法。在NSObjectCategory中实现forwardingTargetForSelector的类方法和实例方法。

#import "NSObject+AvoidCrash.h"
#import "KBPerson.h"
#import "KBErrorObject.h"
#import <objc/message.h>
@implementation NSObject (AvoidCrash)
-(id)forwardingTargetForSelector:(SEL)aSelector
{
    KBErrorObject * error =  [KBErrorObject alloc];

    IMP imp = class_getMethodImplementation(KBErrorObject.class, @selector(sayNB));
    Method method = class_getInstanceMethod(KBErrorObject.class, @selector(sayNB));
    const char *type = method_getTypeEncoding(method);

    class_addMethod(KBErrorObject.class, aSelector, imp, type);


    return error;
}

+ (id)forwardingTargetForSelector:(SEL)aSelector
{
    Class metaClas = object_getClass(KBErrorObject.class);
    IMP imp = class_getMethodImplementation(metaClas, @selector(say666));
    Method method = class_getClassMethod(metaClas, @selector(say666));
    const char *type = method_getTypeEncoding(method);
    class_addMethod(metaClas, aSelector, imp, type);
    return KBErrorObject.class;
}
@end
复制代码

这样在调用的时候就可以做统一处理了

截屏2021-07-03 下午1.56.05.png
方法的快速转发是苹果让我们在动态决议没有实现的时候,让我们换个对象来处理这个消息。替代者去实现

2.methodSignatureForSelector分析

接着之前崩溃日志,如果快速转发没有实现的话,会调用methodSignatureForSelector

截屏2021-07-03 下午2.22.10.png
此方法也用于必须创建NSInvocation对象的情况,没有实现对应的方法时候我们重写此方法以返回适当的方法签名。可以不对这个方法进行处理,值得注意的是要解决问题还要依赖forwardInvocation的实现。

截屏2021-07-03 下午2.45.28.png
我们在NSObjectCategory中实现签名forwardInvocation

-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
    NSMethodSignature * signature = [NSMethodSignature signatureWithObjCTypes:"v@:*"];
    
    return  signature;
}
-(void)forwardInvocation:(NSInvocation *)anInvocation
{
    NSLog(@"%s",__func__);
}
复制代码

没有实现消息转发,也没有发送崩溃。相当于告诉系统这个没实现的方法我知道了,不用处理了。但是如果想处理也是可以的。

-(void)forwardInvocation:(NSInvocation *)anInvocation
{
    anInvocation.target = [KBPerson alloc];
    
    [anInvocation invoke];
    
   
}
复制代码
2021-07-03 15:03:40.337782+0800 消息转发[20554:2455376] -[KBPerson sayHello]

复制代码

相当于告诉说这个方法让KBPerson实例对象去实现,和快速转发一样进行重定向,找别人来实现。类方法也是一样。

3.Hopper分析

这个是通过源码分析出方法崩溃流程,我们下面通过Hopper分析一下
我们把CoreFoundation拖入Hopper搜索forward。选择__forwarding__后查看伪代码实现截屏2021-07-05 上午11.32.59.png
在动态决议失败后进入快速转发。会进行系统的loc_64a67进入慢速转发流程
截屏2021-07-05 上午11.40.26.png
判断methodSignatureForSelector方法签名是否实现。实现了,进行forwardStackInvocation:但是在重写的时候没有响应,应该是系统的方法。继续查看forwardInvocation:是否实现(转发求助,进行方法重定向)。没有实现最终调用doesNotRecognizeSelector:报错。

3.1关于动态决议在methodSignatureForSelector之后再走一次分析

截屏2021-07-03 上午10.59.45.png
methodSignatureForSelector后又走了一次动态决议,我们用Hopper分析一下
点击methodSignatureForSelector选择实例方法
截屏2021-07-05 下午2.20.31.png
得到截屏2021-07-05 下午2.21.44.png
查看___methodDescriptionForSelector具体实现

截屏2021-07-05 下午2.24.16.png
如果没有实现跳转loc_7c68b得到

截屏2021-07-05 下午2.26.39.png
在源码搜索

Method class_getInstanceMethod(Class cls, SEL sel)
{
    if (!cls  ||  !sel) return nil;

    // This deliberately avoids +initialize because it historically did so.

    // This implementation is a bit weird because it's the only place that 
    // wants a Method instead of an IMP.

#warning fixme build and search caches
        
    // Search method lists, try method resolver, etc.
    lookUpImpOrForward(nil, sel, cls, LOOKUP_RESOLVER);

#warning fixme build and search caches

    return _class_getMethod(cls, sel);
}
复制代码

会进行慢速查找流程lookUpImpOrForward,这就是后面再次进行动态决议的原因。

4.总结

  • 在慢速查找没有找到会进行方法的动态决议resolveInstanceMethodresolveClassMethod,就是给当前方法重新赋值一个实现的imp。
  • 动态决议失败后会进行消息的快速转发forwardingTargetForSelector,就是让别的对象或者类来实现这个方法,也就是方法重定向
  • 快速转发失败进入慢速转发。首先methodSignatureForSelector给方法重新签名,必须要重写forwardInvocation。如果实现这个方法就在forwardInvocation中进行重定向。
  • 最后还是没有实现,系统CoreFoundation会调用class_getInstanceMethod进行一次方法动态决议。
  • 消息的转发流程图如下

消息转发流程.jpg

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享