OC底层原理探索之汇编objc_msgSend原理分析

191 阅读4分钟

arm64 cache_t inset的do while算法

#elif __arm64__
static inline mask_t cache_next(mask_t i, mask_t mask) {
    return i ? i-1 : mask;
}

向前存储,一直找到0号位置,如果找到了0号位置就从mask从新开始。如果一直找不到就退出insert循环,bad_cache(receiver, (SEL)sel)

struct bucket_t {
private:
    // IMP-first is better for arm64e ptrauth and no worse for arm64.
    // SEL-first is better for armv7* and i386 and x86_64.
#if __arm64__
    explicit_atomic<uintptr_t> _imp;
    explicit_atomic<SEL> _sel;
#else
    explicit_atomic<SEL> _sel;
    explicit_atomic<uintptr_t> _imp;
#endif

arm64情况下,bucket_t里面先存储imp再存储sel

arm64汇编

在源码里面找到arm64机型下对于ENTRY objc_msgSend入口 image.png 1.cmp p0, #0:p0消息的接收者,判断消息的接收者是否存在,如果不存在,是否支持tagged pointer,不支持就ReturnZero 2.ldr p13, [x0] // p13 = isa: isa放到寄存器x0上 3..macro GetClassFromIsa_p16 src, needs_auth, auth_address (isa 1 x0) 4.条件选择之后到了 image.png 0=p16(空地址)0 = p16(空地址),1 = p13(isa) ,$1 & #ISA_MASK 放到p16地址里,此时得到了p16 = class image.png 此时回到首页578行,继续往下,到了581行 image.png 全局搜索,定位到了.macro CacheLookup Mode, Function, MissLabelDynamic, MissLabelConstant这里 mov x15, x16把p16移动到了x15位置,由于arm是高16位,所以到了下面的这个条件。 image.png 把x16平移CACHE,那么CACHE是多少呢?全局搜索一下,定位到arm64.s文件中

#define CACHE            (2 * __SIZEOF_POINTER__)

2个指针的大小,意思就是这里x16平移16位,就得到了cache_t放到了p11的位置。接着到了这里,CONFIG_USE_PREOPT_CACHES=1,然后到了366行。 image.png 366行出现了一个#0x0000fffffffffffe,也是就是16进制0号位=0,1-47号位都是1,p11=cache_t & #0x0000fffffffffffe=p10=buckets,367行判断p11的0号位置是否为空,不为空就跳转到LLookupPreopt,正常的逻辑是不为0,LLookupPreopt查找共享缓存。继续在LLookupStart往下看 image.png 369行p1 sel >> 7 == value ^= value >> 7 ,前48位是bucket后16位是mask, >> 48位得到了 mask 370行p11=_cmd,得到p12 = 哈希index image.png PTRSHIFT=3,(_cmd & mask) << 4左移4位的原因是内存平移,buckets + ((_cmd & mask) << 4)得到的是b[i],所以p13=当前查找的bucket image.png *bucket--存储到了p9和p17的位置,也就是当前的p9=查找的sel,p17=查找的imp(arm结构下imp在前),如果一直就走缓存命中 image.png 不等于的话走到3,当前sel=0,miss,比较当前的bucket和buckets循环查找所有的bucket知道缓存命中为止 image.png 在cache_t的insert中我们知道是向前查找,假如当前是在2号位,一直找到0号位都没有找到,此时把mask重新定位到最后一位,从最后以位继续向前查找,下面p13 = buckets+7<<4(16)也就是说p13定位到了最后一个位置,继续向前查找image.png

__objc_msgSend_uncached

上面411行,MissLabelDynamic就是函数没有命中走的方法也就是__objc_msgSend_uncached,全局搜索定位到这个函数,可以看到里面就几行代码 image.png 总共三行戴拿,第一行和最后一行意义不大,所以我们定位到了MethodTableLookup我们推断,imp应该在这里拿到,不然没有办法去找 image.png 也就定位到了_lookUpImpOrForward,来到了这里也就从汇编跳出到了C/C++

lookUpImpOrForward

从上面我们知道缓存方法查找的也就是快速查找的过程是用汇编语言来编写的,慢速查找是用c/c++来写的,慢速查找也就是不断的遍历MethodList的过程,在lookUpImpOrForward方法里直接定位到imp赋值这里

