objc_msgSend分析-动态解析+消息转发

1,711 阅读6分钟

环境:xcode 11.5

源码:objc4-781

非常重要的一幅图

首先回归一下objc_msgSend前面的流程:

  1. 汇编代码中在当前classcache中通过进行快速查找,核心函数是CacheLookup
  2. 没找到的话在当前class的方法列表中进行查找,然后在父类cache和方法列表中进行慢速查找,依次往上查询。
  3. 如果查找过程中遇到父类==nil或者父类cache中的imp=imp_forward,开始执行一次方法解析并返回结果。

方法解析

方法解析的入口函数为:resolveMethod_locked,首先我们看两个方法:

resolveInstanceMethod

当前的类为非元类,即普通类的时候,动态解析会执行resolveInstanceMethod,方法实现如下:

static void resolveInstanceMethod(id inst, SEL sel, Class cls)
{
    runtimeLock.assertUnlocked();
    ASSERT(cls->isRealized());
    SEL resolve_sel = @selector(resolveInstanceMethod:);

    if (!lookUpImpOrNil(cls, resolve_sel, cls->ISA())) {
        // 查找一下有没有实现resolveInstanceMethod类方法
        // Resolver not implemented.
        return;
    }

    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
   	// 调用类方法resolveInstanceMethod,如果有动态添加方法的话,此时会被添加到类的方法列表中去。
    bool resolved = msg(cls, resolve_sel, sel);
	
    // Cache the result (good or bad) so the resolver doesn't fire next time.
    // +resolveInstanceMethod adds to self a.k.a. cls
    // 在当前类中重新查找一遍sel,需要注意的是lookUpImpOrNil的具体实现中beahvior为LOOKUP_CACHE和LOOKUP_NIL,因此如果没有找到imp的话不会执行动态解析产生递归。如果找到的话,会将imp存入当前类的缓存中。
    IMP imp = lookUpImpOrNil(inst, sel, cls);
    ...
}

static inline IMP
lookUpImpOrNil(id obj, SEL sel, Class cls, int behavior = 0)
{
    return lookUpImpOrForward(obj, sel, cls, behavior | LOOKUP_CACHE | LOOKUP_NIL);
}

resolveInstanceMethod主要功能如下:

  1. 检测当前类及其父类是否实现+resolveInstanceMethod类方法。
  2. 调用+resolveInstanceMethod方法
  3. 重新进行慢速查找,如果有动态添加sel对应的imp,则存入缓存中。

resolveClassMethod

static void resolveClassMethod(id inst, SEL sel, Class cls)
{
    runtimeLock.assertUnlocked();
    ASSERT(cls->isRealized());
    ASSERT(cls->isMetaClass());

    if (!lookUpImpOrNil(inst, @selector(resolveClassMethod:), cls)) {
    	//查找一下有没有实现+resolveClassMethod类方法
        // 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;
    // 调用+resolveClassMethod类方法
    bool resolved = msg(nonmeta, @selector(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
    // 在元类列表中查找类方法sel是否实现,有的话放入元类的方法缓存中
    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 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));
        }
    }
}

resolveClassMethodresolveInstanceMethod类似:

  1. 查看当前类是否实现了+resolveClassMethod类方法
  2. 调用+resolveClassMethod方法。
  3. 在元类中查找sel对应的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()) {
    	// 类对象不是元类时,执行resolveInstanceMethod,解析实例方法
        // 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
    // 上述操作中有可能是已经将新增方法加入到了缓存中,因此这次查找增加了LOOKUP_CACHE,可以在当前类的cache中进行一次基于汇编的快速查找
    return lookUpImpOrForward(inst, sel, cls, behavior | LOOKUP_CACHE);
}

resolveMethod_locked会对当前的类对象进行判断,非元类会执行resolveInstanceMethod方法,元类的话会执行resolveClassMethod以及resolveInstanceMethod方法。

关于动态解析的总结

  • 当我们调用实例方法/类方法时,runtime会在类/元类以及对应父类的方法缓存以及方法列表中进行查找,如果查找不到则会执行一次方法的动态解析。

  • 方法的动态解析允许开发人员动态添加方法到当前的类/元类中去,来解决没有sel对应的imp的问题

  • 实例方法对应的是类的+resolveInstanceMethod

  • 因为类方法相当于元类的实例方法,因此,类方法动态解析可以对应+resolveClassMethod方法和元类+resolveInstanceMethod方法。

  • 因为类和元类都是NSObject的子类,因此我们可以通过在NSObject中重写resolveClassMethodresolveInstanceMethod来实现方法的动态解析。

  • 元类的元类根元类也是NSObject的子类,因此我们可以只通过在NSObject中重写resolveInstanceMethod方法来实现实例方法和类方法的动态解析。

代码验证

实例方法动态解析

上述代码必然会崩溃。因为ZHYPerson只有对应的方法声明,并没有实现。修改如下: 正常打印没有问题,因为已经添加了对应的imp

类方法动态解析

再来看类方法的情况,在+resolveClassMethod中给元类添加了方法之后也没有问题。

NSObject分类添加方法动态解析类方法和实例方法

接下来看一点稍微高级一点的写法: 增加NSObject的分类如下: main函数如下: 只是在NSObjectresolveInstanceMethod方法中对ZHYPerson类的实例方法instanceMethod和类方法classMethod进行了解析,程序运行正常。

tips

细心的人可能会发现,在上图的场景中,会打印出两个来了--instanceMethod,这里涉及到了消息转发的相关内容,会在后面进行分析。

消息转发

执行动态解析后,runtime会重新查找一遍缓存和方法列表,确认sel是否添加了对应的imp,如果仍然没有找到imp,将会执行消息转发流程。如下图所示: forwardingTargetForSelector是由__forwarding_prep_0__触发的。点进来看一下: __forwarding_prep_0__属于CoreFoundation![](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/2d9b2d7a7a484e239a184d1fabc2d25e~tplv-k3u1fbpfcp-zoom-1.image)框架,但是该框架并没有开源,我们可以通过hopper反编译工具进行查看一些端倪。

反汇编查看伪代码

  1. 通过lldb指令找到CoreFoundation的位置:
  2. finder中找到对应的文件
  3. 拖入hopper中搜索对应的方法__forwarding_prep_0__
  4. 点击跳转到____forwarding___方法 如果实现了forwardingTargetForSelector方法,但是返回为空或者forwardingTargetForSelector未实现时,会执行methodSignatureForSelector逻辑如下:
  5. 如果未实现methodSignatureForSelector方法,跳转到loc_64dd7位置: ,最终执行了doesNotRecognizeSelector方法。
  6. 如果实现了methodSignatureForSelector方法,但是返回值为空,跳转到loc_64e3c位置 最终执行了doesNotRecognizeSelector方法。

总结

至此,消息转发的流程图如下所示:

关于resolveInstanceMethod执行两次的分析

上图是第二次执行resolveInstanceMethod时的堆栈信息,可以发现是被methodSignatureForSelector方法中调起的。 CoreFoundation[NSObject methodSignatureForSelector:]可以看到整个的实现。 我们需要注意的是,一般来说我们会对这几个方法进行重写已处理消息转发的自定义流程,如果最终调用到了NSObject的实现说明我们并没有处理特定的sel

NSObject的默认实现为返回当前所传sel的方法签名,因此调用了class_getInstanceMethod来获取对应的实例方法,实现如下: 正是因为lookUpImpOrForwardLOOKUP_RESOLVER参数的存在,才会最终重新触发一次resolveInstanceMethod