OC底层原理(09)动态方法决议

一. forward_imp

根据前两篇文章,提出两个问题:

  • forward_imp是什么?
  • 如果方法找不到,如何补救?

1.1 forward_imp 是什么?

在上篇文章 imp慢速查找 中知道了:如果方法未找到,即 superclass 一直找到 nil,任未找到,则 imp 会被设置为 forward_imp。那么forward_imp是什么呢? 在慢速查找流程lookUpImpOrForward方法的第一行代码,即对forward_imp进行了赋值:

const IMP forward_imp = (IMP)_objc_msgForward_impcache;
复制代码

__objc_msgForward_impcache 是汇编实现的,在objc_msg_arm64.s中查找到方法的实现:

    STATIC_ENTRY __objc_msgForward_impcache

    // No stret specialization.
    b	__objc_msgForward  //跳转 __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
    
    ...
    .macro TailCallFunctionPointer
	// $0 = function pointer value
	br	$0    //跳转 imp
    .endmacro
    ...
复制代码

__objc_msgForward_impcache 中调用了 __objc_msgForward__objc_msgForward TailCallFunctionPointer 是个宏,前面分析过就是跳转 impx17寄存器存放的是imp,从汇编中可以看出跟x17有关系的就是 __objc_forward_handler

搜索 __objc_forward_handler 汇编中没有具体的实现,可能在 C/C++ 源码中,全局搜索objc_forward_handler,源码如下:

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

有没有一种很熟悉的赶脚... 没错就是我们日常开发中遇到的常见错误:函数未实现,运行程序崩溃时报的错误描述信息。

1.2 找不到方法,怎么办?

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

接下来,来看看 动态方法决议

二. 动态方法决议

在上篇文章 imp慢速查找superclassnil 时,跳出循环执行 resolveMethod_locked 即方法决议

// 如果查询方法的没有实现,系统会尝试一次方法解析 
// behavior = 3,LOOKUP_RESOLVER = 2;
// 3 & 2 = 2,条件成立,进入 if {}   
if (slowpath(behavior & LOOKUP_RESOLVER)) {
    // behavior = behavior ^ LOOKUP_RESOLVER = 3 ^ 2 = 1         behavior ^= LOOKUP_RESOLVER; 
    // 动态方法决议       
    return resolveMethod_locked(inst, sel, cls, behavior);     
}
复制代码

2.1 resolveMethod_locked 源码分析

slowpath(behavior & LOOKUP_RESOLVER)可以理解为一个开关阀,保证动态方法决议只会执行一次!进入 resolveMethod_locked 方法:

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

    runtimeLock.unlock();
    // 判断cls类是否是元类,如果类说明说明调用的是实例方法
    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 (!lookUpImpOrNilTryCache(inst, sel, cls)) {
            resolveInstanceMethod(inst, sel, cls);
        }
    }

    // chances are that calling the resolver have populated the cache
    // so attempt using it
    // 快速查找和慢速查找sel对应的imp返回imp 实际上就是从缓存中取,因为前面已经缓存过了
    return lookUpImpOrForwardTryCache(inst, sel, cls, behavior);
}
复制代码

流程分析:

  • 判断cls的类型;
  • 如果是类,则执行实例方法的动态决议resolveInstanceMethod方法。
  • 如果是元类,则执行类方法的动态决议resolveClassMethod方法。如果元类中没有找到该实例方法或者为空,则在元类的实例方法的动态方法决议resolveInstanceMethod中查找。为什么呢?因为类方法在元类中,是以对象方法的形式存储,所以需要执行元类的实例对象决议方法。也就是说类是元类的实例对象
  • lookUpImpOrForwardTryCache快速查找和慢速查找sel对应的imp 然后返回imp

2.2 resolveInstanceMethod 源码分析