    for (unsigned attempts = unreasonableClassCount();;) {
        if (curClass->cache.isConstantOptimizedCache(/* strict */true)) {
				...
        } else {
            // curClass method list.
            Method meth = getMethodNoSuper_nolock(curClass, sel);
            if (meth) {
                imp = meth->imp(false);
                goto done;
            }

            if (slowpath((curClass = curClass->getSuperclass()) == nil)) {
                // No implementation found, and method resolver didn't help.
                // Use forwarding.
                imp = forward_imp;
                break;
            }
        }

getMethodNoSuper_nolock方法里我们发现了寻找方法的过程是使用的二分查找

ALWAYS_INLINE static method_t *
findMethodInSortedMethodList(SEL key, const method_list_t *list, const getNameFunc &getName)
{
   	// ...
    // 这个算法就是二分查找
    for (count = list->count; count != 0; count >>= 1) {
        probe = base + (count >> 1);
        
        uintptr_t probeValue = (uintptr_t)getName(probe);
        
        if (keyValue == probeValue) {
            // 看注释这里就是优先使用分类的方法
            while (probe > first && keyValue == (uintptr_t)getName((probe - 1))) {
                probe--;
            }
            return &*probe;
        }
        
        if (keyValue > probeValue) {
            base = probe + 1;
            count--;
        }
    }
    
    return nil;
}

慢速查找到了之后再insert到了cache中

objc_msgSend 过程总结

objc_msgSend(receiver, _cmd)也就是通过sel找到imp的过程 1.判断reveiver是否存在 2.receiver-> isa -> class 3.class->内存平移->cache_t(bucket mask) 4.bucket掩码->bucket 5.mask掩码 -> mask 6.4和5位inset哈希函数做准备

static inline mask_t cache_hash(SEL sel, mask_t mask) 
{
    uintptr_t value = (uintptr_t)sel;
#if CONFIG_USE_PREOPT_CACHES
    value ^= value >> 7;
#endif
    return (mask_t)(value & mask);
}

7.获取第一次查找的Index 8.bucket + index定位到在缓存里要查找的bucket 9.通过定位的bucket找到里面的「imp sel」 10.sel == _cmd? -> cacheHit -> imp ^ isa = call imp 11.不相等 *bucket -- 循环查找 12.一直找不到 就到了__objc_msgSend_uncached

补充

cache_t打印里面的-maybeMask = 7 ,_occupied = 1,为什么等于7?我们知道方法插入缓存必要走的一个方法是voidcache_t::insert(SEL sel, IMP imp, id receiver),在源码里加入打印看下一共插入了几个方法

void cache_t::insert(SEL sel, IMP imp, id receiver)
{
    printf("== %s ==%p == %p\n",(char *)sel, imp, receiver);
}

image.png 打印出来我们发现只有前面三个方法跟Person类有关,所以在调用say之前必须要调用两个方法。我们查找源码发现bucket初始化的时候有两个默认的方法[NSObject class] respondsToSelector

bucket_t *cache_t::allocateBuckets(mask_t newCapacity)
{
    // 分配一个额外的桶来标记列表的结尾
    bucket_t *newBuckets = (bucket_t *)calloc(bytesForCapacity(newCapacity), 1);

    bucket_t *end = endMarker(newBuckets, newCapacity);

#if __arm__
    // End marker's sel is 1 and imp points BEFORE the first bucket.
    // This saves an instruction in objc_msgSend.
    end->set<NotAtomic, Raw>(newBuckets, (SEL)(uintptr_t)1, (IMP)(newBuckets - 1), nil);
#else
    // 结束标记的sel为1,imp指向第一个桶
    end->set<NotAtomic, Raw>(newBuckets, (SEL)(uintptr_t)1, (IMP)newBuckets, nil);
#endif
    
    if (PrintCaches) recordNewCache(newCapacity);

    return newBuckets;
}

结束标记的sel为1,imp指向第一个桶,也是改bucket的边界。所以这里也就解释了在saySomething之前已经占用了3个内存,然后就扩容,也刚好对应了mask = capacity - 1 class->allocBucket->bucket.set() ​