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

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

写在前面

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

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

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

1、消息查找流程

消息查找流程部分再也不展开讲解,未实现方法查找主要通过如下流程:缓存

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

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

2、动态方法解析

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

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

动态方法解析按调用方法走不一样分支:ui

  • cls是元类的话说明调用类方法,走_class_resolveInstanceMethod
  • 非元类的话调用了实例方法,走_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,因此只会在本类和父类中查找,并不会动态方法解析spa

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

// 具体搜索 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封装的时候须要给一个容错空间

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

3、消息转发机制

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

4、消息转发机制流程图

写在后面

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

相关文章
相关标签/搜索