消息动态决议

2021-08-01  本文已影响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;

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

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

对象方法动态决议

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

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;
}
位运算
单例解读
.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
得出结论

保证了每一个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);
}

该函数内的三个关键函数

类动态方法决议会进入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));
        }
    }
}

主要步骤如下

崩溃修改

针对实例方法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,有没有更好的解决方案呢?通过方法慢速查找流程可以发现其查找路径有两条

它们的共同点是如果前面没找到,都会来到根类即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的延伸

消息转发流程引入

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

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流程

  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;
}
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;
}
image.png
上一篇 下一篇

猜你喜欢

热点阅读