NSObject方法调用过程详细分析

2,973 阅读11分钟

分析OC方法调用过程的博客多如牛毛,为什么我还来炒剩饭,原因:

  1. 我自己虽然之前也分析过方法调用,但是没有成体系做过笔记,这次相当于自己做一个笔记,便于以后查看。
  2. 网上有详细分析,但是都是基于x86汇编分析的(因为runtime开源的代码可以在macOS上运行起来,更方便分析吧),我只对arm64汇编熟悉,我想应该也有部分同学跟我一样,所以我基于arm64汇编分析一波~
  3. 我这个是基于最新的runtime源码版本(版本号objc4-756.2,苹果官网的源码),网上分析的大多都是几年前的版本,虽然说整个逻辑基本一致,但是还是有些许不同。

消息发送、转发流程图

objc_msgSend

声明原型

以前是:

id _Nullable objc_msgSend(id _Nullable self, SEL _Nonnull op, ...);

Xcode11改成了:

void objc_msgSend(void);

修改原型是为了解决:接收方(被调用者)会从调用方传递参数的相同位置和格式中检索参数。也就是说,被调用者一定知道调用者把参数放在什么寄存器/内存,这样就不会取错参数。避免了调用者把参数放在a寄存器,但是被调用者去b寄存器取参数的错误行为。

例如:

- (void)log: (float)x {
    printf("%f\n", x);
}

因为以前是不定参数,所以objc_msgSend(obj, @selector(log:), (float)M_PI);不会报错,但是 在intel ABI上面,会出错(函数里取得的浮点数是错误的浮点数)。(因为intel ABI中,float跟double在不同的寄存器里,传一个double,但是函数参数是float,函数从float取值)。这个就是调用者把参数放在a寄存器,被调用者去b寄存器取参数。

如何继续使用objc_msgSend

显然,苹果不建议我们直接使用objc_msgSend,但是我们依然想使用,可以用下面两种方法:

  1. 强制转换:
((void (*)(id, SEL, float))objc_msgSend)(obj, @selector(log:), M_PI);

会强制将double转换成float,然后放入float对应的寄存器,被调用者也是去float对应的寄存器取参数。

  1. 声明函数指针来调用:
void (*PILog)(id, SEL, float) = (void (*)(id, SEL, float))objc_msgSend;
PILog(obj, @selector(log:), M_PI);

虽然上面两种方法都是强制转换objc_msgSend,让我们可以直接使用objc_msgSend,但是还是不建议强制转换objc_msgSend。对于某些类型的参数,它在运行时仍可能失败,这就是为什么存在一些变体(为了适配不同cpu架构,比如arm64就不用为返回值是结构体,而专门有objc_msgSend_stret,但是其它cpu架构需要有),例如objc_msgSend_stret,objc_msgSend_fpret,objc_msgSend_fp2ret…… 只要使用基本类型,就应该没问题,但是当开始使用结构体时,或使用long double和复杂类型,就得注意了。

如果我们使用[obj log:M_PI]来调用,不过什么平台的ABI,都不会出错,Xcode都会帮我们准确的翻译好的。所以没有特殊需要,不要直接使用objc_msgSend。

消息发送

arm64源码分析

arm64汇编做3件事:

1. GetIsa

struct objc_object {
private:
    isa_t isa;
    ... 
}

struct objc_class : objc_object {
    // isa_t isa;
    Class superclass;
    cache_t cache;             
    class_data_bits_t bits; 
    ...
}

union isa_t {
    Class cls;
    uintptr_t bits;
    struct {
      uintptr_t nonpointer        : 1;                                       \
      uintptr_t has_assoc         : 1;                                       \
      uintptr_t has_cxx_dtor      : 1;                                       \
      uintptr_t shiftcls (4-36bits,共33bits,存放类地址): 33; /*MACH_VM_MAX_ADDRESS 0x1000000000*/ \
      uintptr_t magic             : 6;                                       \
      uintptr_t weakly_referenced : 1;                                       \
      uintptr_t deallocating      : 1;                                       \
      uintptr_t has_sidetable_rc  : 1;                                       \
      uintptr_t extra_rc          : 19
    };
    ...
};

因为对象的方法存放在对象对应的类里,所以需要获得类的地址。类的地址存放在isa的4-36 bits上,所以需要先获得isa;而对象(地址)放在X0上,对象就是objc_object结构体,所以X0里的地址就是objc_object结构体地址,结构体第一个就是isa,那么X0地址就可以看做是isa地址。所以X0&ISA_MASK(0x0000000ffffffff8ULL)就是类地址(因为类的指针要按照字节(8 bits)内存对齐,其指针后三位必定是0,33(33个1与运算)+3(填充3个0),一共36位表示)。

