iOS底层探索十(方法的本质下-消息转发流程)

前言

相关文章

iOS底层探索二(OC 中 alloc 方法 初探)

iOS底层探索三(内存对齐与calloc分析)  

iOS底层探索四(isa初探-联合体,位域,内存优化)     

iOS底层探索五(isa与类的关系)  

iOS底层探索六(类的分析上)

iOS底层探索七(类的分析下)

iOS底层探索八(方法本质上)

iOS底层探索九(方法的本质下objc_msgSend慢速及方法转发初探)

  相关代码:
      objc4_752源码    消息转发

 上篇文章讲述了方法的查找流程进行了详细的探索,并对方法的转发进行了初步探索,这篇文章,我们对消息的转发流程进行详细的探索。

消息转发

上篇文章我们介绍了,如果在OC中如果调用一个没有实现的方法,在方法查找过程中因为找不到会造成崩溃,但是苹果大大给我们提供了另一个解决思路,就是在动态解析(resolveMethod)的过程中有进行消息转发,并且我们在上篇文章中也实现了初步的防止崩溃方法:

static void resolveMethod(Class cls, SEL sel, id inst)
{
    runtimeLock.assertUnlocked();
    assert(cls->isRealized());

    if (! cls->isMetaClass()) {//不是元类
//判断类不是元类,那sel就是实例方法,那就先转发resolveInstanceMethod方法,判断有没有实现resolveInstanceMethod,没实现就不做处理
        // try [cls resolveInstanceMethod:sel]
        resolveInstanceMethod(cls, sel, inst);
    } 
    else {
        // try [nonMetaClass resolveClassMethod:sel]
        // and [cls resolveInstanceMethod:sel]
        //先转发resolveClassMethod,会先查找下resolveClassMethod,如果没实现就不做处理
        resolveClassMethod(cls, sel, inst);
        //再次查找下方法,如果没有的话,就再转发一下resolveInstanceMethod方法
        if (!lookUpImpOrNil(cls, sel, inst, 
                            NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
        {
            resolveInstanceMethod(cls, sel, inst);
        }
    }
}

实例方法动态消息决议

我们在main函数中调用teacher 中一个没有实现的方法;

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        #pragma clang diagnostic push
//        // 让编译器忽略错误,不然调用没有的方法,编译器编译完语法分析会报错,导致不能运行
        #pragma clang diagnostic ignored "-Wundeclared-selector"
 
                XZTeacher *teacher = [[XZTeacher alloc] init];
//        消息转发流程这个方法.h中之声明,不实现
        [teacher saySomthing];
        
        #pragma clang diagnostic pop
    }
    return 0;
}

因为调用的是实力方法,我们来查看resolveInstanceMethod 方法,这里传入3个参数 cls 类,sel 方法编号, inst 对象

