iOS源码分析之IMP查找及消息转发

在xcode中使用快捷键command+shift+0打开官方文档,搜索Objective-C Runtime可以看到有这样一段描述:

Overview
The Objective-C runtime is a runtime library that provides support for the dynamic properties of the Objective-C language, and as such is linked to by all Objective-C apps. Objective-C runtime library support functions are implemented in the shared library found at /usr/lib/libobjc.A.dylib.
...
译文:Objective-C运行时是一个运行时库,它为Objective-C语言的动态属性提供支持,因此所有Objective-C应用程序都链接到它。Objective-C运行时库支持在在/usr/lib/libobjc.A.dylib的共享库中实现的函数。

我们都知道OC是一门动态性语言,主要是因为苹果公司基于Runtime,在C语言的基础上,加上编译器运行时环境组成了OC语言的基本框架。编译器clang可以将OC语言转换成C++语言,而cc++都是静态语言,一经编辑之后就不能再添加新的类、新的方法或者其他结构上的一些变化,而Runtime则可以实现这些动态性。

关于Runtime的基本概念以及简单应用,我在之前的文章已经写过传送门,本文主要想通过源码进行分析,分析过程比较长,可直接在文章末尾看总结。苹果开源官网下载objc4-818.2源码,如果想要调试源码可以下载大神编译好的objc4_debug


Objective-C从三种不同的层级与Runtime进行交互

  • 通过Objective-C源代码:大部分情况下运行时系统在后台自动运行,我们只需要编辑Objective-C源代码,当编译OC类和方法时,编译器为了实现语言动态特性将自动创建一些数据结构和函数。
  • 通过NSObject的方法间接调用:Cocoa程序中大部分的类都是继承自NSObject,继承了NSObject的行为。某些情况下NSObject类仅仅是定义了方法模板而没有提供所有需要的代码,如 description方法会返回该类内容的字符串表示,然后NSObject并不知道子类中的内容,所以它只是返回类的名字和对象的地址,子类可以重新实现该方法以提供更多的信息。有些方法只是简单的从Runtime系统中获取信息,从而对允许对象进行一定程序的自我检查,如class,superclass,isMemberOfClass,isKindOfClass,isSubclassOfClass,isAncestorOfObject,respondsToSelector,conformsToProtocol,methodForSelector这些方法的实现都调用了runtime中定义的方法。
  • 直接通过Runtime提供的API:Runtime提供的API很多,大致可以总结为几大类
    • objc_xxx函数:这些函数是最顶层的操作,类或协议开配空间创建、注册,类、元类对象、协议获取,操作关联对象,发送objc消息objc_msgSend
    • class_xxx函数:这些函数是对类的内部进行操作,如创建类的实例,添加实例变量、属性、方法、协议,获取、拷贝类包含的信息,检查判断。
    • object_xxx函数:针对对象进行操作,如获取/设置对象的类、实例变量的值。
    • method_xxx函数:针对方法进行操作,获取方法的参数及返回值,方法的实现以及交换等。
    • property_xxx函数:获取属性名称、特性列表,拷贝属性列表、特性值。
    • protocol_xxx函数:与协议相关操作
    • ivar_xxx函数:获取实例变量的名称、类型编码、偏移量
    • sel_xxx函数:方法编号相关
    • imp_xxx函数:方法实现相关的block创建、获取、删除

clang分析

创建一个类继承自NSObject例如CLAnimal,添加一个方法-(void)eat,在main.m中初始化并调用CLAnimal *ani = [[CLAnimal alloc] init]; [ani eat];,使用clang命令将main.m转换为main.cpp文件

#终端命令  -rewrite-objc           Rewrite Objective-C source to C++
clang -rewrite-objc main.m

查看clang生成的main.cpp文件最后面如下

...此处省略十万八千行...
typedef struct objc_object CLAnimal;
typedef struct {} _objc_exc_CLAnimal;
#endif

struct CLAnimal_IMPL {
    struct NSObject_IMPL NSObject_IVARS;
};