static void resolveInstanceMethod(id inst, SEL sel, Class cls)
{ 
    // inst 对象  // cls 类
    runtimeLock.assertUnlocked();
    ASSERT(cls->isRealized());
    SEL resolve_sel = @selector(resolveInstanceMethod:);
    // 只要cls的元类初始化 resolve_sel 方法一定有,因为NSObject默认实现了resolveInstanceMethod
    // 目的是将 resolveInstanceMethod 方法缓存到cls的元类中
    // 通过lookUpImpOrNilTryCache的参数我们知道 `resolve_sel `是类方法
    if (!lookUpImpOrNilTryCache(cls, resolve_sel, cls->ISA(/*authenticated*/true))) {
        // Resolver not implemented.
        return;
    }
    // 发送消息调用 resolveInstanceMethod 方法
    // 通过 objc_msgSend 发送消息 接收者是cls说明是类方法
    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    // 判断 resolve_sel 方法有没有实现,注意是`resolve_sel`方法
    bool resolved = msg(cls, resolve_sel, sel);
    
    // 为什么还有调用 lookUpImpOrNilTryCache 查询缓存和慢速查找呢
    // 虽然 resolveInstanceMethod 方法调用了。但是里面不一定实现了sel 的方法
    // 所以还是要去查找sel对应的imp,如果没有实现就会把imp = forward_imp 插入缓存中
    // 因为慢速查找流程已经走过了,此时 返回的 imp = forward_imp走down和down_unlock
    IMP imp = lookUpImpOrNilTryCache(inst, sel, cls);
     
    // resolved 和 imp 存在说明动态添加了
    if (resolved  &&  PrintResolving) {
        if (imp) {
            ...
        }
        else {
            // Method resolver didn't add anything?
            ...
        }
    }
}
复制代码
  • 首先创建 resolveInstanceMethodSEL resolve_sel

  • 根据lookUpImpOrNilTryCache (cls, resolve_sel, cls->ISA(true)) 知道 resolveInstanceMethod是类方法,通过快速和慢速查找流程查找resolve_sel对应的imp,缓存resolveInstanceMethod方法

  • 直接通过msg(cls, resolve_sel, sel)给类发送消息,从这里也能看到resolveInstanceMethod是类方法

lookUpImpOrNilTryCache(inst, sel, cls)快速和慢速查找流程:

  • 通过lookUpImpOrNilTryCache来确定resolveInstanceMethod方法中有没有实现sel对应的imp
  • 如果实现了,缓存中没有,进入lookUpImpOrForward查找到sel对应imp插入缓存,调用imp查找流程结束
  • 如果没有实现,缓存中没有,进入lookUpImpOrForward查找,sel没有查找到对应的imp,此时imp = forward_imp动态方法决议只调用一次,此时会走done_unlockdone流程,即 selforward_imp插入缓存,进行消息转发

2.3 resolveClassMethod 源码分析

