iOS:SideTable

作者: 康小曹 | 来源:发表于2021-06-23 09:57 被阅读0次

    本文源码来自于 objc4-756.2 版本;

    一、SideTable

    本文研究 sideTable 在 objc4 源码中的使用及其作用,从而解析 iOS 中引用计数器和弱引用的实现原理;

    1. retain 操作

    我们都知道,新版本的 objc 中引入了 Tagged Pointer,且 isa 采用 union 的方式进行构造,其中 isa 的结构体中有一个 extra_rchas_sidetable_rc,这两者共同记录引用计数器。

    直接看看 objc_object::rootRetain() 方法,只看 extra_rc 超出之后 sidetable 相关的代码,删减之后如下:

    
    uintptr_t carry;
        newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry);  // extra_rc++
    
    if (carry) {
        // Leave half of the retain counts inline and prepare to copy the other half to the side table.
        transcribeToSideTable = true;
        newisa.extra_rc = RC_HALF;
        newisa.has_sidetable_rc = true;
    }
    if (slowpath(transcribeToSideTable)) {
            // Copy the other half of the retain counts to the side table.
            sidetable_addExtraRC_nolock(RC_HALF);
    }
    

    那么关键方法就是 sidetable_addExtraRC_nolock()

    bool 
    objc_object::sidetable_addExtraRC_nolock(size_t delta_rc)
    {
        assert(isa.nonpointer);
        // 取出this对象所在的SideTable
        SideTable& table = SideTables()[this];
        // 取出SideTable中存储的refcnts,类型为Map
        size_t& refcntStorage = table.refcnts[this];
        // 记录原始的引用计数器
        size_t oldRefcnt = refcntStorage;
    
        // 容错处理
        assert((oldRefcnt & SIDE_TABLE_DEALLOCATING) == 0);
        assert((oldRefcnt & SIDE_TABLE_WEAKLY_REFERENCED) == 0);
        if (oldRefcnt & SIDE_TABLE_RC_PINNED) return true;
    
        uintptr_t carry;
        size_t newRefcnt = 
            addc(oldRefcnt, delta_rc << SIDE_TABLE_RC_SHIFT, 0, &carry);
        if (carry) {
            // SideTable溢出处理
            refcntStorage =
                SIDE_TABLE_RC_PINNED | (oldRefcnt & SIDE_TABLE_FLAG_MASK);
            return true;
        } else {
            // SideTable未溢出
            refcntStorage = newRefcnt;
            return false;
        }
    }
    

    这个函数的逻辑如下:

    1. 根据 this,也就是对象的地址从 SideTables 中取出一个 SideTable;
    2. 获取 SideTable 的 refcnts,这个成员变量是一个 Map;
    3. 存储旧的引用计数器;
    4. 进行 add 计算,并记录是否有溢出;
    5. 根据是否溢出计算并记录结果,最后返回;

    那么,这里有几个点需要解开:

    1. 什么是 SideTables;
    2. 什么是 SideTable;
    3. 什么是 refcnts;
    4. add 的计算逻辑为什么需要位移?
    5. SideTable 中的溢出时如何处理的?

    接下来,一一解决~~~

    2. SideTables

    直接来看 SideTables 的代码:

    static StripedMap<SideTable>& SideTables() {
        return *reinterpret_cast<StripedMap<SideTable>*>(SideTableBuf);
    }
    

    首先,这是个静态函数,返回 StripedMap<SideTable> 类型,但是 & 是什么意思呢?这个是 C++ 语法,表示返回引用类型,看个例子:

    &的用法

    & 的用法还有些限制,比如不能返回栈中的引用,否则会栈变量消失后会出现 error,还有一些其他的限制,有兴趣可以深究,这里只需要知道 & 表示返回引用类型,也就是可以通过 & func() 来获取函数返回值的指针,其他的不再赘述;

    接着,比较懵逼的是 *reinterpret_cast ,其实这个是 C++ 的强制类型转换语法,不用深究,有兴趣的可以自行百度。

    所以,总结下这段代码:

    1. SideTables() 使用 static 修饰,是一个静态函数;
    2. & 表示返回引用类型;
    3. reinterpret_cast 是一个强制类型转换符号;
    4. 函数最终的结果就是返回 SideTableBuf;

    那么 SideTableBuf 又是什么?

    3. SideTableBuf

    直接看代码:

    // We cannot use a C++ static initializer to initialize SideTables because
    // libc calls us before our C++ initializers run. We also don't want a global 
    // pointer to this struct because of the extra indirection.
    // Do it the hard way.
    
    alignas(StripedMap<SideTable>) static uint8_t 
        SideTableBuf[sizeof(StripedMap<SideTable>)];
    

    首先看注释,说明了两点:

    1. SideTables 在 C++ 的 initializers 函数之前被调用,所以不能使用 C++ 初始化函数来初始化 SideTables,而 SideTables 本质就是 SideTableBuf;
    2. 不能使用全局指针来指向这个结构体,因为涉及到重定向问题;

    其实还是比较懵逼为什么 SideTableBuf 要这么设计,原理有待考究~~~估计和初始化有关;

    继续看 SideTableBuf,要点包括:

    1. alignas 表示对齐;
    2. StripedMap<SideTable> 的 size 为 4096(存疑,待验证);
    3. uint8_t 实际上是 unsigned char 类型,即占 1 个字节;

    由此可以得出:

    • SideTableBuf 本质上是一个长度为 sizeof(StripedMap<SideTable>) 的 char 类型的数组;

    同时也可以这么理解:

    • SideTableBuf 本质上就是一个大小为和 StripedMap<SideTable> 对象一致的内存块;

    这也是为什么 SideTableBuf 可以用来表示 StripedMap<SideTable> 对象。本质上而言,SideTableBuf 就是指一个 StripedMap<SideTable>对象;

    那么接下来就是搞清楚 StripedMap<SideTable> 是个什么东西了......

    4. StripedMap<SideTable>

    先上代码,删减一些方法之后的代码为:

    template<typename T>
    class StripedMap {
    #if TARGET_OS_IPHONE && !TARGET_OS_SIMULATOR
        enum { StripeCount = 8 };
    #else
        enum { StripeCount = 64 };
    #endif
    
        struct PaddedT {
            T value alignas(CacheLineSize);
        };
    
        PaddedT array[StripeCount];
    
        static unsigned int indexForPointer(const void *p) {
            uintptr_t addr = reinterpret_cast<uintptr_t>(p);
            return ((addr >> 4) ^ (addr >> 9)) % StripeCount;
        }
    
     public:
        T& operator[] (const void *p) { 
            return array[indexForPointer(p)].value; 
        }
        const T& operator[] (const void *p) const { 
            return const_cast<StripedMap<T>>(this)[p]; 
        }
        ...省略了对象方法...
    }
    

    上述代码的逻辑为:

    1. 根据是否为 iphone 定义了一个 StripeCount,iphone 下为 8;
    2. 源码中 CacheLineSize 为 64,使用 T 定义了一个结构体,而 T 就是 SideTable 类型;
    3. 生成了一个长度为 8 类型为 SideTable 的数组;
    4. indexForPointer() 逻辑为根据传入的指针,经过一定的算法,计算出一个存储该指针的位置,因为使用了取模运算,所以值的范围是 0 ~ (StripeCount-1),所以不会出现数组越界;
    5. 后面的 operator 表示重写了运算符 [] 的逻辑,调用了 indexForPointer() 方法,这样使用起来更像一个数组;

    至此,SideTables 的含义已经很清楚了:

    • SideTables 可以理解成一个类型为 StripedMap<SideTable> 静态全局对象,内部以数组的形式存储了 StripeCount 个 SideTable;

    那么第一个问题已经解决,按照 sidetable_addExtraRC_nolock() 方法中的逻辑,先从 SideTables 数组中取出一个 SideTable,然后进行相关操作,所以现在就来看看 SideTable 是个啥~~~

    5. SideTable

    struct SideTable {
        spinlock_t slock;
        RefcountMap refcnts;
        weak_table_t weak_table;
    
        SideTable() {
            memset(&weak_table, 0, sizeof(weak_table));
        }
    
        ~SideTable() {
            _objc_fatal("Do not delete SideTable.");
        }
        ...省略对象方法...
    }
    

    可以看到,SideTable 有三个成员变量:

    1. spinlock_t:自旋锁,负责加锁相关逻辑;
    2. refcnts:存储引用计数器的 Map;
    3. weak_table:存储弱引用的表;

    自旋锁暂不讨论,来看看 refcnts 的定义:

    typedef objc::DenseMap<DisguisedPtr<objc_object>,size_t,RefcountMapValuePurgeable> RefcountMap;
    

    DenseMap 就是一个 hash Map,过于复杂,先不看。来看看基类 DenseMapBase 中的部分代码,如下,DenseMapBase中重写了操作符 []:

    ValueT &operator[](const KeyT &Key) {
        return FindAndConstruct(Key).second;
      }
    

    大意是通过传入的 Key 寻找对应的 Value。而 Key 是 DisguisedPtr<objc_object> 类型,Value 是 size_t 类型。即使用 obj.address :refCount 的形式来记录引用计数器;

    回到最初的 sidetable_addExtraRC_nolock 方法中:

    size_t& refcntStorage = table.refcnts[this];
    

    上述代码就是通过 this ,即 object 对象的地址,取出 refcnts 这个哈希表中存储的引用计数器;

    refcnts 可以理解成一个 Map,使用 address:refcount 的形式存储了很多个对象的引用计数器;

    6. 引用计数器原理总结

    SideTables 和 SideTable
    1. iphone 中 SideTables() 本质是返回一个 SideTableBuf 对象,该对象存储 8 个 SideTable;
    2. 因为涉及到多线程和效率的问题,必定不可能只使用一个 SideTable 来存储对象相关的引用计数器和弱引用;
    3. Apple 通过对 object 的地址进行运算之后,对 SideTable 的个数进行取模运算,以此来决定将对象分配到哪个 SideTable 进行信息存储,因为有取模运算,不会出现数组溢出的情况;

    总结:

    • objc 中当对象需要使用到 sideTable 时,会被分配到 8/64 个全局 sideTables 中的某一个表中存储相关的引用计数器或者弱引用信息;

    7. weak_table

    继续看弱引用如何实现的,从上文中可以看出,8/64 个 SideTable 对象中不仅保存了引用计数器相关的 Map,还保存了一个 weak_table,来看看 weak_table_t 源码:

    /**
     * The global weak references table. Stores object ids as keys,
     * and weak_entry_t structs as their values.
     */
    struct weak_table_t {
        weak_entry_t *weak_entries;
        size_t    num_entries;
        uintptr_t mask;
        uintptr_t max_hash_displacement;
    };
    

    注释也说明了 weak_table_t 是一个全局引用表,object 的地址作为 key,weak_entry_t 作为 Value。只不过这个全局引用表有 8 或者 64 个;

    即:

    • weak_table 中以 weak_entry_t 的形式存储对象的弱引用;

    那么具体是怎么存储的呢?这个 weak_entry_t 是什么,又是怎么用的呢,弱引用的存储逻辑是怎样的?

    上述成员变量是 weak_entries,前面又带 *,感觉很像是一个指向类型为 weak_entry_t 的数组,如果是这样,那也正好和注释的描述相符,大胆猜测一下:

    • weak_table_t 中使用数组的形式来存储 weak_entry_t 对象,以此来表示该表中每个对象的弱引用情况;

    接下来就是验证~~~

    8. store_weak流程分析

    要弄清楚 weak_table_t 和 weak_entry_t 的使用,就要从新增弱引用作为突破口,来看看 objc_storeWeak() 方法:

    id
    objc_storeWeak(id *location, id newObj)
    {
        return storeWeak<DoHaveOld, DoHaveNew, DoCrashIfDeallocating>
            (location, (objc_object *)newObj);
    }
    

    storeWeak 的定义如下:

    enum CrashIfDeallocating {
        DontCrashIfDeallocating = false, DoCrashIfDeallocating = true
    };
    
    template <HaveOld haveOld, HaveNew haveNew,
              CrashIfDeallocating crashIfDeallocating>
    static id 
    storeWeak(id *location, objc_object *newObj)
    {
    ...省略...
    }
    

    这里不要被这些 template 吓到,storeWeak 只不过又是一个模板函数,这是 C++ 中的语法,可以暂不深究,有兴趣的可以去学习学习。

    这里只需要知道,haveOldhaveNew 是作为参数来使用的,从上面的storeWeak 调用代码以及后文对这两个参数的使用,也可以看出个大概,不必纠结~~~

    精简 storeWeak() 函数的代码如下:

    SideTable *oldTable;
    SideTable *newTable;
    
    // 根据参数判断是否存在旧表决定使用哪个表进行存储
    if (haveOld) {
        oldObj = *location;
        oldTable = &SideTables()[oldObj];
    } else {
        oldTable = nil;
    }
    if (haveNew) {
        newTable = &SideTables()[newObj];
    } else {
        newTable = nil;
    }
    
    ...省略很多异常场景处理代码...
    
    // 只看 new 的逻辑
    if (haveNew) {
        // 在weak_table中新增弱引用
        // 如果失败则会返回 nil,成功则返回对象本身
        newObj = (objc_object *)weak_register_no_lock(&newTable->weak_table, (id)newObj, location, crashIfDeallocating);
    
        if (newObj  &&  !newObj->isTaggedPointer()) {
            // 成功则设置weakly_referenced为1;
            newObj->setWeaklyReferenced_nolock();
        }
    
        // 赋值
        *location = (id)newObj;
    }
    

    其中,location 是作为入参传递进来的,是被 __weak 修饰的指针本身,而 newObj 就是这个弱指针所指向的对象,伪代码如下:

    __weak NSObject * location = newObj;
    

    现在梳理下 storeWeak() 的逻辑:

    1. 根据 haveOld/haveNew 调用 SideTables() 方法,获取到 8/64 个全局 SideTable 中的某一个;
    2. 调用 weak_register_no_lock()方法将 newObj 添加到 SideTable 的 weak_table 中,如果失败则会返回 nil,成功则返回对象本身;
    3. 调用 setWeaklyReferenced_nolock() 方法,设置 isa 的 weakly_referenced为 1;
    4. 将 location 正式指向 newObj 进行赋值,但是注意此时并没有调用 retain 方法,所以引用计数器不会 + 1;

    那么接下来看看 weak_register_no_lock() 方法,精简后如下:

    id 
    weak_register_no_lock(weak_table_t *weak_table, id referent_id, 
                          id *referrer_id, bool crashIfDeallocating)
    {
        objc_object *referent = (objc_object *)referent_id;
        objc_object **referrer = (objc_object **)referrer_id;
    
        ...省略异常场景处理代码...
    
        // now remember it and where it is being stored
        weak_entry_t *entry;
        if ((entry = weak_entry_for_referent(weak_table, referent))) {
            // 存在 weak_entry_t 对象则直接新增
            append_referrer(entry, referrer);
        }  else {
            // 不存在则证明是第一次被弱引用,新建一个weak_entry_t对象
            weak_entry_t new_entry(referent, referrer);
            weak_grow_maybe(weak_table);
            weak_entry_insert(weak_table, &new_entry);
        }
        return referent_id;
    }
    

    精简之后的代码逻辑非常清晰:

    1. 存在 weak_entry_t 则证明该对象存在其他的弱引用,直接在原来的 weak_entry_t 最后新增一个 new_referrer
    2. 不存在 weak_entry_t 则证明该对象是第一次被弱引用,新增一个weak_entry_t后插入;

    append_referrer() 等插入的函数就不赘述了,还涉及到内联和外联的操作和实现,有兴趣的可以自己看代码;

    9. 总结

    一张图总结吧:

    SideTable

    二、SideTables 的初始化时机和流程

    1. 初始化流程

    SideTables 也就是 SideTableBuf, 是在 SideTableInit() 方法中初始化:

    static void SideTableInit() {
        new (SideTableBuf) StripedMap<SideTable>();
    }
    

    来看看 SideTableInit 的调用顺序,代码就不贴了:

    SideTableInit调用顺序

    map_images_nolock( )的代码太多了,就不贴了,只看下 arr_init( ) 的调用代码吧:

    arr_init

    要想知道 SideTables 何时被初始化,那么关键就在于 map_images( ) 何时被调用,而这个函数应该相当熟悉了吧:

    void _objc_init(void)
    {
        static bool initialized = false;
        if (initialized) return;
        initialized = true;
        
        // fixme defer initialization until an objc-using image is found?
        environ_init();
        tls_init();
        static_init();
        lock_init();
        exception_init();
    
        _dyld_objc_notify_register(&map_images, load_images, unmap_image);
    }
    

    如上,_objc_init( ) 调用 dyld 的 Api 注册通知并绑定了三个函数:

    1. map_images:印射到内存中的回调;
    2. load_images:加载时的回调;
    3. unmap_image:从内存中移除时的回调;

    _dyld_objc_notify_register 最终调用 registerObjCNotifiers 函数,dyld 中的源码如下:

    void registerObjCNotifiers(_dyld_objc_notify_mapped mapped, _dyld_objc_notify_init init, _dyld_objc_notify_unmapped unmapped)
    {
        // record functions to call
        sNotifyObjCMapped   = mapped;
        sNotifyObjCInit     = init;
        sNotifyObjCUnmapped = unmapped;
    
        // call 'mapped' function with all images mapped so far
        try {
            notifyBatchPartial(dyld_image_state_bound, true, NULL, false, true);
        }
        catch (const char* msg) {
            // ignore request to abort during registration
        }
    }
    

    如上图代码,mapped 的回调最终被赋值给了 sNotifyObjCMapped,而该函数的调用只存在于 notifyBatchPartial ( )中,且 state 为 dyld_image_state_bound

    上述代码的注释也说的很清楚,回调赋值之后就立马尝试了一次 notifyBatchPartial( )的调用。

    至此可以总结一下:

    1. map_images 函数在第一次注册 dyld 监听时被调用,会将所有具有 objc section 的 image 进行回传;
    2. 如果有新的 objc 相关的 image 被印射到内存,也会触发 map_images 的调用;
    3. SideTables 在第一次处理包含 objc section 的 image 时被初始化(只会被初始化一次,具体参见源码);
    4. arr_init 只调用一次,所以这个 SideTables 是整个生命周期只会生成一次,记录着所有对象的引用计数器和弱引用关系。这也是为什么注释中写道不能析构的原因;

    其逻辑如下:

    SideTableInit( )

    2. 补充

    再次温习下 _dyld_objc_notify_register() 函数:

    _dyld_objc_notify_register

    翻译注释中的几个要点:

    1. 该方法仅供 objc runtime 使用;
    2. Dyld 会以数组的形式将包含 objc 相关 section 的 image 进行回调,调用 mapped 方法;
    3. 上述的 image 都已经被自动增加了引用计数器,不需要再调用 dlopen() 方法来维持 image 不被 unmap;
    4. 新的包含 objc section 的 image 被 dlopen( )时也会调用 mapped 回调;
    5. 当将要调用 C++ 初始化方法时,init 回调将会被调用;

    之前梳理过 dyld 流程复习一下:

    1. dyld 自举
    2. 加载共享缓存
    3. 实例化主程序
    4. 加载插入的动态库
    5. 链接主程序(递归加载依赖库、递归刷新层级、递归rebase、递归bind、weakbind暂不绑定)
    6. 链接插入的动态库;
    7. weak bind;
    8. 调用主程序初始化方法(依赖库初始化方法调用、主程序初始化方法调用)
    9. 寻找并调用main函数;

    因为 libSystem 是依赖库,调用 libSystem 的初始化方法时,前面加载了所有的依赖库,所以此时的回调将会回调所有的包含 objc section 的 image 到 mapped 函数;

    这种逻辑正好也是相称的,objc 的初始化流程大概是:

    1. 因为 ImageLoaderMachO::doModInitFunctions 符号断点不是第一次就进入 libSystem_initializer,所以可以确认某些层级高于 libsystem.B.dylib 库的初始化函数调用,这些库应该是非 OBJC 库;
    2. libsystem.B.dylib 库可以理解成一个包装库,相对于其他 objc 库而言,需要优先被初始化。此时函数 libSystem_initializer 被调用;
    3. _objc_init 被调用,使用 dyld_objc_register 绑定了三个 image 相关的回调并触发 map_image( ) 函数被调用,进而完成了 SideTables 的初始化(与此同时 map_image 也做了很多其他的初始化操作);
    4. 至此,在 objc 相关库(如Foundation、UIKit等)的初始化方法被调用之前,objc 的环境就已经被配置完成;
    5. 其他依赖库的初始化方法调用,触发 load_image 进而触发 +load 方法,此时会使用到 SideTables 等 objc 全局相关的配置;

    3. 验证

    来个符号断点:

    符号断点

    看看结果:

    调用栈

    再来个 + load 的调用栈:

    +load

    三、疑问

    1. 为什么 weak 能够自动置为 nil;

    这个问题应该说的更具体一点:

    __weak 修饰的对象在被析构之后,弱指针为何会被置为 nil?而 assign 修饰的指针则仍然存储原来的内存地址;

    那么,这里就应该从对象的析构开始研究:

    inline void
    objc_object::rootDealloc()
    {
        if (isTaggedPointer()) return;  // fixme necessary?
    
        if (fastpath(isa.nonpointer  &&  
                     !isa.weakly_referenced  &&  
                     !isa.has_assoc  &&  
                     !isa.has_cxx_dtor  &&  
                     !isa.has_sidetable_rc))
        {
            assert(!sidetable_present());
            free(this);
        } 
        else {
            object_dispose((id)this);
        }
    }
    

    如上,如果开启了指针优化、没有弱引用、没有关联对象、没有 c++ 析构函数、引用计数器未存储到 sidetable 中,则直接 free(this),否则进入object_dispose()

    很显然,我们要寻找的逻辑肯定不符合上述的条件,继续用看看这个函数的代码:

    id 
    object_dispose(id obj)
    {
        if (!obj) return nil;
        objc_destructInstance(obj);    
        free(obj);
        return nil;
    }
    

    objc_destructInstance 函数中做了很多处理,比如 c++ 析构函数的处理、关联对象的处理等,暂时不关心这些逻辑,只关心弱引用逻辑,顺着代码最终进入到这个函数:

    inline void 
    objc_object::clearDeallocating()
    {
        if (slowpath(!isa.nonpointer)) {
            // Slow path for raw pointer isa.
            sidetable_clearDeallocating();
        }
        else if (slowpath(isa.weakly_referenced  ||  isa.has_sidetable_rc)) {
            // Slow path for non-pointer isa with weak refs and/or side table data.
            clearDeallocating_slow();
        }
    
        assert(!sidetable_present());
    }
    

    很显然 isa.weakly_referenced == 1,我们要的逻辑在 clearDeallocating_slow 中,最终进入到 weak_clear_no_lock 函数,在这里我们找到了答案:

    weak_clear_no_lock

    即:弱引用标志为 1 的对象在析构时,会遍历 weak_table 中的 referrers 数组并将指针置为 nil。该数组正是存储了哪些指针对该对象进行了弱引用。

    2. 数据结构

    其实 SideTable 可以作为复习数据结构的一个很好的实践例子,后续有时间可以研究下 refMap、weak_table 等各种数据结构的具体实现,暂略~~~;

    相关文章

      网友评论

        本文标题:iOS:SideTable

        本文链接:https://www.haomeiwen.com/subject/wripyltx.html