美文网首页
消息动态决议

消息动态决议

作者: 浅墨入画 | 来源:发表于2021-08-01 21:54 被阅读0次

前言

前面我们分析了消息的查找流程快速查找流程慢速查找流程。如果这两种方式都没找到方法的实现,苹果给了两个建议

  • 动态方法决议:慢速查找流程未找到后,会执行一次动态方法决议
  • 消息转发:如果动态方法决议仍然没有找到实现,则进行消息转发

如果这两个建议都没有找到方法的实现,就会崩溃报错unrecognized selector sent to instance 0x600000c880d0

定义LGPerson类,其中sayHello实例方法没有实现,main.m文件调用,崩溃如下

<!-- LGPerson.h文件 -->
#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface LGPerson : NSObject
- (void)sayHello;
- (void)sayMaster;
@end

NS_ASSUME_NONNULL_END

<!-- LGPerson.m文件 -->
#import "LGPerson.h"

@implementation LGPerson
- (void)sayMaster {
    NSLog(@"%@ : %s",self,__func__);
}
@end
image.png

方法找不到的报错底层

根据慢速查找源码,我们发现其报错后都是走到__objc_msgForward_impcache方法,以下是报错流程的源码

STATIC_ENTRY __objc_msgForward_impcache

// No stret specialization.
b   __objc_msgForward

END_ENTRY __objc_msgForward_impcache

ENTRY __objc_msgForward

adrp    x17, __objc_forward_handler@PAGE
ldr p17, [x17, __objc_forward_handler@PAGEOFF]
TailCallFunctionPointer x17
    
END_ENTRY __objc_msgForward

汇编实现中查找__objc_forward_handler没有找到,在源码中去掉一个下划线进行全局搜索_objc_forward_handler有如下实现,本质是调用objc_defaultForwardHandler方法

// Default forward handler halts the process.
__attribute__((noreturn, cold)) void
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);
}
void *_objc_forward_handler = (void*)objc_defaultForwardHandler;

这就是我们日常开发中最常见的错误:没有实现函数,运行程序,崩溃报错提示。探索流程总结

  • 由控制台打印信息unrecognized selector sent to instance为出发点,向下深挖方法的响应流程
  • 通过对LookUpImpOrForward的分析得出在慢速查找未找到的时候,给imp默认赋值forward_impobjc_msgForward_impcache
  • 通过对objc_msgForward_impcache的查找,发现在汇编中它只是一个中间函数,真正指向的是objc_msgForward
  • objc_msgForward的分析得出,方法内调用了objc_forward_handle获取返回值存到x17寄存器,再由TailCallFunctionPointer方法跳转到x17寄存器内真正指向的imp地址
  • _objc_forward_handler会给定默认的实现objc_defaultForwardHandle,当imp最终的查找流程全部走完的时候还未找到imp,那么此时就会进入objc_defaultForwardHandle函数,将错误信息打印出来。

为了防止方法未实现的崩溃,下面我们从对象方法动态决议开始探索

对象方法动态决议

慢速查找流程未找到方法实现时,循环查找结束,接下来进入动态方法决议

IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
{
    const IMP forward_imp = (IMP)_objc_msgForward_impcache;
    IMP imp = nil;

    for (unsigned attempts = unreasonableClassCount();;) {

        if (slowpath((curClass = curClass->getSuperclass()) == nil)) {
            //当消息的快速查找、慢速查找流程都走完还找不到的情况下
            //imp会给一个默认值forward_imp,进入消息转发流程
            imp = forward_imp;
            break;
        }
        //找到了sel对应的imp
        if (fastpath(imp)) {
            goto done;
        }
    }
    /**
    * 如果遍历查找的过程找到了,会跳过此步骤,取到done分支,进行后续操作
    * 如果找不到,会进行下面这个算法,最终进入resolveMethod_locked函数
    * 此算法真正达到的目的为单例,保证一个lookUpImpOrForward
    * 只执行一次resolveMethod_locked
    */ 
    if (slowpath(behavior & LOOKUP_RESOLVER)) {
        behavior ^= LOOKUP_RESOLVER;
        return resolveMethod_locked(inst, sel, cls, behavior);
    }
    return imp;
}
位运算
  • &:按位与,是双目运算符。其功能是参与运算的两数各对应的二进位相与。相同位置数字都为1时,结果位才为1。
  • ^:按位异或,比较的是二进制位,相同位置数字不同为1,相同为0
  • ^=:按位异或,比较的是二进制位,相同位置数字不同为1,相同为0,将结果赋值为运算符左边。