static void resolveClassMethod(id inst, SEL sel, Class cls)
{
    runtimeLock.assertUnlocked();
    ASSERT(cls->isRealized());
    ASSERT(cls->isMetaClass());
    // inst 类 //cls元类
    // 查询元类有没有实现  NSObject默认实现resolveClassMethod方法
    if (!lookUpImpOrNilTryCache(inst, @selector(resolveClassMethod:), cls)) {
        // 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;
    // 向类中发送消息
    bool resolved = msg(nonmeta, @selector(resolveClassMethod:), sel);
   
    // 类方法相当于元类中的实例方法,同样去快速和慢速的查找
    IMP imp = lookUpImpOrNilTryCache(inst, sel, cls);
    ...
}
复制代码
  • resolveClassMethodNSobject中已经实现,只要元类初始化就可以了,目的是缓存在元类中
  • 调用 resolveClassMethod 类方法,目的是实现可能resolveClassMethod 方法中动态实现 sel 对应的 imp
  • imp = lookUpImpOrNilTryCache(inst, sel, cls) 缓存sel对应的imp,不管imp有没有动态添加,如果没有缓存的就是forward_imp

2.4 lookUpImpOrNilTryCache

lookUpImpOrNilTryCache 方法名字,可以理解就是查找 imp 或者 nil尽可能的通过查询 cache 的方式,在resolveInstanceMethod方法和resolveClassMethod 方法都调用 lookUpImpOrNilTryCache

extern IMP lookUpImpOrNilTryCache(id obj, SEL, Class cls, int behavior = 0);

 IMP lookUpImpOrNilTryCache(id inst, SEL sel, Class cls, int behavior)
{
    // LOOKUP_NIL = 4  没有传参数behavior = 0,  0 | 4 = 4
    return _lookUpImpTryCache(inst, sel, cls, behavior | LOOKUP_NIL);
}
复制代码

首先最后一个参数默认是 behavior = 0

ALWAYS_INLINE
static IMP _lookUpImpTryCache(id inst, SEL sel, Class cls, int behavior)
{
    runtimeLock.assertUnlocked();
    // cls 是否初始化
    if (slowpath(!cls->isInitialized())) {
        // 没有初始化就去查找 lookUpImpOrForward 查找时可以初始化
        return lookUpImpOrForward(inst, sel, cls, behavior);
    }
    // 在缓存中查找sel对应的imp
    IMP imp = cache_getImp(cls, sel);
    // imp 有值 进入done流程
    if (imp != NULL) goto done;
#if CONFIG_USE_PREOPT_CACHES
    // 是否有共享缓存
    if (fastpath(cls->cache.isConstantOptimizedCache(/* strict */true))) {
        imp = cache_getImp(cls->cache.`preoptFallbackClass(), sel);
    }`
#endif
    // 缓存中没有查询到 imp 进入慢速查找流程
    // behavior = 4 ,4 & 2 = 0 不会进入动态方法决议,所以不会一直循环
    if (slowpath(imp == NULL)) {
        return lookUpImpOrForward(inst, sel, cls, behavior);
    }

done:
    // (behavior & LOOKUP_NIL) = 4 & 4 = 1
    // LOOKUP_NIL 只是配合_objc_msgForward_impcache 写入缓存
    if ((behavior & LOOKUP_NIL) && imp == (IMP)_objc_msgForward_impcache) {
        return nil;
    }
    return imp;
}
复制代码

判断cls是否初始化一般都会初始化的

缓存中查找

  • 在缓存中查找sel对应的imp,如果imp存在跳转done流程
  • 判断是否有共享缓存给系统底层库用的
  • 如果缓存中没有查询到 imp,进入慢速查找流程 慢速查找流程
  • 慢速查找流程中,behavior= 44 & 2 = 0 不会进入动态方法决议
  • 最重要的如果没有查询到,此时 imp = forward_imp,跳转lookUpImpOrForward 中的 done_unlockdone 流程,插入缓存,返回forward_imp

done流程:

done流程: (behavior & LOOKUP_NIL) 且 imp = _objc_msgForward_impcache,如果缓存中的是forward_imp,就直接返回nil,否者返回的impLOOKUP_NIL条件就是来查找是否动态添加了imp还有就是将imp插入缓存

lookUpImpOrNilTryCache 的主要作用通过LOOKUP_NIL来控制插入缓存,不管sel对应的imp有没有实现,还有就是如果imp返回了有值那么一定是在动态方法决议中动态实现了imp

三 实例探究

3.1 resolveInstanceMethod实例探究

YJPerson.h中声明 say1 方法,不实现; 在 YJPerson.m 类中实现 resolveInstanceMethod 方法:

@implementation YJPerson
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
    NSLog(@"resolveInstanceMethod : %@ - %@", self, NSStringFromSelector(sel));
    return [super resolveInstanceMethod:sel];
}
@end
复制代码

调用:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        YJPerson *person = [[YJPerson alloc] init];
        [person say1];
    }
    return 0;
}
复制代码

Xnip2022-07-11_22-29-30.png

在崩溃之前确实调用了 resolveInstanceMethod方法,但为什么会调用两次resolveInstanceMethod 方法呢?

通过 bt 查看堆栈信息:

Xnip2022-07-12_09-29-35.png

  • 第一次,和我们分析的是一致的,是在查找 say1 时没有找到,会进入动态方法决议,发送 resolveInstanceMethod 消息。

  • 第二次,底层系统库 CoreFoundation调起,在消息转发完成以后再次开启慢速查找流程,进入动态方法决议又调用一次 resolveInstanceMethod,所以总共是两次

3.2 动态添加 sayNice 方法

@implementation YJPerson
- (void)sayNice {
    NSLog(@"%s", __func__);
}

+ (BOOL)resolveInstanceMethod:(SEL)sel
{
    NSLog(@"resolveInstanceMethod : %@ - %@", self, NSStringFromSelector(sel));
    if (@selector(say1) == sel) {
        IMP imp = class_getMethodImplementation(self , @selector(sayNice));
        Method meth = class_getInstanceMethod(self, @selector(sayNice));
        const char * type = method_getTypeEncoding(meth);
        return  class_addMethod(self , sel, imp, type);;
    }
    return [super resolveInstanceMethod:sel];
}
@end
复制代码