// -(void)eat;
/* @end */

#pragma clang assume_nonnull end
int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 

        NSLog((NSString *)&__NSConstantStringImpl__var_folders_2__ts8l68y17cncks73d_vjz4vh0000gn_T_main_bf6255_mi_0);

        CLAnimal *animal = ((CLAnimal *(*)(id, SEL))(void *)objc_msgSend)((id)((CLAnimal *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("CLAnimal"), sel_registerName("alloc")), sel_registerName("init"));
        ((void (*)(id, SEL))(void *)objc_msgSend)((id)animal, sel_registerName("eat"));

    }
    return 0;
}
static struct IMAGE_INFO { unsigned version; unsigned flag; } _OBJC_IMAGE_INFO = { 0, 2 };

我们同样可以直接使用使用这种写法,需要导入头文件#import ,另外往父类发送消息有些不同objc_msgSendSuper(id super, SEL _cmd), 其中super是一个结构体objc_super{ id receiver,Class super_class}, 从super_class中查找方法,消息接受者super->receiver仍然是self

从上面的代码可以很直观的得出两个结论:

  • OC对象的本质是就是一个结构体objc_object
  • 方法的本质就是消息发送objc_msgSend(id self, SEL _cmd)id类型参数表示调用者,_cmd表示方法编号

objc_msgSend的实现过程

从源码中查找c函数objc_msgSend,发现并没有它的实现,因为它是使用汇编来实现的,搜索对应的汇编代码ENTRY _objc_msgSend。为什么要使用汇编呢?原因有两个,一是因为C函数不能直接保留未知的参数,然后跳转到任意的指针,但是汇编有寄存器可以保存x0~x31,二是因为使用汇编效率高,汇编语言是二进制指令的文本形式,与指令是一一对应的关系,是最底层的低级语言。

    ENTRY _objc_msgSend //方法入口
    UNWIND _objc_msgSend, NoFrame 

    cmp p0, #0          // nil check and tagged pointer check
#if SUPPORT_TAGGED_POINTERS
    b.le    LNilOrTagged        //  (MSB tagged pointer looks negative)
#else
    b.eq    LReturnZero
#endif
    ldr p13, [x0]       // p13 = isa
    GetClassFromIsa_p16 p13, 1, x0  // p16 = class
LGetIsaDone:
    // calls imp or objc_msgSend_uncached
    CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached

#if SUPPORT_TAGGED_POINTERS
LNilOrTagged:
    b.eq    LReturnZero     // nil check
    GetTaggedClass
    b   LGetIsaDone
// SUPPORT_TAGGED_POINTERS
#endif

LReturnZero:
    // x0 is already zero
    mov x1, #0
    movi    d0, #0
    movi    d1, #0
    movi    d2, #0
    movi    d3, #0
    ret

    END_ENTRY _objc_msgSend

汇编部分我们可以大致分析一下:

  • 首先检查是否为niltagged pointer类型(NSNumber、NSDate、NSString等小对象),是执行LNilOrTagged,再次检查是nil则跳转到LReturnZero结束,tagged pointer类型执行GetTaggedClass ---> LGetIsaDone
  • 第一步检查不是nil或tagged pointer类型将会继续执行GetClassFromIsa_p16 ---> LGetIsaDone
  • LGetIsaDoneisa操作之后会调用CacheLookup 在缓存中查找imp,找到则调用 imp,找不到调用objc_msgSend_uncached
  • LGetIsaDone中只是对CacheLookup这个宏的调用CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached
   // calls imp or objc_msgSend_uncached
  .macro CacheLookup Mode, Function, MissLabelDynamic, MissLabelConstant
   LLookupStart\Function:
  1.  ldp   p17, p9, [x13], #-BUCKET_SIZE   //     {imp, sel} = *bucket--
      cmp   p9, p1              //     if (sel != _cmd) {
      b.ne  3f              //         scan more
  2.  CacheHit \Mode                // hit:    call or return imp
  3.  cbz   p9, \MissLabelDynamic       //     if (sel == 0) goto Miss;
      #if CACHE_MASK_STORAGE == ...
      add ...
  4.  ldp   p17, p9, [x13], #-BUCKET_SIZE   //     {imp, sel} = *bucket--
      cmp   p9, p1              //     if (sel == _cmd)
      b.eq  2b              //         goto hit
      cmp   p9, #0              // } while (sel != 0 &&
      ccmp  p13, p12, #0, ne        //     bucket > first_probed)
      b.hi  4b

