iOS探索 动态方法解析和消息转发机制

4,900 阅读10分钟

欢迎阅读iOS探索系列(按序阅读食用效果更加)

写在前面

上一篇文章讲了方法在底层是如何通过sel找到imp的,本文就将通过源码来研究“没有实现的方法在底层要通过多少关卡才能发出unrecognized selector sent to instanceCrash”,看完本文后你会明白程序崩溃也是一个很复杂的过程

动态方法决议源码中,FXSon中有两个只声明未实现的方法,分别调用它们:

  • - (void)doInstanceNoImplementation;
  • + (void)doClassNoImplementation;

一、消息查找流程

消息查找流程部分不再展开讲解,未实现方法查找主要经过以下流程:

  • 汇编中通过isa平移得到class,内存偏移得到cache->buckets查找缓存
  • c++中
    • 先查找本类缓存,再找本类方法列表
    • 遍历父类:查找父类缓存,再找父类方法列表

由于慢速流程调用的是lookUpImpOrForward(cls, sel, obj, YES/*initialize*/, NO/*cache*/, YES/*resolver*/),遍历父类无果后来到动态方法解析

二、动态方法解析

只有resolvertriedResolver满足条件下才会进入动态方法解析

if (resolver  &&  !triedResolver) {
    runtimeLock.unlock();
    _class_resolveMethod(cls, sel, inst);
    runtimeLock.lock();
    // Don't cache the result; we don't hold the lock so it may have 
    // changed already. Re-do the search from scratch instead.
    triedResolver = YES;
    goto retry;
}

动态方法解析按调用方法走不同分支:

  • cls是元类的话说明调用类方法,走_class_resolveClassMethod
  • 非元类的话调用了实例方法,走_class_resolveInstanceMethod
void _class_resolveMethod(Class cls, SEL sel, id inst)
{
    if (! cls->isMetaClass()) {
        // try [cls resolveInstanceMethod:sel]

        _class_resolveInstanceMethod(cls, sel, inst);
    } 
    else {
        // try [nonMetaClass resolveClassMethod:sel]
        // and [cls resolveInstanceMethod:sel]
        _class_resolveClassMethod(cls, sel, inst);
        if (!lookUpImpOrNil(cls, sel, inst, 
                            NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
        {
            _class_resolveInstanceMethod(cls, sel, inst);
        }
    }
}

1.实例方法

static void _class_resolveInstanceMethod(Class cls, SEL sel, id inst)
{
    if (! lookUpImpOrNil(cls->ISA(), SEL_resolveInstanceMethod, cls, 
                         NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
    {
        // Resolver not implemented.
        return;
    }

    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    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
    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));
        }
    }
}

①检查cls中是否有SEL_resolveInstanceMethod(resolveInstanceMethod)方法

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

注意这里的lookUpImpOrForward中的resolver为NO,所以只会在本类和父类中查找,并不会动态方法解析

但cls没有这个方法,其实根类NSObject已经实现了这个方法(NSProxy没有实现)

// 具体搜索 NSObject.mm
+ (BOOL)resolveClassMethod:(SEL)sel {
    return NO;
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    return NO;
}

②向本类发送SEL_resolveInstanceMethod消息,即调用这个方法

lookUpImpOrNil再次查找当前实例方法imp,找到就填充缓存,找不到就返回

④结束动态方法解析,回到lookUpImpOrForward方法将triedResolver置否并goto retry重新查找缓存和方法列表

2.实例方法流程图

3.类方法

相较于实例方法,类方法就复杂多了

static void _class_resolveClassMethod(Class cls, SEL sel, id inst)
{
    assert(cls->isMetaClass());

    if (! lookUpImpOrNil(cls, SEL_resolveClassMethod, inst, 
                         NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
    {
        // Resolver not implemented.
        return;
    }

    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    bool resolved = msg(_class_getNonMetaClass(cls, inst), 
                        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));
        }
    }
}

