iOS 底层原理探索 之 isa - 类的底层原理结构(中)

1,595 阅读7分钟
写在前面: iOS底层原理探究是本人在平时的开发和学习中不断积累的一段进阶之
路的。 记录我的不断探索之旅,希望能有帮助到各位读者朋友。

目录如下:

  1. iOS 底层原理探索 之 alloc
  2. iOS 底层原理探索 之 结构体内存对齐
  3. iOS 底层原理探索 之 对象的本质 & isa的底层实现
  4. iOS 底层原理探索 之 isa - 类的底层原理结构(上)

以上内容的总结专栏


细枝末节整理


写在前面

在之前的 iOS 底层原理探索 之 isa - 类的底层原理结构(上) 文章中,我们分析了isa的走位,以及类的继承的关系, 并且 结合isa和类的继承综合做了分析;再接着我们探索来类的底层结构,并且重点分析了class_data_bits_t bits,和其中的class_rw_t* data以及其内部的propertiesmethodsprotocolsro - ivars 并且验证了我们对于类方法存储在元类中的猜想是正确的。今天,我们继续探索类的底层原理结构之类的 cache

cache 缓存的意思,那么,要么缓存方法,要么缓存属性。我们根据探索bits的节奏,也是通过内存平移来一步一步探索cache。 通过上一篇的总结,我们知道,偏移到bits是32字节,偏移到cache是16字节。

准备

IMPSEL 的关系

  • SEL : 方法编号 (相当于书本中目录的名称)
  • IMP : 函数指针地址 (相当于书本中目录的页码)

首先,我们明白要找到书本中的什么内容 (SEL 目录里面的名称);然后,通过名称找对应的页码 (IMP);最后,通过页码去定位具体的内容。

cache_t 是什么数据结构

首先我们看下 cache_t的内存结构

image.png

从内存结构中并不能直接看到cache_t中的那个属性是我们想要的内容,那么,根据我们对于cache的了解, 要缓存方法或者属性,那么,一定会提供一些关于增删改查的方法,所以我们去看下cache_t所提供的方法,知道我们看到了有一个insert方法 image.png 不要犹豫,果断点进去看下实现

image.png

可以看到 insert 的过程中 对一个 bucket_t 的结构体做了操作。 接下来,我们就具体看下 bucket_t 的内容

image.png

果不其然,在bucket_t这里看到 selimp

到这一步,我们简答总结下 cache_t的数据结构:

cache_t.001.jpeg

LLDB 验证 方法的存储

接下来,我们要做一下验证,来查看下里面的内容:

image.png image.png 那么,我们可以让 SMPerson 的实例 p 执行一下实例方法 image.png 接着,再获取一遍cache_tbuckets(),我们发现还是没有内容 image.png 那么,同样的方法,我们再去buckets_t 里面查找看看相关的方法, 果然,找到了 熟悉的 sel()imp() image.png 那么,继续LLDB image.png

cache_t 流程分析

首先cache_t 是一个结构体, _bucketsAndMaybeMask 是一个 uintptr_t 也就是 unsigned long, 其内部存储的是两部分数据正如其名字一样,是 bucketsmaybeMask

作为缓存,首先需要有写接着可以读。 所以 我们顺着思路找到cache_t

insert()

void insert(SEL sel, IMP imp, id receiver);

通过方法也可以明白,插入的是sel和imp到receiver中。

image.png

INIT_CACHE_SIZE 为 4

image.png

_maybeMask.store

image.png 类中的cache在实例调用方法的时候,就开始进行存储了,首先新创建一个容器 bucket,其中的imp不是直接存储,而是将8字节的地址指针存储器中。否则,会占用很大的内存空间。在需要读取类信息的时候,就顺着这个指针地址去找寻实现。这样,不至于每次读取类信息会很慢。

image.png

所以, 此时 occupied = 1, maybeMask = 3; image.png

buckets是一个数组,插进来的数据存储到哪里,此时,并不知道,在这里会进行cache_hash算法,这样可以得到一个哈希地址。 image.png

cache_hash

image.png

接下来就开始寻找,在sel()不存在的时候,先对occupied++, 然后在 bucket_t-b 合适的位置中,将 sel imp cls() 插入进去。 如果 sel() 已经存在,那么就不在缓存。

image.png

incrementOccupied()

image.png

之后继续存储的时候,会判断是否超出了75%的容量,没有,正常存储;超出,则 判断 capacity存在后, 进行 2倍的 扩容操作(原来的 4 * 2 = 8, m = 8 - 1 = 7)。扩容后,会将,之前存储的内容一并清掉,只会有一个新存储的方法的sel imp。<重新开辟一个新的buckets, 将旧的bucket和capacity 内存地址 清空回收>

image.png

cache_fill_ratio

image.png

collect_free

image.png

为什么不直接追加,而是扩容后清空呢?