单例解读
.macro MethodTableLookup
    
    SAVE_REGS MSGSEND

    // lookUpImpOrForward(obj, sel, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER)
    // receiver and selector already in x0 and x1
    mov x2, x16
    mov x3, #3
    bl  _lookUpImpOrForward

    // IMP in x0
    mov x17, x0

    RESTORE_REGS MSGSEND

.endmacro
  • 前提条件(初始值) behavior = 3 LOOKUP_RESOLVER = 2
  • 初次判断操作behavior & LOOKUP_RESOLVER = 3 & 2 = 0x11 & 0x10 = 0x10 = 2
  • 重置behaivor behavior = behavior ^ LOOKUP_RESOLVER = 3 ^ 2 = 0x11 ^ 0x10 = 0x01 = 1
  • 再次进入判断操作(第二次至无限次)behavior & LOOKUP_RESOLVER = 1 & 2 = 0x01 & 0x10 = 0x00 = 0
得出结论

保证了每一个lookUpImpOrForward函数最多只能执行一次resolveMethod_locked(动态方法决议),直到behavior被重新赋值

动态方法决议源码

调用一个方法流程,先进入消息快速查询流程 -> 然后消息慢速查找流程,当底层源码已经查找了两遍依然找不到方法的实现,此时imp=nil,理论上来讲程序应该崩溃。但在开发者的角度,崩溃表示这个框架不稳定,系统很不友善。所以此框架决定再给你一次机会,为你提供一个返回自定义imp的机会,resolveMethod_locked函数就是动态消息转发的入口。

static NEVER_INLINE IMP
resolveMethod_locked(id inst, SEL sel, Class cls, int behavior)
{
    runtimeLock.assertLocked();
    ASSERT(cls->isRealized());

    runtimeLock.unlock();
    //对象 -- 类
    if (! cls->isMetaClass()) { //类不是元类,调用对象的解析方法
        // try [cls resolveInstanceMethod:sel]
        resolveInstanceMethod(inst, sel, cls);
    } 
    else {//如果是元类,调用类的解析方法, 类 -- 元类
        // try [nonMetaClass resolveClassMethod:sel]
        // and [cls resolveInstanceMethod:sel]
        resolveClassMethod(inst, sel, cls);
        //为什么要有这行代码? -- 类方法在元类中是对象方法,所以还是需要查询元类中对象方法的动态方法决议
        if (!lookUpImpOrNil(inst, sel, cls)) { //如果没有找到或者为空,在元类的对象方法解析方法中查找
            resolveInstanceMethod(inst, sel, cls);
        }
    }

    // chances are that calling the resolver have populated the cache
    // so attempt using it
    //如果方法解析中将其实现指向其他方法,则继续走方法查找流程
    return lookUpImpOrForward(inst, sel, cls, behavior | LOOKUP_CACHE);
}

该函数内的三个关键函数

  • resolveInstanceMethod:实例方法动态添加imp
  • resolveClassMethod:类方法动态添加imp
  • lookUpImpOrForwardTryCache,当完成添加之后,回到之前的慢速查找流程再来一遍
类动态方法决议会进入resolveClassMethod,然后根据判断有可能会再次进入resolveInstanceMethod为什么?

正常的类对象动态方法决议会进入resolveClassMethod,这点是毋庸置疑的,但是类方法查找过程是在元类中查找,那么通过isa的指向图中可以得知,类方法的查找过程也是有继承关系的,会一直向上找,找到superMetaClass,找到rootMetaClass,最终找到NSObject,到这一层所有的方法对于NSObject来讲都是实例方法所以会调用resolveInstanceMethod

实例方法源码

针对实例方法调用,在快速-慢速查找均没有找到实例方法的实现时,我们有一次挽救的机会即尝试一次动态方法决议,由于是实例方法,所以会走到resolveInstanceMethod方法