Xnip2022-07-12_09-46-37.png

  • resolveInstanceMethod只调用一次,因为动态添加了 sayNice 方法lookUpImpOrForwardTryCache 直接获取 imp,然后调用,查找流程结束
  • 具体流程:
    • resolveMethod_locked-->
    • resolveInstanceMethod(向类发送决议方法,类中是否有处理:将imp指向新的实现) -->
    • lookUpImpOrNilTryCache(inst, sel, cls) --> _lookUpImpTryCache (快速查找cache_getImp,结果没有;慢速查找lookUpImpOrForward,有了(因为类中已经将imp指向新的实现),同时加入缓存) -->
    • lookUpImpOrForwardTryCache-->_lookUpImpTryCache (快速查找cache_getImp,这时已经有了,直接return imp)

3.3 resolveClassMethod实例探究

YJPerson.h 中声明 +(void)sayClass方法,不实现 在 YJPerson.m 类中实现 resolveClassMethod 方法:

@implementation YJPerson
+ (BOOL)resolveClassMethod:(SEL)sel
{
    NSLog(@"resolveInstanceMethod : %@ - %@", self, NSStringFromSelector(sel));
    return [super resolveClassMethod:sel];
}
@end
复制代码

调用

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        [YJPerson sayClass];
    }
    return 0;
}
复制代码

Xnip2022-07-12_11-16-51.png

  • 在崩溃之前确实调用了resolveClassMethod方法,而且调用了两次,调用两次的逻辑和resolveInstanceMethod方法调用两次是一样的

  • 调用resolveClassMethod以后,会去查找lookUpImpOrNilTryCache有没有具体动态实现sel对应的imp,元类的缓存中此时有sel对应的imp,这个impforward_implookUpImpOrNilTryCache里面有判断直接返回nil,此时直接到resolveInstanceMethod查找,因为类方法实际上就是元类中的实例方法

  • 如果最后还是没有实现lookUpImpOrForwardTryCache获取到forward_imp进入消息转发流程

3.4 动态添加resayClass方法

+ (BOOL)resolveClassMethod:(SEL)sel{
  if (@selector(sayClass) == sel) {
    NSLog(@"resolveClassMethod : %@ - %@", self, NSStringFromSelector(sel));
    IMP imp = class_getMethodImplementation(object_getClass([self class]), @selector(resayClass));
    Method meth = class_getClassMethod(object_getClass([self class]) , @selector(resayClass));
    const char * type = method_getTypeEncoding(meth);
    return  class_addMethod(object_getClass([self class]) ,sel, imp, type);;
  }
    return [super resolveClassMethod:sel];
}

+ (void)resayClass {
    NSLog(@"--%s---", __func__);
}
复制代码

Xnip2022-07-12_11-22-30.png

  • resolveClassMethod只调用一次,因为动态添加了test方法

  • resolveClassMethodresolveInstanceMethod的调用流程基本一样,如果resolveClassMethod没有查询到调用一次resolveInstanceMethod调用

3.5 整合动态方法决议

resolveClassMethod 方法中如果没有动态添加类方法,会调用 元类 中的resolveInstanceMethod。那么能不能把 resolveInstanceMethod 写到一个公用类中,使 类方法实例方法 都能调用

  • 实例方法查找流程:对象 --> -->直到根类(NSObject) --> nil
  • 类方法查找流程: --> 元类 -->直到根类(NSObject) --> nil 到最后都找到NSObject类中,所以这个公用类就是NSObject分类
@implementation NSObject (YJ)
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
    NSLog(@"resolveInstanceMethod : %@ - %@", self, NSStringFromSelector(sel));
    if (@selector(say1) == sel) 
    {
        IMP imp = class_getMethodImplementation(self , @selector(sayNice));
        Method meth = class_getInstanceMethod(self , @selector(sayNice));
        const char * type = method_getTypeEncoding(meth);
        return  class_addMethod(self ,sel, imp, type);;
    }
    else if (@selector(sayClass) == sel)
    {
        IMP imp = class_getMethodImplementation(object_getClass([self class]), @selector(resayClass));
        Method meth = class_getClassMethod(object_getClass([self class]) , @selector(resayClass));
        const char * type = method_getTypeEncoding(meth);
        return  class_addMethod(object_getClass([self class]) ,sel, imp, type);;
    }
    return NO;
}

- (void)sayNice {
     NSLog(@"%s", __func__);
}

+ (void)resayClass {
    NSLog(@"%s", __func__);
}
@end
复制代码

Xnip2022-07-12_11-49-40.png

不管是实例方法还是类方法调用,系统都自动调用了resolveInstanceMethod方法,和上面探究的吻合。 动态方法决议优点

  • 可以统一处理方法崩溃的问题,出现方法崩溃可以上报服务器

猜你喜欢

转载自juejin.im/post/7119345032875737102