LLookupStart\Function:可以看到查找的4种情况(通过注释以及单词大致推测):

  1. sel_cmd不匹配,继续
  2. 执行或返回imp
  3. 找不到则调用MissLabelDynamicCacheLookup的第三个参数__objc_msgSend_uncached,随后添加到缓存中
  4. 根据sel循环查找,查找到执行2
  • 先看下CacheHit这个方法,它也是个宏,这里进入时为NORMAL,直接调用IMPTailCallCachedImp x17, x10, x1, x16
  • 接下来我们需要看一下__objc_msgSend_uncached的实现
  • 继续搜索跟踪发现它的实现里面又调用了MethodTableLookup,翻译一下就是方法列表查找,而它下面TailCallFunctionPointer x17翻译一下就是调用函数指针,很明显得出结论是在MethodTableLookup这个方法中进行的IMP查找,不然下面x17里面的地址从哪来呢。
  • 查看MethodTableLookup实现,bl跳转到_lookUpImpOrForward,继续搜索_lookUpImpOrForward,咦?全是callbl,没有实现??仔细看汇编实现里面有两行注释,原来是在C函数lookUpImpOrForward里面
.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

最终在objc_runtime_new.mm文件中找到它的实现方法,看到这里终于舒服一些了,这一刻我忽然觉得C语言竟然也是这么的亲切~,这个方法就是漫长的IMP查找流程,我在里面加了一些中文注释,最新的源码与之前的版本有些不一样,但是大体的逻辑都是一样的

IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior){
    const IMP forward_imp = (IMP)_objc_msgForward_impcache;
    IMP imp = nil;
    Class curClass;
    runtimeLock.assertUnlocked();
   if (slowpath(!cls->isInitialized())) {//判断类是否已经被初始化通过元类中的class_rw_t中的flag字段
       behavior |= LOOKUP_NOCACHE;
    }
    runtimeLock.lock();
    checkIsKnownClass(cls);//检查是否为runtime中已知的类,包括共享缓存,加载的image或使用api注册的
    cls = realizeAndInitializeIfNeeded_locked(inst, cls, behavior & LOOKUP_INITIALIZE);
    runtimeLock.assertLocked();
    curClass = cls;
   for (unsigned attempts = unreasonableClassCount();;) {
        if (curClass->cache.isConstantOptimizedCache(/* strict */true)) {
#if CONFIG_USE_PREOPT_CACHES
            imp = cache_getImp(curClass, sel);
            if (imp) goto done_unlock;
            curClass = curClass->cache.preoptFallbackClass();
#endif
        } else {
           // curClass method list.获取当前类的方法列表中查找
            Method meth = getMethodNoSuper_nolock(curClass, sel);
            if (meth) {
                imp = meth->imp(false);
                goto done; //若找到,则跳转到done位置
            }

            if (slowpath((curClass = curClass->getSuperclass()) == nil)) {
                //将curClass的父类 赋值给curClass
                // No implementation found, and method resolver didn't help.
                // Use forwarding.
                imp = forward_imp;
                break;//若当前类是NSObject,那么不需要再继续查找了,跳出循环 给imp赋值为forward_imp
            }
        }
         // Halt if there is a cycle in the superclass chain.
        if (slowpath(--attempts == 0)) {
            _objc_fatal("Memory corruption in class list.");
        }

        // Superclass cache. 
        //当前类不是NSObject,查找父类缓存,这里又是汇编查找:ENTRY _cache_getImp --> CacheLookUp
        imp = cache_getImp(curClass, sel);
        if (slowpath(imp == forward_imp)) {
            //找到的是一个转发方法,停止搜索,且不进行缓存
            // Found a forward:: entry in a superclass.
            // Stop searching, but don't cache yet; call method
            // resolver for this class first.
            break;
        }
        if (fastpath(imp)) {
            //在父类中找到了这个方法,缓存它到当前类
            // Found the method in a superclass. Cache it in this class.
            goto done;
        }
    }

 // No implementation found. Try method resolver once.
//最终没有找到实现方法,那么尝试一次调用resolver
    if (slowpath(behavior & LOOKUP_RESOLVER)) {
        behavior ^= LOOKUP_RESOLVER;
        return resolveMethod_locked(inst, sel, cls, behavior);
    }

 done:
    if (fastpath((behavior & LOOKUP_NOCACHE) == 0)) {
#if CONFIG_USE_PREOPT_CACHES
        while (cls->cache.isConstantOptimizedCache(/* strict */true)) {
            cls = cls->cache.preoptFallbackClass();
        }
#endif
    //将imp添加到当前类缓存的方法
        log_and_fill_cache(cls, imp, sel, inst, curClass);
    }
 done_unlock:
    runtimeLock.unlock();
    if (slowpath((behavior & LOOKUP_NIL) && imp == forward_imp)) {
        return nil;
    }
    return imp;
}