当X0小于0时候,说明X0是tagged pointer,通过类索引来获取类地址。

CacheLookup

//缓存的数据结构
typedef uint32_t mask_t;
struct cache_t {
    struct bucket_t *_buckets; //哈希表地址
    mask_t _mask;  //哈希表的大小,值为2^n-1
    mask_t _occupied; //哈希表中元素个数
}

typedef uintptr_t cache_key_t;
struct bucket_t {
    cache_key_t _key; //SEL
    IMP _imp;  //函数指针
}

先讨论一个数学问题: a%b=a-(a/b)*b,这个很明显吧;那么当b=2^n - 1,比如b=3、7、15等等,a%b=a&b。比如13%3=1,13&3也是等于1。(注意15%3 = 0,但是15&3 = 3。但是对这个求hash无影响,可以不考虑)

讲人话,就是当b=2^n - 1,可以用与运算(a&b)来替代模运算(a%b),但是避免了模操作的昂贵开销。汇编里,用sel&mask来代替sel%mask。

sel%mask(哈希表大小)+buckets,结果就是sel函数在缓存里的地址;如果cache里为0,说明没有缓存,调用__objc_msgSend_uncached;如果发生哈希冲突,那么从后往前遍历,如果SEL跟X1匹配上了,则缓存命中;如果遍历到bucket_t的SEL为0,则调用__objc_msgSend_uncached。 X12第一次遍历到buckets(哈希表表头)时,将X12置为哈希表尾,重新从后往前遍历。整个遍历过程如果遇到SEL为0,则调用__objc_msgSend_uncached,X12第二次遍历到buckets时,也调用__objc_msgSend_uncached,遍历过程如果缓存命中,则调用imp,直接ret。

__objc_msgSend_uncached

__objc_msgSend_uncached就是调用前保存X0-X8/q0-q7寄存器,然后调用__class_lookupMethodAndLoadCache3函数,返回函数imp放在x17,恢复寄存器,然后调用imp。

C/C++源码分析

_class_lookupMethodAndLoadCache3

IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
    return lookUpImpOrForward(cls, sel, obj, 
                              YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}

_class_lookupMethodAndLoadCache3就是调用了lookUpImpOrForward函数而已。

lookUpImpOrForward

lookUpImpOrForward函数主要干:1.类没有注册,就注册类;2.类没有初始化,就初始化类;3.分别从缓存(cache_getImp)和类方法列表(getMethodNoSuper_nolock)里遍历,寻找sel函数;4.循环从父类的缓存和方法列表遍历,直到父类为nil;5.如果还没有找到,则进行方法解析(resolveMethod);6.如果最后依然没有找到方法,就把imp赋值为_objc_msgForward_impcache,返回imp。下面详细分析这几个过程:

注册类

//注册类
if (!cls->isRealized()) {
    cls = realizeClassMaybeSwiftAndLeaveLocked(cls, runtimeLock);
    // runtimeLock may have been dropped but is now locked again
}

一般情况下,类在App启动加载macho文件时候,就已经注册了。但是也有特例,比如weaklink的类,可能运行到这里,还没有初始化。为什么有weaklink,就是App最低版本支持iOS9,但是却使用了iOS11 SDK的新功能,如果没有weaklink,程序里肯定是不能使用新版本的功能的。更详细介绍,请见官网

注册类(realizeClassWithoutSwift) 这个过程会申请class_rw_t空间,递归realize父类跟元类,然后设置类的父类跟元类;添加类的方法、属性、协议;添加分类的方法、属性、协议。返回这个类的结构体

初始化类

if (initialize && !cls->isInitialized()) {
    cls = initializeAndLeaveLocked(cls, inst, runtimeLock);
    // runtimeLock may have been dropped but is now locked again

    // If sel == initialize, class_initialize will send +initialize and 
    // then the messenger will send +initialize again after this 
    // procedure finishes. Of course, if this is not being called 
    // from the messenger then it won't happen. 2778172
}

如果类没有初始化,先递归初始化父类,然后给这个类发送objc_msgSend(cls, SEL_initialize)方法。所以initialize不需要显示调用父类,并且子类没有实现initialize,会调用父类的initialize方法(这个方法没啥特别的,也是通过objc_msgSend来调用的)。

