Objective-C 引用计数原理

引言:这两天刚好将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,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
    static void lockTwo(SideTable *lock1, SideTable *lock2);
    template
    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 引用计数原理)