_class_resolveClassMethod进入

    else {
        // try [nonMetaClass resolveClassMethod:sel]
        // and [cls resolveInstanceMethod:sel]
        _class_resolveClassMethod(cls, sel, inst);
        if (!lookUpImpOrNil(cls, sel, inst, 
                            NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
        {
            _class_resolveInstanceMethod(cls, sel, inst);
        }
    }

lookUpImpOrNil查找SEL_resolveClassMethod(resolveClassMethod)是否实现

③向非元类发送SEL_resolveClassMethod消息(由于cls是元类,_class_getNonMetaClass(cls, inst)得到inst

lookUpImpOrNil再次查找当前实例方法imp,找到就填充缓存,找不到就返回

⑤结束_class_resolveClassMethodlookUpImpOrNil查找selimp,若有imp则退出动态方法决议,若无则进入_class_resolveInstanceMethod

⑥检查cls中是否有SEL_resolveInstanceMethod(resolveInstanceMethod)方法

⑦向本类发送SEL_resolveInstanceMethod消息

lookUpImpOrNil再次查找当前实例方法imp,找到就填充缓存,找不到就返回

⑨结束动态方法解析,回到lookUpImpOrForward方法将triedResolver置否并goto retry重新查找缓存和方法列表

4.类方法流程图

5.动态方法决议

Objective-C提供了一种名为动态方法决议的手段,使得我们可以在运行时动态地为一个selector 提供实现,并在其中为指定的selector 提供实现即可——子类重写+resolveInstanceMethod:+resolveClassMethod:

  • 对于实例方法

实例方法流程图中可以看出,解决崩溃的方法就是resolveInstanceMethod阶段添加一个备用实现

#import "FXSon.h"
#import <objc/message.h>

@implementation FXSon

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    
    if (sel == @selector(doInstanceNoImplementation)) {
        NSLog(@"——————————找不到%@-%@方法,崩溃了——————————", self, NSStringFromSelector(sel));
        IMP insteadIMP = class_getMethodImplementation(self, @selector(doInstead));
        Method insteadMethod = class_getInstanceMethod(self, @selector(doInstead));
        const char *instead = method_getTypeEncoding(insteadMethod);
        return class_addMethod(self, sel, insteadIMP, instead);
    }
    
    return NO;
}

- (void)doInstead {
    NSLog(@"——————————解决崩溃——————————");
}

@end
  • 对于类方法——resolveClassMethod阶段

效仿解决实例方法崩溃,类方法也可以往元类中塞一个imp实例方法存在类对象中,类方法存在元类对象中)

#import "FXSon.h"
#import <objc/message.h>

@implementation FXSon

+ (BOOL)resolveClassMethod:(SEL)sel {
    
    if (sel == @selector(doClassNoImplementation)) {
        NSLog(@"——————————找不到%@+%@方法,崩溃了——————————", self, NSStringFromSelector(sel));
        IMP classIMP = class_getMethodImplementation(objc_getMetaClass("FXSon"), @selector(doClassNoInstead));
        Method classMethod = class_getInstanceMethod(objc_getMetaClass("FXSon"), @selector(doClassNoInstead));
        const char *cls = method_getTypeEncoding(classMethod);
        return class_addMethod(objc_getMetaClass("FXSon"), sel, classIMP, cls);
    }
    
    return NO;
}

+ (void)doClassNoInstead {
    NSLog(@"——————————解决崩溃——————————");
}

@end
  • 对于类方法——resolveInstanceMethod阶段

因为元类的方法以实例方法存储在根元类中,由于元类根源类由系统创建无法修改,所以只能在根元类的父类NSObject中,重写对应的实例方法resolveInstanceMethod进行动态解析(isa走位图完美说明一切)

#import "NSObject+FX.h"
#import <objc/message.h>

@implementation NSObject (FX)

+ (BOOL)resolveInstanceMethod:(SEL)sel {

    if ([NSStringFromSelector(sel) isEqualToString:@"doClassNoImplementation"]) {
        NSLog(@"——————————找不到%@-%@方法,崩溃了——————————", self, NSStringFromSelector(sel));
        IMP instanceIMP = class_getMethodImplementation(objc_getMetaClass("NSObject"), @selector(doInstanceNoInstead));
        Method instanceMethod = class_getInstanceMethod(objc_getMetaClass("NSObject"), @selector(doInstanceNoInstead));
        const char *instance = method_getTypeEncoding(instanceMethod);
        return class_addMethod(objc_getMetaClass("NSObject"), sel, instanceIMP, instance);
    }

    return NO;
}

- (void)doInstanceNoInstead {
    NSLog(@"——————————解决崩溃——————————");
}

@end

6.动态方法决议总结

  • 实例方法可以重写resolveInstanceMethod添加imp
  • 类方法可以在本类重写resolveClassMethod往元类添加imp,或者在NSObject分类重写resolveInstanceMethod添加imp
  • 动态方法解析只要在任意一步lookUpImpOrNil查找到imp就不会查找下去——即本类做了动态方法决议,不会走到NSObjct分类的动态方法决议
  • 所有方法都可以通过在NSObject分类重写resolveInstanceMethod添加imp解决崩溃

那么把所有崩溃都在NSObjct分类中处理,加以前缀区分业务逻辑,岂不是美滋滋?错!

  • 统一处理起来耦合度高
  • 逻辑判断多
  • 可能在NSObjct分类动态方法决议之前已经做了处理
  • SDK封装的时候需要给一个容错空间

这也不行,那也不行,那该怎么办?放心,苹果爸爸已经给我们准备好后路了!

三、消息转发机制

lookUpImpOrForward方法在查找类、父类缓存和方法列表以及动态方法解析后,如果还没有找到imp那么将进入消息处理的最后一步——消息转发流程

 imp = (IMP)_objc_msgForward_impcache;
 cache_fill(cls, sel, imp, inst);

在汇编中发现了_objc_msgForward_impcache,如下是arm64的汇编代码

最后会来到c++中_objc_forward_handler

void *_objc_forward_handler = (void*)objc_defaultForwardHandler;

objc_defaultForwardHandler(id self, SEL sel)
{
    _objc_fatal("%c[%s %s]: unrecognized selector sent to instance %p "
                "(no message forward handler is installed)", 
                class_isMetaClass(object_getClass(self)) ? '+' : '-', 
                object_getClassName(self), sel_getName(sel), self);
}

再来看看崩溃信息,崩溃之前底层还调用了___forwarding____CF_forwarding_prep_0等方法,但是CoreFoundation库不开源

在无从下手之际,只能根据前辈们的经验开始着手——然后在logMessageSend找到了探索方向(lookUpImpOrForward->log_and_fill_cache->logMessageSend)

通过方法我们可以看到,日志会记录在/tmp/msgSends目录下,并且通过objcMsgLogEnabled变量来控制是否存储日志

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

instrumentObjcMessageSends可以改变objcMsgLogEnabled的值

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

所以我们可以根据以下代码来记录并查看日志(仿佛不能在源码工程中操作)

extern void instrumentObjcMessageSends(BOOL flag);

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        FXSon *son = [[FXSon alloc] init];
        
        instrumentObjcMessageSends(true);
        [son doInstanceNoImplementation];
        instrumentObjcMessageSends(false);
    }
}

访达shift+command+G访问/tmp/msgSends

动态方法解析doesNotRecognizeSelector崩溃之间,就是消息转发流程——分为快速流程forwardingTargetForSelector慢速流程methodSignatureForSelector

1.快速流程

forwardingTargetForSelector在源码中只有一个声明,并没有其它描述,好在帮助文档中提到了关于它的解释:

  • 该方法的返回对象是执行sel的新对象,也就是自己处理不了会将消息转发给别人的对象进行相关方法的处理,但是不能返回self,否则会一直找不到
  • 该方法的效率较高,如果不实现,会走到forwardInvocation:方法进行处理
  • 底层会调用objc_msgSend(forwardingTarget, sel, ...);来实现消息的发送
  • 被转发消息的接受者参数、返回值等应和原方法相同

2.快速流程解决崩溃

如下代码就是是通过快速转发解决崩溃——即FXSon实现不了的方法,转发给FXTeacher去实现(转发给已经实现该方法的对象)

#import "FXTeacher.h"

@implementation FXSon

// FXTeacher已实现了doInstanceNoImplementation
- (id)forwardingTargetForSelector:(SEL)aSelector{
    NSLog(@"%s -- %@",__func__,NSStringFromSelector(aSelector));
    if (aSelector == @selector(doInstanceNoImplementation)) {
        return [FXTeacher alloc];
    }
    return [super forwardingTargetForSelector:aSelector];
}

@end

3.慢速流程

在快速流程找不到转发的对象后,会来到慢速流程methodSignatureForSelector

依葫芦画瓢,在帮助文档中找到methodSignatureForSelector

点击查看forwardInvocation

  • forwardInvocationmethodSignatureForSelector必须是同时存在的,底层会通过方法签名,生成一个NSInvocation,将其作为参数传递调用
  • 查找可以响应NSInvocation中编码的消息的对象(对于所有消息,此对象不必相同)
  • 使用anInvocation将消息发送到该对象。anInvocation将保存结果,运行时系统将提取结果并将其传递给原始发送者

4.慢速流程解决崩溃

慢速流程流程就是先methodSignatureForSelector提供一个方法签名,然后forwardInvocation通过对NSInvocation来实现消息的转发

#import "FXTeacher.h"

@implementation FXSon

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
    NSLog(@"%s -- %@",__func__,NSStringFromSelector(aSelector));
    if (aSelector == @selector(doInstanceNoImplementation)) {
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }
    return [super methodSignatureForSelector:aSelector];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation{
    NSLog(@"%s ",__func__);
   SEL aSelector = [anInvocation selector];

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

@end

四、消息转发机制流程图

写在后面

有兴趣的小伙伴们可以看看Demo,加深对OC消息机制的理解和防崩溃的运用