static void resolveInstanceMethod(Class cls, SEL sel, id inst)
{
    runtimeLock.assertUnlocked();
    assert(cls->isRealized());
//查找系统方法中是否有实现resolveInstanceMethod这个方法,这里会根据继承链进行查找
    if (! lookUpImpOrNil(cls->ISA(), SEL_resolveInstanceMethod, cls, 
                         NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
    {
        // Resolver not implemented.
        return;
    }
    //发送SEL_resolveInstanceMethod消息,系统给你一次机会-->你是不是要针对这个没有实现的sel进行操作一下
    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    //发送消息SEL_resolveInstanceMethod 传参为我们的方法编号
    //崩溃的方法不是这个方法,说明再NSObject方法中有实现这个方法
    bool resolved = msg(cls, SEL_resolveInstanceMethod, sel);

    // Cache the result (good or bad) so the resolver doesn't fire next time.
    // +resolveInstanceMethod adds to self a.k.a. cls
//再次查找类中是否sel方法,因为resolveInstanceMethod方法执行后可能动态进行添加了,resolver是不要进行消息转发了
    IMP imp = lookUpImpOrNil(cls, sel, inst, 
                             NO/*initialize*/, YES/*cache*/, NO/*resolver*/);

    if (resolved  &&  PrintResolving) {
        if (imp) {
            _objc_inform("RESOLVE: method %c[%s %s] "
                         "dynamically resolved to %p", 
                         cls->isMetaClass() ? '+' : '-', 
                         cls->nameForLogging(), sel_getName(sel), imp);
        }
        else {
            // Method resolver didn't add anything?
            _objc_inform("RESOLVE: +[%s resolveInstanceMethod:%s] returned YES"
                         ", but no new implementation of %c[%s %s] was found",
                         cls->nameForLogging(), sel_getName(sel), 
                         cls->isMetaClass() ? '+' : '-', 
                         cls->nameForLogging(), sel_getName(sel));
        }
    }
}

我们可以看到首先lookUpImpOrNil查找类中方法中是否有实现resolveInstanceMethod这个方法,这里会根据继承链进行查找,因为是根据继承链查找,也就是说我们类中如果没有实现的话,就需要查找系统类,如果都找不到,这里就直接return,

我们可以在类中查找到这个方法(resolveInstanceMethod),有这个方法后,代码继续执行,然后使用

msg(cls, SEL_resolveInstanceMethod, sel),方法给resolveInstanceMethod发送消息从而继续lookUpImpOrNil查询方法IMP有没有实现,所以我们可以在我们自己的类中实现resolveInstanceMethod方法并对IMP进行绑定,所以在teacher中实现+resolveInstanceMethod 方法,并对未实现的方法进行IMP实现及绑定,使得,本身调用saySomthing 方法,经过IMP绑定后,调用了SayHello方法。从而防止了崩溃, 

#import <objc/message.h>
 
- (void)sayHello{
    NSLog(@"%s",__func__);
 
}
 
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
    if (sel == @selector(saySomthing)) {
        NSLog(@"进入方法了");
        IMP saySomeIMP = class_getMethodImplementation(self, @selector(sayHello));
        Method sayMethod = class_getInstanceMethod(self, @selector(sayHello));
        const char *sayType = method_getTypeEncoding(sayMethod);
        return class_addMethod(self, sel, saySomeIMP, sayType);
    }
    return [super resolveInstanceMethod:sel];
}

打印结果:

根据打印结果,我们可以看出,系统调用我们在XZTeacher 类中的resolveInstanceMethod方法,并根据我们的绑定,执行了sayHello方法。

实例方法我们看到了可以这么处理,那么类方法呢,我们也来看一下,我们在Main 函数中调用[XZTeacher sayLove],方法,sayLove 方法在XZTeacher类中只声明不实现,运行崩溃,这是肯定的就不进行演示了,这里根据源码,我们可以看到会进入resolveMethod方法,并进入resolveClassMethod方法;

static void resolveClassMethod(Class cls, SEL sel, id inst)
{
    runtimeLock.assertUnlocked();
    assert(cls->isRealized());
    assert(cls->isMetaClass());
    //查找下类是否实现了resolveClassMethod方法,NSObject类已经实现了
    if (! lookUpImpOrNil(cls, SEL_resolveClassMethod, inst, 
                         NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
    {
        // Resolver not implemented.
        return;
    }

    Class nonmeta;
    {
        mutex_locker_t lock(runtimeLock);
        nonmeta = getMaybeUnrealizedNonMetaClass(cls, inst);
        // +initialize path should have realized nonmeta already
        if (!nonmeta->isRealized()) {
            _objc_fatal("nonmeta class %s (%p) unexpectedly not realized",
                        nonmeta->nameForLogging(), nonmeta);
        }
    }
    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    //切记,此处是向元类发送resolveClassMethod消息,也就是调用resolveClassMethod方法
    bool resolved = msg(nonmeta, SEL_resolveClassMethod, sel);

    // Cache the result (good or bad) so the resolver doesn't fire next time.
    // +resolveClassMethod adds to self->ISA() a.k.a. cls
    IMP imp = lookUpImpOrNil(cls, sel, inst, 
                             NO/*initialize*/, YES/*cache*/, NO/*resolver*/);

    if (resolved  &&  PrintResolving) {
        if (imp) {
            _objc_inform("RESOLVE: method %c[%s %s] "
                         "dynamically resolved to %p", 
                         cls->isMetaClass() ? '+' : '-', 
                         cls->nameForLogging(), sel_getName(sel), imp);
        }
        else {
            // Method resolver didn't add anything?
            _objc_inform("RESOLVE: +[%s resolveClassMethod:%s] returned YES"
                         ", but no new implementation of %c[%s %s] was found",
                         cls->nameForLogging(), sel_getName(sel), 
                         cls->isMetaClass() ? '+' : '-', 
                         cls->nameForLogging(), sel_getName(sel));
        }
    }
}

这里我们需要注意的是传入的参数cls 为元类,sel方法编号,inst为类;我们可以看到这里和resolveInstanceMethod方法中的逻辑基本一样,不过这里检查的系统方法为resolveClassMethod方法,这个方法我们可以看到上面的截图里面在NSObject类中也是有实现这个方法的;

类方法动态消息决议

我们在main函数中调用一个类方法 [XZTeacher sayLove] 在XZTeacher类中实现resolveClassMethod方法并运行程序:

可以看到确实来了resolveClassMethod方法,所以我们可以在这个方法进行类似处理来进行防止崩溃;在方法内加入代码:

+(BOOL)resolveClassMethod:(SEL)sel
{
    NSLog(@"来了:resolveClassMethod");
    if (sel == @selector(sayLove)) {
        NSLog(@"进入方法了sayLove");
        IMP sayObjIMP = class_getMethodImplementation(objc_getMetaClass("XZTeacher"), @selector(sayObjc));
        Method sayObjMethod = class_getClassMethod(objc_getMetaClass("XZTeacher"), @selector(sayObjc));
        const char *sayObjType = method_getTypeEncoding(sayObjMethod);
        // 类方法在元类 objc_getMetaClass("XZTeacher")
        BOOL isAdd = class_addMethod(objc_getMetaClass("XZTeacher"), sel, sayObjIMP, sayObjType);
        return isAdd;
    }
    return [super resolveClassMethod:sel];
}

运行程序,查看运行结果,可以看出方法正常运行

resolveMethod方法中类方法在调用resolveClassMethod完成后还进行了一次调用

        if (!lookUpImpOrNil(cls, sel, inst, 
                            NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
        {
            resolveInstanceMethod(cls, sel, inst);
        }


IMP lookUpImpOrNil(Class cls, SEL sel, id inst, 
                   bool initialize, bool cache, bool resolver)
{
    IMP imp = lookUpImpOrForward(cls, sel, inst, initialize, cache, resolver);
    if (imp == _objc_msgForward_impcache) return nil;
    else return imp;
}

在这里我们可以看出,因为这里是的cls是元类,所以在元类中lookUpImpOrNil查找sayLove方法肯定也是找不到的,这里就会调用resolveInstanceMethod,那我们是不是可以考虑将所有的处理都放到这个方法中呢resolveInstanceMethod,尝试将resolveInstanceMethod重写,并调用sayLove方法

可以看出还是只来了resolveClassMethod方法,这是我们考虑,因为是在元类职工查找resolveInstanceMethod方法,所以找不到,这个时候会想到,查找方法就是根据继承链来进行查找方法的,也就是如下图:

根据继承链关系,可以看到根元类也是继承与NSObject类,那就可以在NSObject类中只处理resolveInstanceMethod方法,先写上这个方法添加处理方法:

+ (BOOL)resolveInstanceMethod:(SEL)sel
{
    NSLog(@"来了,%s",__func__);
    if (sel == @selector(saySomthing)) {
        NSLog(@"进入方法了saySomthing");
        IMP saySomeIMP = class_getMethodImplementation(self, @selector(sayMaster));
        Method sayMethod = class_getInstanceMethod(self, @selector(sayMaster));
        const char *sayType = method_getTypeEncoding(sayMethod);
        return class_addMethod(self, sel, saySomeIMP, sayType);
    }

        if (sel == @selector(sayLove)) {
            NSLog(@"进入方法了sayLove");
            IMP sayObjIMP = class_getMethodImplementation(objc_getMetaClass("NSObject"), @selector(sayEasy));
            Method sayObjMethod = class_getClassMethod(objc_getMetaClass("NSObject"), @selector(sayEasy));
            const char *sayObjType = method_getTypeEncoding(sayObjMethod);
            // 类方法在元类 objc_getMetaClass("XZTeacher")
            return  class_addMethod(objc_getMetaClass("NSObject"), sel, sayObjIMP, sayObjType);;
        }

//    [super resolveInstanceMethod:sel],因为这里是根类不能调用super了,直接返回NO就行
    return NO;
}

运行查看下运行结果

调用类方法sayLove方法可以调用到SayEasy方法,调用实例方法saySomething根据IMP转换调用了SayMaster方法;说民这里处理确实可以一劳永逸

崩溃优化策略

  1. 可以在自己的项目工程中每个模块进行命名规范,例如首页的所有方法都以home_开头,我的都有me_开头

  2. 实现一个NSObject分类,进行bug替换,当检测到是home_开始,没有方法时,直接跳转到首页,并上报服务器,这样的简单操作就可以实现一个优化策略了。

这个策略可以发小是有漏洞的,如果在上层类中实现了resolveInstanceMethod方法那么实例方法在防止的时候就不会走到NSObject类中了,可能就会导致上报不全,等问题。所以我们再进行消息处理的时候,一般会放到消息转发的最后一步,那么我们继续来看消息转发中的流程,在源码中我们可以看到resolveMethod调用之后就是cache_fill 就会导致崩溃了,那么中件流程我们根本没法探索,这时我们需要回头看一下方法缓存;

消息转发流程探索

根据查找到imp后会进行缓存调用方法为log_and_fill_cache,看到log说明应该是有日志产生的,进入方法进行查看

log_and_fill_cache(Class cls, IMP imp, SEL sel, id receiver, Class implementer)
{
#if SUPPORT_MESSAGE_LOGGING
    //如果有这个objcMsgLogEnabled变量就会写入日志
    if (objcMsgLogEnabled) {
        bool cacheIt = logMessageSend(implementer->isMetaClass(), 
                                      cls->nameForLogging(),
                                      implementer->nameForLogging(), 
                                      sel);
        if (!cacheIt) return;
    }
#endif
//   写入缓存
    cache_fill (cls, sel, imp, receiver);
}

需要一个参数为objcMsgLogEnabled为true时调用logMessageSend 方法进行日志书写,继续进入

bool objcMsgLogEnabled = false;
static int objcMsgLogFD = -1;

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;
}

可以看到日志是写入了本地的"/tmp/msgSends-编号"的目录下总结下来就是需要2个参数objcMsgLogEnabled为true 可以看到函数上面默认为false ,还有objcMsgLogFD<0 函数上这个值默认为-1,所以就只需要看objcMsgLogEnabled为true即可;搜索一下这个函数的赋值方法

OBJC_EXPORT void
instrumentObjcMessageSends(BOOL flag)
    OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);
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;
}

找到这个方法我们需要在外部声明一下这个函数来看一下在main文件中这枚写下

#import "XZPerson.h"
#import "XZTeacher.h"
#import "NSObject+XZ.h"
#import <objc/runtime.h>

extern void instrumentObjcMessageSends(BOOL flag);

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        XZTeacher *teacher = [XZTeacher alloc];
//        方法调用日志打开
        instrumentObjcMessageSends(true);
        [teacher saySomthing];
//        [XZTeacher sayLove];
        instrumentObjcMessageSends(false);
    }
    return 0;
}