至此IMP查找环节就结束了,若没有查找到IMP则开始动态方法解析


动态解析

接着上一步IMP查找,如果没有找到,则会调用resolveMethod_locked,在此方法中会根据是否为元类分别调用resolveClassMethodresolveInstanceMethod

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 (!lookUpImpOrNilTryCache(inst, sel, cls)) {
            resolveInstanceMethod(inst, sel, cls);
        }
    }

    // chances are that calling the resolver have populated the cache
    // so attempt using it
    return lookUpImpOrForwardTryCache(inst, sel, cls, behavior);
}

以类方法resolveInstanceMethod为例,我们可以写个示例测试一下,调用一个未实现的SEL,并重写resolveInstanceMethod,然而发现,这个方法竟然被调用了两次?

image.png

仔细分析两次的调用栈并不一样,第一次调用确实是与刚才的分析一致,第二次是从methodSignatureForSelector调用,具体的流程暂且不清楚,但是可以确认的一点是我们还可以在第一次调用resolveInstanceMethod:之后的methodSignatureForSelector方法内去处理消息。

我们尝试使用runtime动态添加添加一个IMP,代码示例,果然不再崩溃,另外很多人会被那张经典的消息转发流程图所误导,以为只有返回YES消息才是已处理,NO会尝试消息转发,其实不然,查看源码发现方法返回值只跟日志打印有关系,只有在返回YES并且OBJC_PRINT_RESOLVED_METHODS = YES时才会打印,可以自己尝试验证一下

image.png

image.png

若没有处理消息会发生什么呢?通过debug源码跟踪发现在动态方法解析之后会再次调用lookUpImpOrForward方法,并在最后一次for循环也就是NSObject层时,会对IMP进行赋值为forward_imp,这个forward_imp = _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

经过一顿搜索查找。。。跟不下去了,没有后续的源码实现了,苹果对这块并没有开源,怎么办呢?我们只能通过查看log来试试,在源码内部有一个非常好用的函数

// env NSObjCMessageLoggingEnabled
OBJC_EXPORT void
instrumentObjcMessageSends(BOOL flag)
    OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);

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

从注释可以看到我们可以直接添加环境变量NSObjCMessageLoggingEnabled来使用,它会输出一个tmp/msgSends-(xxx pid)的文件,搜索logMessageSend方法可以看到写入文件路径,这里的完整路径是Macintosh HD --> private --> tmp --> msgSends-xxx,在818.2源码添加完可能没有写入成功,将源码中的objcMsgLogLock注释掉,然后重新编译一下objc再进行debug即可