cache_getImp

retry:    
runtimeLock.assertLocked();

// Try this class's cache.
imp = cache_getImp(cls, sel);
if (imp) goto done;

因为多线程,此时cache可能改变了,所以需要重新来次CacheLookup。

getMethodNoSuper_nolock

// Try this class's method lists.
{
    Method meth = getMethodNoSuper_nolock(cls, sel);
    if (meth) {
        log_and_fill_cache(cls, meth->imp, sel, inst, cls);
        imp = meth->imp;
        goto done;
    }
}

getMethodNoSuper_nolock内部会调用search_method_list函数,search_method_list函数就是遍历类的方法列表,只不过当方法列表是排序的,就二分法查找,否则就是依次遍历。

循环遍历父类的cache_getImp跟getMethodNoSuper_nolock

 // Try superclass caches and method lists.
{
    unsigned attempts = unreasonableClassCount();
    从上图可以看出,不管是类还是元类,都是一直遍历到RootClass(NSObject)。
    整个过程,不过是cache中,还是methodlist中找到sel的imp,都调用log_and_fill_cache,将sel和imp放入cache中
    for (Class curClass = cls->superclass;
         curClass != nil;
         curClass = curClass->superclass)
    {
        // Halt if there is a cycle in the superclass chain.
        if (--attempts == 0) {
            _objc_fatal("Memory corruption in class list.");
        }
        
        // 父类cache寻找
        imp = cache_getImp(curClass, sel);
        if (imp) {
            if (imp != (IMP)_objc_msgForward_impcache) {
                // Found the method in a superclass. Cache it in this class.
                log_and_fill_cache(cls, imp, sel, inst, curClass);
                goto done;
            }
            else {
                // 如果imp为_objc_msgForward_impcache,说明这个sel之前寻找过,没有找到。所以退出循环
                // Found a forward:: entry in a superclass.
                // Stop searching, but don't cache yet; call method 
                // resolver for this class first.
                break;
            }
        }
        
        // Superclass method list.
        Method meth = getMethodNoSuper_nolock(curClass, sel);
        if (meth) {
            log_and_fill_cache(cls, meth->imp, sel, inst, curClass);
            imp = meth->imp;
            goto done;
        }
    }
}

resolveMethod(方法解析)

// 如果上面都没有找到sel的imp,就不会执行goto done;进而走到这里来,这里会调用方法解析,方法解析后,然后goto retry,
又回到上面的cache_getImp--> getMethodNoSuper_nolock -->
循环遍历父类的cache_getImp跟getMethodNoSuper_nolock -->
再次到此处,但是再次到此处时候,不会进入if里面了,因为triedResolver已经设置为YES了。
if (resolver  &&  !triedResolver) {
    runtimeLock.unlock();
    resolveMethod(cls, sel, inst);
    runtimeLock.lock();
    // Don't cache the result; we don't hold the lock so it may have 
    // changed already. Re-do the search from scratch instead.
    triedResolver = YES;
    goto retry;
}