static void resolveInstanceMethod(id inst, SEL sel, Class cls)
{
    runtimeLock.assertUnlocked();
    ASSERT(cls->isRealized());
    SEL resolve_sel = @selector(resolveInstanceMethod:);
    
    // look的是 resolveInstanceMethod --相当于是发送消息前的容错处理
    if (!lookUpImpOrNil(cls, resolve_sel, cls->ISA())) {
        // Resolver not implemented.
        return;
    }

    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    bool resolved = msg(cls, resolve_sel, sel); //发送resolve_sel消息

    // Cache the result (good or bad) so the resolver doesn't fire next time.
    // +resolveInstanceMethod adds to self a.k.a. cls
    //查找say666
    IMP imp = lookUpImpOrNil(inst, sel, cls);

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

主要步骤如下

  • 在发送resolveInstanceMethod消息前,需要查找cls类中是否有该方法的实现,即通过lookUpImpOrNil方法又会进入lookUpImpOrForward慢速查找流程查找resolveInstanceMethod方法
    如果没有直接返回
    如果有则发送resolveInstanceMethod消息
  • 再次慢速查找实例方法的实现,即通过lookUpImpOrNil方法又会进入lookUpImpOrForward慢速查找流程查找实例方法
崩溃修改

针对实例方法sayHello未实现的报错,可以通过在类中重写resolveInstanceMethod类方法,并将其指向其他方法的实现。即在LGPerson中重写resolveInstanceMethod类方法,将实例方法sayHello的实现指向sayMaster方法实现,如下所示

// LGPerson.m 文件添加如下resolveInstanceMethod方法
+ (BOOL)resolveInstanceMethod:(SEL)sel{
    if (sel == @selector(sayHello)) {
        NSLog(@"%@ 来了", NSStringFromSelector(sel));
        //获取sayMaster方法的imp
        IMP imp = class_getMethodImplementation(self, @selector(sayMaster));
        //获取sayMaster的实例方法
        Method sayMethod  = class_getInstanceMethod(self, @selector(sayMaster));
        //获取sayMaster的丰富签名
        const char *type = method_getTypeEncoding(sayMethod);
        //将sel的实现指向sayMaster
        return class_addMethod(self, sel, imp, type);
    }
    return [super resolveInstanceMethod:sel];
}

// 运行工程其打印如下
2021-08-01 17:51:30.226277+0800 DDDDD[5106:762939] sayHello 来了
2021-08-01 17:51:30.226734+0800 DDDDD[5106:762939] <LGPerson: 0x281ecc040> : -[LGPerson sayMaster]

类方法的动态决议

类方法动态决议源码

/*********************************************************
* 解析类方法
* 调用+resolveClass 方法,寻找要添加到类cls 的方法。
* cls 应该是一个元类。
* 不检查该方法是否已经存在。
*********************************************************/
static void resolveClassMethod(id inst, SEL sel, Class cls)
{
    runtimeLock.assertUnlocked();
    ASSERT(cls->isRealized());
    ASSERT(cls->isMetaClass());

    //当你为实现resolveClassMethod的时候,此处也不会进入return
    //因为系统给resolveClassMethod函数默认返回NO,即默认实现了
    if (!lookUpImpOrNilTryCache(inst, @selector(resolveClassMethod:), cls)) {
        // Resolver not implemented.
        return;
    }
    
    //nonmeta容错处理
    Class nonmeta;
    {
        mutex_locker_t lock(runtimeLock);
        nonmeta = getMaybeUnrealizedNonMetaClass(cls, inst);
        if (!nonmeta->isRealized()) {
            _objc_fatal("nonmeta class %s (%p) unexpectedly not realized",
                        nonmeta->nameForLogging(), nonmeta);
        }
    }
    //系统会在此处为你发送一个消息resolveClassMethod
    //当你的这个类检测了这个消息,并且做了处理
    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    bool resolved = msg(nonmeta, @selector(resolveClassMethod:), sel);

    //那么此时系统会重新查找,此函数最终会触发LookUpImpOrForward
    IMP imp = lookUpImpOrNilTryCache(inst, sel, cls);

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

LGPerson类中添加类方法如下,其中sayHappy方法没有实现

<!-- LGPerson.h文件 -->
+ (void)sayNB;
+ (void)sayHappy;

<!-- LGPerson.m文件 -->
+ (void)sayNB {
    NSLog(@"%@ : %s",self,__func__);
}

// 添加类方法动态决议
+ (BOOL)resolveClassMethod:(SEL)sel{
    if (sel == @selector(sayHappy)) {
        NSLog(@"%@ 来了", NSStringFromSelector(sel));
        
        IMP imp = class_getMethodImplementation(objc_getMetaClass("LGPerson"), @selector(sayNB));
        Method lgClassMethod  = class_getInstanceMethod(objc_getMetaClass("LGPerson"), @selector(sayNB));
        const char *type = method_getTypeEncoding(lgClassMethod);
        return class_addMethod(objc_getMetaClass("LGPerson"), sel, imp, type);
    }
    return [super resolveClassMethod:sel];
}

// 运行工程其打印如下
2021-08-01 18:46:27.239762+0800 DDDDD[5295:774648] sayHappy 来了
2021-08-01 18:46:27.239814+0800 DDDDD[5295:774648] LGPerson : +[LGPerson sayNB]

注意: resolveClassMethod类方法的重写传入的cls不再是类,而是元类。可以通过objc_getMetaClass方法获取类的元类,原因是类方法在元类中是实例方法

得出结论
  • 通过手动添加resolveInstanceMethod/resolveClassMethod可以防止方法未实现崩溃
  • 通过判断当前查找的sel动态添加一个新的imp
  • 此时sel(sayHello/sayHappy)对应实现的imp不再是sayHello/sayHappy,而是变成了我动态指定的sayMaster/sayNB
优化

日常开发中不可能每个类中都实现一份resolveInstanceMethod/resolveClassMethod,有没有更好的解决方案呢?通过方法慢速查找流程可以发现其查找路径有两条

  • 实例方法:类 -- 父类 -- 根类 -- nil
  • 类方法:元类 -- 根元类 -- 根类 -- nil

它们的共同点是如果前面没找到,都会来到根类即NSObject中查找,所以我们可以将上述的两个方法整合在一起。通过NSObject添加分类的方式来统一处理,而且由于类方法的查找在其继承链中查找的也是实例方法,所以可以将实例方法和类方法统一放在resolveInstanceMethod方法中处理,如下所示

@implementation NSObject (LG)

//为实例对象创建一个IMP
- (void) sayMaster {
    NSLog(@"%@ : %s",self,__func__);
}

//为元类对象创建创建一个IMP
+ (void) sayNB {
    NSLog(@"%@ - %s",self,__func__);
}

#pragma clang diagnostic push
// 让编译器忽略错误
#pragma clang diagnostic ignored "-Wundeclared-selector"

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(sayHello)) {
        NSLog(@"%@ 来了", NSStringFromSelector(sel));
        IMP imp = class_getMethodImplementation(self, @selector(sayMaster));
        Method sayMethod  = class_getInstanceMethod(self, @selector(sayMaster));
        const char *type = method_getTypeEncoding(sayMethod);
        return class_addMethod(self, sel, imp, type);
    } else if (sel == @selector(sayHappy)) {
        NSLog(@"%@ 来了", NSStringFromSelector(sel));
        IMP imp = class_getMethodImplementation(objc_getMetaClass("LGPerson"), @selector(sayNB));
        Method lgClassMethod  = class_getInstanceMethod(objc_getMetaClass("LGPerson"), @selector(sayNB));
        const char *type = method_getTypeEncoding(lgClassMethod);
        return class_addMethod(objc_getMetaClass("LGPerson"), sel, imp, type);
    }
    return NO;
}
#pragma clang diagnostic pop
@end