将之前处理崩溃的都先注释掉,然后调用日志打开,运行并查看/tmp/路径下以msgSends开头的文件进行查看

+ XZTeacher NSObject resolveInstanceMethod:
+ XZTeacher NSObject resolveInstanceMethod:
- XZTeacher NSObject forwardingTargetForSelector:
- XZTeacher NSObject forwardingTargetForSelector:
- XZTeacher NSObject methodSignatureForSelector:
- XZTeacher NSObject methodSignatureForSelector:
- XZTeacher NSObject class
+ XZTeacher NSObject resolveInstanceMethod:
+ XZTeacher NSObject resolveInstanceMethod:
- XZTeacher NSObject doesNotRecognizeSelector:
- XZTeacher NSObject doesNotRecognizeSelector:
- XZTeacher NSObject class
- OS_xpc_serializer OS_xpc_object dealloc
- OS_object NSObject dealloc
+ OS_xpc_payload NSObject class
。。。根据名称可以判断下面都是系统方法

可以看到都是调用了2次分别为resolveInstanceMethodforwardingTargetForSelectormethodSignatureForSelector方法,这个方法resolveInstanceMethod已经研究过了,下面我们对后面2个方法研究一下

forwardingTargetForSelector 方法研究

点击xcode 工具中的help 搜索