bool logMessageSend(bool isClassMethod,
                    const char *objectsClass,
                    const char *implementingClass,
                    SEL selector)
 {
    ...
    //    objcMsgLogLock.lock();
        write (objcMsgLogFD, buf, strlen(buf));
    //    objcMsgLogLock.unlock();
  }
image.png

可以看到在崩溃之前,它内部的方法调用顺序,在动态解析之后调用了forwardingTargetForSelector:方法,然后又调用了methodSignatureForSelector,哈哈,爽歪歪有没有?

消息转发

这部分源码并没有开源,我们只能得到它的调用顺序,然我们可以在苹果的官方文档查看forwardingTargetForSelector用法和描述,使用command+shift+0打开官方文档,只摘重点

If an object implements (or inherits) this method, and returns a non-nil (and non-self) result, 
that returned object is used as the new receiver object and the message dispatch resumes to that new object. 

如果一个对象实现(或继承)这个方法,并返回一个非nil(非self)结果,
那么返回的对象将被用作新的接收方对象,消息调度将恢复到这个新对象。

This is useful when you simply want to redirect messages to another object 
and can be an order of magnitude faster than regular forwarding.

当您只想将消息重定向到另一个对象时使用此方法比常规转发快一个数量级。

快速转发简单使用示例:


image.png

同样的对于methodSignatureForSelector也只能通过官方文档查阅,可以知道该方法用于协议的实现。此方法还用于必须创建NSInvocation对象的情况,例如在消息转发期间。如果您的对象维护委托或能够处理它不直接实现的消息,则应该重写此方法以返回适当的方法签名。

This method is used in the implementation of protocols. 
This method is also used in situations where an NSInvocation object must be created, 
such as during message forwarding. 
If your object maintains a delegate or is capable of handling messages 
that it does not directly implement, 
you should override this method to return an appropriate method signature.

慢速转发流程示例:


image.png

如果对消息没有处理最终会调用doesNotRecognizeSelector崩溃


总结

最后总结一下整个IMP查找,动态解析以及消息转发的完整过程,它是由汇编,c/c++共同完成的

  • 首先会进行汇编的快速查找ENTRY _objc_msgSend,在当前类的缓存中查找,若有则直接调用IMP。在runtime中对应的查询路径为objc_class-->cache_t-->buckets-->{sel, imp},查找过程使用了哈希算法
  • 汇编查找不到则会调用c的方法_lookUpImpOrForward
  • _lookUpImpOrForward查找流程为先在自己的方法列表中查找,若找到则返回IMP,并缓存
  • 如果在自己的方法列表中查找不到,判断当前是否为NSObject类,如果是跳出循环;
  • 循环查询父类的缓存,在父类缓存中查询到的IMP若为forward_imp,那么直接调用这个IMP且不做缓存,若不是将它缓存到当前类
  • 在循环中继续查询父类方法列表,直到根类NSObject
  • 循环查询父类方法列表,直到根类NSObject
  • 若最终都没有找到,则开始动态方法解析resolveInstanceMethod,类方法是resolveClassMethod。这个方法返回值只影响log打印。
  • 再次执行 _lookUpImpOrForward,循环查找是否实现了resolveInstanceMethod直到NSObject,最终会将forward_imp赋值给IMP并返回
  • forward_imp进入消息转发流程,这块是由汇编写的,可通过instrumentObjcMessageSends方法开启写入日志查看崩溃前的方法调用顺序日志,然后查看官方文档解释作进一步分析得出,在崩溃前消息转发有快速转发和慢速转发两种。快速转发是forwardingTargetForSelector提供一个对象处理消息。慢速转发是使用forwardInvocation:methodSignatureForSelector:两个方法。
  • 若消息最后没有处理,将会抛出异常doesNotRecognizeSelector

PS:本文是对照最新的objc4-818.2源码进行对照,与旧版本方法以及参数有些出入的地方,另外对于汇编不太了解,基本上都是按照推测来写,如有错误请指正。

你可能感兴趣的:(iOS源码分析之IMP查找及消息转发)