美文网首页
Objective-C 引用计数原理

Objective-C 引用计数原理

作者: 圣僧留步 | 来源:发表于2020-06-11 12:06 被阅读0次
    引言:这两天刚好将runtime的源码给看了一遍,自己也总结了一些东西,正好想要分享给大家,就写了这篇文章,咱们今天只说原理,不讲用法。

    大家都知道ARC在编译阶段会自动为我们插入引用计数的代码,那么Objective-C在内部又是如何存储引用计数的呢?存储方式有以下四种:

    • 对于支持使用TaggedPointer的

    1.对于有些对象如果支持使用TaggedPointer,苹果则会直接将其指针值作为引用计数返回。

    • 如果不支持使用TaggedPointer,可以分为以下两种情况

    1.如果当前设备是64位环境并且使用Objective-C 2.0,那么"一些"对象会使用其isa指针的一部分空间来存储它的引用计数;
    2.使用一张散列表SideTables()来管理引用计数。

    • 使用垃圾回收判断(UseGC属性),但是这种早就已经弃用了。而且初始化垃圾回收机制的 void gc_init(BOOL wantsGC)方法一直被传入NO。

    TaggedPointer存储引用计数

    判断当前对象是否在使用TaggedPointer需要看标志位是否为1

    # if SUPPORT_MSB_TAGGED_POINTERS
    # define TAG_MASK (1ULL<<63)
    #else
    #   define TAG_MASK 1
    
    inline bool 
    objc_object::isTaggedPointer() 
    {
    #if SUPPORT_TAGGED_POINTERS
        return ((uintptr_t)this & TAG_MASK);
    #else
        return false;
    #endif
    }  
    

    再讲解TaggedPointer之前,咱们先看一个例子:

     NSNumber *num = @(12) 
    

    咱们需要存储12这个数据,按照正常的技术方案,在64位CPU下,应该先去创建NSNumber对象,其值是12,然后再有个指向该地址的指针num。这样做存在什么问题呢?

    • 内存浪费

      由于OC中的内存对齐,在64位下,创建一个对象至少16字节,再加上一个指针8个字节,总共24字节,也就是说,为了存储这个12而需要24字节,对内存方面是极大的浪费。

    • 性能浪费

    为了存储和访问一个 NSNumber 对象,我们需要在堆上为其分配内存,另外还要维护它的引用计数,管理它的生命期。这些都给程序增加了额外的逻辑,造成运行效率上的损失

    Tagged Pointer 技术

    为了解决这个问题,苹果提出了Tagged Pointer的概念。对于 64 位程序,引入 Tagged Pointer 后,相关逻辑能减少一半的内存占用,以及 3 倍的访问速度提升,106 倍的创建、销毁速度提升。

    1、Tagged Pointer技术,主要为了用于优化NSNumber、NSDate、NSString等小对象的存储。
    2、在没有使用Tagged Pointer之前, NSNumber等对象需要动态分配内存、维护引用计数等,NSNumber指针存储的是堆中NSNumber对象的地址值。
    3、使用Tagged Pointer之后,NSNumber指针里面存储的数据变成了:Tag(引用计数) + num,Tagged Pointer 指针的值不在是地址了,而是真正的值。所以,实际上它不再是一个对象了,它只是一个披着对象皮的普通变量而已。所以,它的内存并不存储在堆中,也不需要 malloc 和 free。
    4、当指针不够存储数据时,才会使用动态分配内存的方式来存储数据

    isa指针存储引用计数

    用 64 bit 存储一个内存地址显然是种浪费,毕竟很少有那么大内存的设备。于是可以优化存储方案,用一部分额外空间存储其他内容。isa 指针第一位为 1 即表示使用优化的 isa 指针,这里列出不同架构下的 64 位环境中 isa 指针结构

    union isa_t 
    {
        isa_t() { }
        isa_t(uintptr_t value) : bits(value) { }
    
        Class cls;
        uintptr_t bits;
    
    #if SUPPORT_NONPOINTER_ISA
    # if __arm64__
    #   define ISA_MASK        0x00000001fffffff8ULL
    #   define ISA_MAGIC_MASK  0x000003fe00000001ULL
    #   define ISA_MAGIC_VALUE 0x000001a400000001ULL
        struct {
            uintptr_t indexed           : 1;
            uintptr_t has_assoc         : 1;
            uintptr_t has_cxx_dtor      : 1;
            uintptr_t shiftcls          : 30; // MACH_VM_MAX_ADDRESS 0x1a0000000
            uintptr_t magic             : 9;
            uintptr_t weakly_referenced : 1;
            uintptr_t deallocating      : 1;
            uintptr_t has_sidetable_rc  : 1;
            uintptr_t extra_rc          : 19;
    #       define RC_ONE   (1ULL<<45)
    #       define RC_HALF  (1ULL<<18)
        };
    
    # elif __x86_64__
    #   define ISA_MASK        0x00007ffffffffff8ULL
    #   define ISA_MAGIC_MASK  0x0000000000000001ULL
    #   define ISA_MAGIC_VALUE 0x0000000000000001ULL
        struct {
            uintptr_t indexed           : 1;
            uintptr_t has_assoc         : 1;
            uintptr_t has_cxx_dtor      : 1;
            uintptr_t shiftcls          : 44; // MACH_VM_MAX_ADDRESS 0x7fffffe00000
            uintptr_t weakly_referenced : 1;
            uintptr_t deallocating      : 1;
            uintptr_t has_sidetable_rc  : 1;
            uintptr_t extra_rc          : 14;
    #       define RC_ONE   (1ULL<<50)
    #       define RC_HALF  (1ULL<<13)
        };
    
    # else
        // Available bits in isa field are architecture-specific.
    #   error unknown architecture
    # endif
    
    // SUPPORT_NONPOINTER_ISA
    #endif
    
    }; 
    

    SUPPORT_NONPOINTER_ISA 用于标记是否支持优化的 isa 指针,其字面含义意思是 isa 的内容不再是类的指针了,而是包含了更多信息,比如引用计数,析构状态,被其他 weak 变量引用情况。判断方法也是根据设备类型:

    // Define SUPPORT_NONPOINTER_ISA=1 on any platform that may store something
    // in the isa field that is not a raw pointer.
    #if !SUPPORT_INDEXED_ISA  &&  !SUPPORT_PACKED_ISA
    #   define SUPPORT_NONPOINTER_ISA 0
    #else
    #   define SUPPORT_NONPOINTER_ISA 1
    #endif
    

    综合看来目前只有 arm64 架构的设备支持,下面列出了 isa 指针中变量对应的含义:

    变量名 含义
    indexed 0 表示普通的 isa 指针,1 表示使用优化,存储引用计数
    has_assoc 表示该对象是否包含 associated object,如果没有,则析构时会更快
    has_cxx_dtor 表示该对象是否有 C++ 或 ARC 的析构函数,如果没有,则析构时更快
    shiftcls 类的指针
    magic 固定值为 0xd2,用于在调试时分辨对象是否未完成初始化。
    weakly_referenced 表示该对象是否有过 weak 对象,如果没有,则析构时更快
    deallocating 表示该对象是否正在析构
    has_sidetable_rc 表示该对象的引用计数值是否过大无法存储在 isa 指针
    extra_rc 存储引用计数值减一后的结果

    在 64 位环境下,优化的 isa 指针并不是就一定会存储引用计数,毕竟用 19bit (iOS 系统)保存引用计数不一定够。需要注意的是这 19 位保存的是引用计数的值减一。has_sidetable_rc 的值如果为 1,那么引用计数会存储在一个叫 SideTable 的类的属性中,也就是咱们下面会讲到的散列表;后面会做详细讲解。

    散列表存储引用计数

    散列表来存储引用计数具体是用 DenseMap 类来实现,这个类中包含好多映射实例到其引用计数的键值对,并支持用 DenseMapIterator 迭代器快速查找遍历这些键值对。接着说键值对的格式:键的类型为 DisguisedPtr<objc_object>,DisguisedPtr 类是对 objc_object * 指针及其一些操作进行的封装,目的就是为了让它给人看起来不会有内存泄露的样子(真是心机裱),其内容可以理解为对象的内存地址;值的类型为 __darwin_size_t,在 darwin 内核一般等同于 unsigned long。其实这里保存的值也是等于引用计数减一。使用散列表保存引用计数的设计很好,即使出现故障导致对象的内存块损坏,只要引用计数表没有被破坏,依然可以顺藤摸瓜找到内存块的位置。

    接下来简单介绍一下SideTable这个结构体,它的结构如下:

    struct SideTable {
        spinlock_t slock;//保证原子操作的自选锁
        RefcountMap refcnts;//保存引用计数的散列表
        weak_table_t weak_table;//保存 weak 引用的全局散列表
    
        SideTable() {
            memset(&weak_table, 0, sizeof(weak_table));
        }
    
        ~SideTable() {
            _objc_fatal("Do not delete SideTable.");
        }
    
        void lock() { slock.lock(); }
        void unlock() { slock.unlock(); }
        void forceReset() { slock.forceReset(); }
    
        // Address-ordered lock discipline for a pair of side tables.
    
        template<HaveOld, HaveNew>
        static void lockTwo(SideTable *lock1, SideTable *lock2);
        template<HaveOld, HaveNew>
        static void unlockTwo(SideTable *lock1, SideTable *lock2);
    };
    

    这个结构体,它主要管理引用计数表和 weak 表,并使用 spinlock_lock 自旋锁来防止操作表结构时可能的竞态条件。它用一个 64*128 大小的 uint8_t 静态数组作为 buffer 来保存所有的 SideTable 实例。从上面我们可以看到SideTable给我们提供了三个公有属性:

    spinlock_t slock;//保证原子操作的自选锁
    RefcountMap refcnts;//保存引用计数的散列表
    weak_table_t weak_table;//保存 weak 引用的全局散列表
    

    它还给我们提供了一个工厂方法,用于根据对象的地址在 buffer 中寻找对应的 SideTable 实例:

    static SideTable *tableForPointer(const void *p)
    

    weak 表的作用是在对象执行 dealloc 的时候将所有指向该对象的 weak 指针的值设为 nil,避免悬空指针。weak表的具体结构如下:

    struct weak_table_t {
        weak_entry_t *weak_entries;
        size_t    num_entries;
        uintptr_t mask;
        uintptr_t max_hash_displacement;
    };
    

    苹果使用一个全局的 weak 表来保存所有的 weak 引用。并将对象作为键,weak_entry_t 作为值。weak_entry_t 中保存了所有指向该对象的 weak 指针。

    相关文章

      网友评论

          本文标题:Objective-C 引用计数原理

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