自己没有处理的方法,会使用快速转发到forwardingTargetForSelector方法中,使用这个方法,返回一个对象,让别人进行处理这个方法,那么我们新定义一个类XZStudent类,在这个类实现saySomethingsayLove 方法并实现

#import "XZStudent.h"

@implementation XZStudent
- (void)saySomthing
{
    NSLog(@"%s",__func__);
}
+ (void)sayLove{
    NSLog(@"%s",__func__);
}

@end

在XZTeacher类中实现forwardingTargetForSelector方法

//实例方法调用这个转发
- (id)forwardingTargetForSelector:(SEL)aSelector
{
    if (aSelector == @selector(saySomthing)) {
        return [XZStudent alloc];
    }
    return [super forwardingTargetForSelector:aSelector];
}
//类方法调用这个转发
+ (id)forwardingTargetForSelector:(SEL)aSelector
{
    if (aSelector == @selector(sayLove)) {
        return [XZStudent class];
    }
    return [super forwardingTargetForSelector:aSelector];
}

并在main中调用saySomething方法查看打印效果

正常打印,可以看出这样也是能完美解决崩溃

methodSignatureForSelector方法研究

根据xcode中的help 搜索,methodSignatureForSelector搜索

  这个方法methodSignatureForSelector 方法关联使用的就是forwardInvocation方法,将之前的处理先去掉,在XZTeacher类中加入这个最新处理:

//方法签名的下层慢速处理,返回一个方法签名 但是只有这个方法肯定是不够的
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
    NSLog(@"%s -- %@",__func__,NSStringFromSelector(aSelector));
    if (aSelector == @selector(saySomthing)) {
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }
    return [super methodSignatureForSelector:aSelector];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation{
    NSLog(@"%s",__func__);
}

这里的方法签名是有固定格式的,标示不同的类型具体可查看官方文档

查看一下运行结果:

可以正常执行到方法,而且不崩溃了 ,但是也没有对这个方法进行处理,我们继续查看文档中的这个方法

看到可以这么进行处理,

- (void)forwardInvocation:(NSInvocation *)invocation
{
    SEL aSelector = [invocation selector];
 
    if ([friend respondsToSelector:aSelector])
        [invocation invokeWithTarget:friend];
    else
        [super forwardInvocation:invocation];
}

按照官方文档我们进行处理

    if ([[XZStudent alloc] respondsToSelector:aSelector])
        [anInvocation invokeWithTarget:[XZStudent alloc]];
    else
        [super forwardInvocation:anInvocation];

运行查看结果:

也可以正常运行,同样的这个如果是类方法处理方式和实例方法处理方式不同,如下代码:

//方法签名的下层慢速处理,返回一个方法签名 但是只有这个方法肯定是不够的
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
    NSLog(@"%s -- %@",__func__,NSStringFromSelector(aSelector));
    if (aSelector == @selector(saySomthing)) {
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }
    return [super methodSignatureForSelector:aSelector];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation{
    NSLog(@"%s",__func__);
    //    在这里进行处理方法,如果处理的话就可以处理,不处理就会失效
    SEL aSelector = [anInvocation selector];
//    查看XZStudent 对象能否进行处理,如果能,就交给他处理不能就不处理了
    if ([[XZStudent alloc] respondsToSelector:aSelector])
        [anInvocation invokeWithTarget:[XZStudent alloc]];
    else
        [super forwardInvocation:anInvocation];
}
//类方法处理
+ (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
    NSLog(@"%s -- %@",__func__,NSStringFromSelector(aSelector));
    if (aSelector == @selector(sayLove)) {
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }
    return [super methodSignatureForSelector:aSelector];
}
+ (void)forwardInvocation:(NSInvocation *)anInvocation{
    NSLog(@"%s",__func__);
    //    在这里进行处理方法,如果处理的话就可以处理,不处理就会失效
    SEL aSelector = [anInvocation selector];
    //    查看XZStudent 类能否进行处理,如果能,就交给他处理不能就不处理了
    if ([[XZStudent class] respondsToSelector:aSelector])
        [anInvocation invokeWithTarget:[XZStudent class]];
    else
        [super forwardInvocation:anInvocation];
}

如果这几个方法都没有实现的话,系统就会调用 doesNotRecognizeSelector崩溃信息出来

+ (void)doesNotRecognizeSelector:(SEL)sel {

    _objc_fatal("+[%s %s]: unrecognized selector sent to instance %p", 

                class_getName(self), sel_getName(sel), self);

}

总结

这篇文章我们就对方法的本质进行了完整的探索:

  1. 方法的底层就是调用objc_msgSend 方法

  2. 进行汇编在缓存中快速查找

  3. 调用_class_lookupMethodAndCache3进行慢速查找

  4. 找不到方法后进行动态解析

          4.1 :resolveInstanceMethod:为发送消息的对象的添加一个IMP,然后再让该对象去处理

          4.2:forwardingTargetForSelector:将该消息转发给能处理该消息的对象

          4.3:methodSignatureForSelectorforwardInvocation:第一个方法生成方法签名,然后创建NSInvocation对象作为参数给第二个方法,

         4.3.2 然后在第二个方法(forwardInvocation)里面做消息处理,只要在第二个方法里面不执行父类的方法,即使不处理也不会崩溃

    5.如果不进行动态解析就会导致崩溃

最后附带一份苹果的官方消息转发图:

写给自己:

喋喋不休不如观心自省,埋怨他人不如即听即忘。能干扰你的,往往是自己的太在意,能伤害你的,往往是自己的想不开,未完待续。。。。

 

原创文章 88 获赞 21 访问量 2万+

猜你喜欢

转载自blog.csdn.net/ZhaiAlan/article/details/105052681