static void resolveMethod(Class cls, SEL sel, id inst)
{
    runtimeLock.assertUnlocked();
    assert(cls->isRealized());
    //如果类不是元类,调用resolveInstanceMethod,
    //resolveInstanceMethod函数会调用objc_msgSend(cls, SEL_resolveInstanceMethod, sel);
    if (! cls->isMetaClass()) {
        // try [cls resolveInstanceMethod:sel]
        resolveInstanceMethod(cls, sel, inst);
    } 
    else {
        //如果类是元类,就调用resolveClassMethod
        //resolveClassMethod函数会调用objc_msgSend(nonmeta, SEL_resolveClassMethod, sel);
        // try [nonMetaClass resolveClassMethod:sel]
        // and [cls resolveInstanceMethod:sel]
        resolveClassMethod(cls, sel, inst);
        if (!lookUpImpOrNil(cls, sel, inst, 
                            NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
        {
            resolveInstanceMethod(cls, sel, inst);
        }
    }
}

//只给出resolveInstanceMethod函数,resolveClassMethod类似。
static void resolveInstanceMethod(Class cls, SEL sel, id inst)
{
    runtimeLock.assertUnlocked();
    assert(cls->isRealized());

    if (! lookUpImpOrNil(cls->ISA(), SEL_resolveInstanceMethod, cls, 
                         NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
    {
        // Resolver not implemented.
        return;
    }

    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    bool resolved = msg(cls, SEL_resolveInstanceMethod, sel);
    //从这里可以看出执行完SEL_resolveInstanceMethod,返回的bool值,跟会不会进行消息转发无关,仅仅跟打印系统日志有关。
    // Cache the result (good or bad) so the resolver doesn't fire next time.
    // +resolveInstanceMethod adds to self a.k.a. cls
    IMP imp = lookUpImpOrNil(cls, sel, inst, 
                             NO/*initialize*/, YES/*cache*/, NO/*resolver*/);

    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函数跟resolveClassMethod函数,一般用来add method,他们返回的bool值,跟是否会进入消息转发无关,网上文章绝大部分都说返回YES就表示消息解析已经处理了这个消息,不会进行消息转发,而返回NO,就进入消息转发。其实是错误的,读者可以自己写demo验证。

根据上面的流程图,我们可以清楚知道,消息解析后,会重新进行类cache_getImp--> 类getMethodNoSuper_nolock --> 循环遍历父类的cache_getImp跟getMethodNoSuper_nolock,如果找到了,填充cache,然后到done,ret。如果没有找到,imp赋值为_objc_msgForward_impcache,而执行_objc_msgForward_impcache才会进入消息转发,跟resolveInstanceMethod返回的bool值确实没有关系。

_objc_msgForward_impcache

调用_objc_msgForward_impcache:(接口宏,定义在arm64里) 在arm64汇编里,最后调用了_objc_forward_handler函数。 _objc_msgForward-->_objc_forward_handler。

// Default forward handler halts the process.
__attribute__((noreturn)) 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;

#if SUPPORT_STRET
struct stret { int i[100]; };
__attribute__((noreturn)) struct stret 
objc_defaultForwardStretHandler(id self, SEL sel)
{
    objc_defaultForwardHandler(self, sel);
}
void *_objc_forward_stret_handler = (void*)objc_defaultForwardStretHandler;
#endif

void objc_setForwardHandler(void *fwd, void *fwd_stret)
{
    _objc_forward_handler = fwd;
#if SUPPORT_STRET
    _objc_forward_stret_handler = fwd_stret;
#endif
}

// Define SUPPORT_STRET on architectures that need separate struct-return ABI.
#if defined(__arm64__)
#   define SUPPORT_STRET 0
#else
#   define SUPPORT_STRET 1
#endif

因为arm64中(不用为返回值是结构体,而需要支持objc_msgSend_stret(这也是为啥其它文章里面有许多objc_msgSend变体,而本文没有)等。),SUPPORT_STRET为0。
上面代码在arm64中,可以简洁为:

// Default forward handler halts the process.
__attribute__((noreturn)) 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;

void objc_setForwardHandler(void *fwd, void *fwd_stret)
{
    _objc_forward_handler = fwd;
}

可以看到_objc_forward_handler的默认实现是objc_defaultForwardHandler(打印系统日志,杀掉进程),
但是App在启动时候,会调用objc_setForwardHandler,重新给_objc_forward_handler赋值新的函数指针。赋值成什么函数呢?在Core Foundation
中。

消息转发阶段

Core Foundation 里面没找到objc_setForwardHandler的调用,但是打符号断点,发现App启动时候,通过_CFInitialize调用了objc_setForwardHandler函数,说明_objc_forward_handler被重新赋值了。

通过消息转发调用堆栈,发现_objc_forward_handler被替换成了_CF_forwarding_prep_0函数,_CF_forwarding_prep_0调用___forwarding___函数。

forwarding 函数(打符号断点看到有336行汇编) 大概做了:

  1. 如果类实现了forwardingTargetForSelector,调用,返回对象target跟self不同,重新调用objc_msgSend(target,sel...) 然后ret。
  2. 如果实现了methodSignatureForSelector,调用,返回sig,则调用forwardInvocation,然后返回结果;否则调用doesNotRecognizeSelector

// Replaced by CF (throws an NSException)这里说了,
也是被Core Foundation替换,其实也是打日志,抛异常。
+ (void)doesNotRecognizeSelector:(SEL)sel {
    _objc_fatal("+[%s %s]: unrecognized selector sent to instance %p", 
                class_getName(self), sel_getName(sel), self);
}

// Replaced by CF (throws an NSException)
- (void)doesNotRecognizeSelector:(SEL)sel {
    _objc_fatal("-[%s %s]: unrecognized selector sent to instance %p", 
                object_getClassName(self), sel_getName(sel), self);
}

参考

  1. www.mikeash.com/pyblog/objc…
  2. yulingtianxia.com/blog/2016/0…