// 运行工程其打印如下
2021-08-01 19:20:31.446141+0800 DDDDD[5405:783621] sayHello 来了
2021-08-01 19:20:31.446775+0800 DDDDD[5405:783621] <LGPerson: 0x2838f8080> : -[NSObject(LG) sayMaster]
2021-08-01 19:20:31.446847+0800 DDDDD[5405:783621] sayHappy 来了
2021-08-01 19:20:31.446881+0800 DDDDD[5405:783621] LGPerson - +[NSObject(LG) sayNB]

这种方式的实现,正好与源码中针对类方法的处理逻辑是一致的,即完美阐述为什么调用了类方法动态方法决议,还要调用对象方法动态方法决议,其根本原因还是类方法在元类中的实例方法
当然,上面这种写法还是会有其他的问题,比如系统方法也会被更改,针对这一点是可以优化的,即我们可以针对自定义类中方法统一方法名的前缀,根据前缀来判断是否是自定义方法,然后统一处理自定义方法,例如可以在崩溃前pop到首页,主要是用于app线上防崩溃的处理,提升用户的体验。

aop和oop总结

oop面向对象编程,不同类做不同事情,分工明确。
  • 优点:耦合度很低
  • 缺点:代码冗余,常规解决办法是提取,这样会有一个公共类。所有人对公共类进行集成,形成强依赖,也就代表着出现了强耦合
