引言:这两天刚好将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 指针。