原来我们已经开辟的内存是无法改的,开辟新的内存空间后是一个新的地址,原来存储的值并不拿过来,是因为内存在数组平移的时候是十分耗费性能的,并且,苹果在底层的一个原则是越新越好。 在扩容时,之前调用的方法,再一次被使用的几率时很低的,然而,扩容之时的调用的那个方法,是很重要的,所以,系统会见只将扩容调用的那次做存储。

当然,我们可以作为探索,在扩容之后,再次调用之前的方法,发现,在cache中是会继续添加进来的;不过此时,是无序的因为上面的cache_hash算法。

最终 cache_t的流程

未命名.001.jpeg

细节

_bucketsAndMaybeMask

image.png

bucket() 取值过程中,通过 _bucketsAndMaybeMask 的指针平移才可以获取其他的buckets();

bucket()

image.png

拿到 _bucketsAndMaybeMask 的地址,然后做 操作强转成 bucket_t *

bucketsMask 在不同架构下,值是不同的 image.png

CACHE_MASK_STORAGE image.png

补充

架构和环境变量相关

image.png

bucket_t 获取IMP

    inline IMP imp(UNUSED_WITHOUT_PTRAUTH bucket_t *base, Class cls) const {
        uintptr_t imp = _imp.load(memory_order_relaxed);
        if (!imp) return nil;
#if CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_PTRAUTH
        SEL sel = _sel.load(memory_order_relaxed);
        return (IMP)
            ptrauth_auth_and_resign((const void *)imp,
                                    ptrauth_key_process_dependent_code,
                                    modifierForSEL(base, sel, cls),
                                    ptrauth_key_function_pointer, 0);
#elif CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_ISA_XOR
        return (IMP)(imp ^ (uintptr_t)cls);
#elif CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_NONE
        return (IMP)imp;
#else
#error Unknown method cache IMP encoding.
#endif
    }

这里传入了cls ,为什么 要 (IMP)(imp ^ (uintptr_t)cls); 有一个 ^ (异或) 的操作呢?意义是什么呢?

的IMP获取和存储是一个编码和解码的过程,在存储的时候,存储的是一个数值(无符号的长整形 存储到bucket_t 中)见表1,而不是一个纯粹 的指针地址,这也就意味着需要进行局部的操作,也就是编码在 _imp.load() 拿到存储到 imp 然后和 cls 进行按位异或。

  • why? 为什么和 cls 按位异或 ?
  1. 因为在imp存储到时候,进行了编码操作,就是按位异或了 cls( 详见 表2 表3)。
  2. imp 和 sel 都是属于 cls 所以加密操作中盐值就是cls。

加密操作

  • 一个数A异或另一个数B得到结果C; ( c = a ^ b )
  • 结果C 异或了 数B 就会得到 数A; ( a = c ^ b )
  • 这是一个算法规律。

验证

先插入我们 say666 到if代码到断点调试:

image.png

mian函数中调用 say666 方法:

image.png

image.png

  • 验证下加密算法

image.png

表1

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

...

表2


template<Atomicity atomicity, IMPEncoding impEncoding>
void bucket_t::set(bucket_t *base, SEL newSel, IMP newImp, Class cls)
{
    ASSERT(_sel.load(memory_order_relaxed) == 0 ||
           _sel.load(memory_order_relaxed) == newSel);

    // objc_msgSend uses sel and imp with no locks.
    // It is safe for objc_msgSend to see new imp but NULL sel
    // (It will get a cache miss but not dispatch to the wrong place.)
    // It is unsafe for objc_msgSend to see old imp and new sel.
    // Therefore we write new imp, wait a lot, then write new sel.
    
    uintptr_t newIMP = (impEncoding == Encoded
                        ? encodeImp(base, newImp, newSel, cls)
                        : (uintptr_t)newImp);

    if (atomicity == Atomic) {
        _imp.store(newIMP, memory_order_relaxed);
        
        if (_sel.load(memory_order_relaxed) != newSel) {
#ifdef __arm__
            mega_barrier();
            _sel.store(newSel, memory_order_relaxed);
#elif __x86_64__ || __i386__
            _sel.store(newSel, memory_order_release);
#else
#error Don't know how to do bucket_t::set on this architecture.
#endif
        }
    } else {
        _imp.store(newIMP, memory_order_relaxed);
        _sel.store(newSel, memory_order_relaxed);
    }
}

表3

    // Sign newImp, with &_imp, newSel, and cls as modifiers.
    uintptr_t encodeImp(UNUSED_WITHOUT_PTRAUTH bucket_t *base, IMP newImp, UNUSED_WITHOUT_PTRAUTH SEL newSel, Class cls) const {
        if (!newImp) return 0;
#if CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_PTRAUTH
        return (uintptr_t)
            ptrauth_auth_and_resign(newImp,
                                    ptrauth_key_function_pointer, 0,
                                    ptrauth_key_process_dependent_code,
                                    modifierForSEL(base, newSel, cls));
#elif CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_ISA_XOR
        return (uintptr_t)newImp ^ (uintptr_t)cls;
#elif CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_NONE
        return (uintptr_t)newImp;
#else
#error Unknown method cache IMP encoding.
#endif
    }