aop:面向切面编程,是oop的延伸
  • 优点:对业务无侵入,通过动态方式将某些方法进行注入
  • 缺点:做了一些判断,执行了很多无关代码,包括系统方法,造成性能消耗,会打断apple的方法动态转发流程。

消息转发流程引入

在慢速查找的流程中,我们了解到如果快速+慢速没有找到方法实现,动态方法决议也不行,就使用消息转发。但是我们找遍了源码也没有发现消息转发的相关源码,可以通过以下方式来了解,方法调用崩溃前都走了哪些方法

  • 通过instrumentObjcMessageSends方式打印发送消息的日志

objc4-818.2源码全局搜索instrumentObjcMessageSends实现如下

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

探索instrumentObjcMessageSends流程

  • 通过lookUpImpOrForward --> log_and_fill_cache --> logMessageSend,在logMessageSend源码下方找到instrumentObjcMessageSends的源码实现
  • main中调用instrumentObjcMessageSends打印方法调用的日志信息,有以下两点准备
  1. 打开objcMsgLogEnabled开关,即调用instrumentObjcMessageSends方法时传入YES
  2. main中通过extern声明instrumentObjcMessageSends方法
extern void instrumentObjcMessageSends(BOOL flag);

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        LGPerson *person = [LGPerson alloc];
        //使用区域,将想要查找的方法用instrumentObjcMessageSends圈起来
        //开始监听传递参数为YES,结束监听传递参数为NO
        instrumentObjcMessageSends(YES);
        [person sayHello];
        // 这里再次赋值为NO,表示只查看sayHello方法
        instrumentObjcMessageSends(NO);
    }
    return 0;
}
  • 通过logMessageSend源码了解到消息发送打印信息存储在/tmp/msgSends目录
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目录,发现有msgSends开头的日志文件,打开发现在崩溃前执行了以下方法
    两次动态方法决议:resolveInstanceMethod方法
    两次消息快速转发:forwardingTargetForSelector方法
    两次消息慢速转发:methodSignatureForSelector + resolveInvocation
image.png

相关文章

  • 消息动态决议

    前言 前面我们分析了消息的查找流程,快速查找流程,慢速查找流程。如果这两种方式都没找到方法的实现,苹果给了两个建议...

  • 消息动态决议

    我们上一章针对[消息的慢速查找](https://www.jianshu.com/p/f03392ffe0b3[h...

  • 消息动态决议

    我们先从lookUpImpOrForward看起 realizeAndInitializeIfNeeded_loc...

  • 消息的动态决议

    当lookupImpOrForward函数从cache和methodTable中找不到对应Method,继续向下执...

  • runtime简单笔记

    动态特性: 动态类型、动态绑定、动态方法决议、动态加载、内省 编译器会把[接收器 消息]形式的对象消息,转换为含有...

  • Runtime — 动态方法决议

    前言 在 消息发送 中,当查不到方法时,会进行动态方法决议,下面我们就来具体分析一下动态方法决议过程中,系统如果操...

  • 九、消息流程之动态决议

    在消息经过慢速查找之后还没有找到,就会走到resolveMethod_locked进行消息动态决议,看一下reso...

  • 消息转发之动态方法决议 & 消息转发

    前言 前面的两篇文章我们已经探索了消息的快速查找和慢速查找的流程。 objc_msgSend 流程之缓存查找[ht...

  • 十、消息流程—动态方法决议 & 消息转发

    主要内容:objc_msgSend的快速查找和慢速查找,都没有找到方法时,苹果给了挽救的机会:一、动态方法决议1....

  • iOS Objective-C 消息的转发

    iOS Objective-C 消息的转发 1.动态方法决议(解析) 在上一篇消息查找的文章中我们在消息查找中没有...

网友评论

      本文标题:消息动态决议

      本文链接:https://www.haomeiwen.com/subject/zxghvltx.html