3

NSObject方法调用过程详细分析

 3 years ago
source link: https://juejin.cn/post/6844904000450478087
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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

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

消息发送、转发流程图

1

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

复制代码

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK