深入了解 iOS 引用计数

iOS 通过引用计数来管理内存,简单的说就是当一个 id 类型的对象引用计数变成 0 的时候就会被销毁,回收内存。
本文将通过断点调试来探究引用计数的存储及读取在底层的实现。全程无聊,请配合源码阅读

源码版本:objc4-779.1
当然还是推荐使用这个来学习:可编译的源码

引用计数存储在哪

引用计数只适用于 id 类型的对象

有一种特殊的 id 类型对象,Tagged Pointer,如果不知道的话可以看我另一篇博客 深入了解Tagged Pointer了解一下

因为 Tagged Pointer 并不是真正的对象,所以它并不通过引用计数来管理其内存。
用下面的代码求 n1 的引用计数,输出结果为 9223372036854775807

1
2
3
// 创建一个 Tagged Pointer 对象
NSNumber *n1 = [NSNumber numberWithInt:1];
NSLog(@"n1 rc = %ld", CFGetRetainCount((__bridge CFTypeRef)(n1)));

我们知道每个 id 类型对象都有一个 isa 指针,其中只有 33 位用来保存其父类或者元类信息,剩余的31位也不能浪费啊,这其中就会一些 bit 用来保存引用计数。
下面是 arm64 架构下的 isa 结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
union isa_t {
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }

Class cls;
uintptr_t bits;

struct {
# define ISA_MASK 0x0000000ffffffff8ULL
# define ISA_MAGIC_MASK 0x000003f000000001ULL
# define ISA_MAGIC_VALUE 0x000001a000000001ULL
# define ISA_BITFIELD \
uintptr_t nonpointer : 1; \
uintptr_t has_assoc : 1; \
uintptr_t has_cxx_dtor : 1; \
uintptr_t shiftcls : 33; /*MACH_VM_MAX_ADDRESS 0x1000000000*/ \
uintptr_t magic : 6; \
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)
};
#endif
};

可以发现,isa_t 是一个联合体(union),几个不同类型的变量 clas 和 bits 及 struct 共用同一段内存。当 isa 指针的第一位nonpointer为 1 时表示它是优化的 isa 指针,即将多余的 bit 用来存储其它信息,从它的名字nonpointer也可以看出它不再是一个纯粹的指针了。当为 1 时,第一个成员变量cls是没有用的,因为 isa 不再使用它来指向父类 cls。下面是 isa 指针各位域的含义

变量名 含义
nonpointer 表示是否对 isa 指针开启优化 0:纯isa指针,1:存储了额外信息
has_assoc 表示该对象是否包含 associated object,如果没有,则析构时会更快
has_cxx_dtor 表示该对象是否有 C++ 或 ARC 的析构函数,如果没有,则析构时更快
shiftcls 类的指针
magic 固定值 0x1a, 用于调试器判断当前对象是真的对象还是没有初始化的空间
weakly_referenced 表示该对象是否有过 weak 对象,如果没有,则析构时更快
deallocating 表示该对象是否正在析构
has_sidetable_rc 表示该对象的引用计数值是否过大无法存储在 isa 指针
extra_rc 存储引用计数值减一后的结果

在 arm64 下, isa 使用 19 个 bit 用来存储引用计数. 当引用计数超过这个数时,将会把 RC_HALF 数量的引用计数存储在一个全局哈希表中,此时 has_sidetable_rc 变为 1

增加引用计数

下面让我们通过调试来探究具体的存储过程
因为是在模拟器中运行的,isa 最多只能使用 8 个 bit 来存储引用计数, 即在 isa 中只能存储不超过 256 的数目。所以我们只要将对象的引用计数增加到 256 以上,那么系统就会将多余的引用计数存储到哈希表。

1
2
3
4
5
6
void foo(void) {
NSObject *o1 = [[NSObject alloc] init];
for (int i = 0; i < 500; i++) {
_objc_rootRetain(o1);
}
}

_objc_rootRetain()方法最终会调用objc_object::rootRetain(bool tryRetain, bool handleOverflow), 这个方法的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
ALWAYS_INLINE id 
objc_object::rootRetain(bool tryRetain, bool handleOverflow)
{
if (isTaggedPointer()) return (id)this;

bool sideTableLocked = false;
bool transcribeToSideTable = false;

isa_t oldisa;
isa_t newisa;

do {
// 是否将引用计数转移到索引表中
transcribeToSideTable = false;
// LoadExclusive 的作用是让读取操作原子化
oldisa = LoadExclusive(&isa.bits);
newisa = oldisa;
// if (slowpath) else (),表示大概率执行 else 后面的语句,用来提高效率。
// fastpath 则表示大概率执行 if 后面的语句
if (slowpath(!newisa.nonpointer)) {
// arm64 架构下,一般的对象 nonpointer 为YES,但也有特例,例如 block 的 nonpointer 为 False,即不使用 isa 保存额外信息
ClearExclusive(&isa.bits);
if (rawISA()->isMetaClass()) return (id)this;
if (!tryRetain && sideTableLocked) sidetable_unlock();
if (tryRetain) return sidetable_tryRetain() ? (id)this : nil;
else return sidetable_retain();
}
// don't check newisa.fast_rr; we already called any RR overrides
if (slowpath(tryRetain && newisa.deallocating)) {
ClearExclusive(&isa.bits);
if (!tryRetain && sideTableLocked) sidetable_unlock();
return nil;
}
// 用来表示 isa 指针的值是否溢出。因为引用计数存储在指针的高位,当引用计数增加到一定程度,会超过最高位的数字,此时 carry 的值不等于 0,表示溢出
uintptr_t carry;
// addc() 是一个内置函数,作用是将 isa 里面的引用计数 + 1 后保存
newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry); // extra_rc++

if (slowpath(carry)) {
// newisa.extra_rc++ overflowed
// 移除的话则需要对多余的引用计数保存到索引表中。
// 刚开始进入的时候 handleOverflow == false
if (!handleOverflow) {
ClearExclusive(&isa.bits);
return rootRetain_overflow(tryRetain);
}
// 加锁
if (!tryRetain && !sideTableLocked) sidetable_lock();
// 此时 isa 指针存储的引用计数应该是满的, extra_rc 的 19 位都是 1,将 extra_rc 赋值为原来的一半,也就是将 isa 的最高位赋值为 0,这样做的目的是下次引用计数增加的时候可以直接存储在 isa 中,而不需要调用索引表来存储。将 has_sidetable_rc 赋值为 1,标记有额外的引用计数存储在索引表中
sideTableLocked = true;
transcribeToSideTable = true;
newisa.extra_rc = RC_HALF;
newisa.has_sidetable_rc = true;
}
// StoreExclusive(), 如果&isa.bits和oldisa.bits相等,那么就把newisa.bits的值赋给&isa.bits,并且返回true。
} while (slowpath(!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)));

if (slowpath(transcribeToSideTable)) {
// Copy the other half of the retain counts to the side table.
// 如果溢出的话,则将 isa 存储的引用计数赋值为最大值的一半,即 RC_HALF,那么减少的一半则转移到索引表中
sidetable_addExtraRC_nolock(RC_HALF);
}

if (slowpath(!tryRetain && sideTableLocked)) sidetable_unlock();
return (id)this;
}

写了点注释帮助你们理解. 现在我们在sidetable_addExtraRC_nolock(RC_HALF);那一行打个断点,启动程序

step into进入这个方法

因为用的模拟器,所以 RC_HALF == delta_rc == 128,符合预期。

代码SideTable& table = SideTables()[this],通过计算指针值得到哈希值获取到对应的 sidetable
让我们点开方法SideTables()

1
2
3
4
5
static objc::ExplicitInit<StripedMap<SideTable>> SideTablesMap;

static StripedMap<SideTable>& SideTables() {
return SideTablesMap.get();
}

class ExplicitInit 的定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
template <typename Type>
class ExplicitInit {
alignas(Type) uint8_t _storage[sizeof(Type)];

public:
template <typename... Ts>
void init(Ts &&... Args) {
new (_storage) Type(std::forward<Ts>(Args)...);
}

Type &get() {
return *reinterpret_cast<Type *>(_storage);
}
};

ExplicitInit的作用是生成一个模板类型 Type 的实例。alignas(Type) uint8_t _storage[sizeof(Type)];成员变量_storage是一个 sizeof(Type) 长度的 uint8_t 数组,而 uint8_t 占用一个字节,所以实际上_storage的长度跟一个 Type 实例所占用的内存是一样多的。成员函数 Type &get() 将 _storage 数组指针用reinterpret_cast<Type *>强转成了 Type * 类型指针,前面再加一个 *,说明返回的实际上是 Type 实例内存的首地址。而另一个成员函数init用来初始化生成的 Type 实例。

所以static StripedMap<SideTable>& SideTables()函数实际上返回了一个全局静态StripedMap<SideTable>的实例。
下面是class StripedMap的部分定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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;
}
// ...省略
}
  • 局部结构体PaddedT的作用是用来内存对齐,常量CacheLineSize是固定值 64。在 arm64 架构下,成员变量array是一个长度为 8,元素类型为 T,每个元素占 64 字节的数组,而在模拟器下是一个长度为 64,每个元素占 64 字节的数组。
  • 成员函数indexForPointer用来根据指针来计算哈希值,确定对应于array里面第几个元素。
  • T& operator[] (const void *p)使用 operator 关键字重载了[]符号。举个例子,StripedMap 有一个实例 sm,有一个 int 类型的变量 a,当执行sm[&a]的代码时,就会跳转到T& operator[] (const void *p)这个方法里面。而在上面的重载[]函数中,传入的参数为指针类型,然后计算出其数组 index,返回对应的元素 T,也就是 SideTable

继续调试,断点此时在SideTable& table = SideTables()[this];这一行。点击 step into

_storage的数组长度为 4096,也就是 64 * 64,符合预期。继续step into,最终会跳转到下面这里

跳转到了重载的[]方法里面了。

目前已知道的情报就是,有一个全局的StripedMap条目索引,里面包含了若干个SideTable索引表。在使用时,根据指针得到对应的 sidetable。SideTable 的部分定义如下

1
2
3
4
5
6
7
8
9
10
11
12
13
struct SideTable {
spinlock_t slock;
RefcountMap refcnts;
weak_table_t weak_table;

SideTable() {
memset(&weak_table, 0, sizeof(weak_table));
}
void lock() { slock.lock(); }
void unlock() { slock.unlock(); }
void forceReset() { slock.forceReset(); }
// ...省略
};

SideTable 的成员对象里面有一个自旋锁,引用映射表RefcountMap refcnts,弱引用表weak_table_t weak_table。在上面我们提到了每个 sidetable 占用 64 个字节, 那是因为 spinlock_t 占用 4, RefcountMap 占用 24, weak_table_t 占用 32, 加起来就是 64.
在这里我们只了解下 RefcountMap, 因为它跟我们引用计数相关.
RefcountMap是一个类型别名,它的定义如下:

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

先别急着查看DenseMap的定义,我们先看看DisguisedPtr是个什么类。它的定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
template <typename T>
class DisguisedPtr {
uintptr_t value;

static uintptr_t disguise(T* ptr) {
return -(uintptr_t)ptr;
}

static T* undisguise(uintptr_t val) {
return (T*)-val;
}

public:
DisguisedPtr() { }
DisguisedPtr(T* ptr)
: value(disguise(ptr)) { }
DisguisedPtr(const DisguisedPtr<T>& ptr)
: value(ptr.value) { }

DisguisedPtr<T>& operator = (T* rhs) {
value = disguise(rhs);
return *this;
}
DisguisedPtr<T>& operator = (const DisguisedPtr<T>& rhs) {
value = rhs.value;
return *this;
}

operator T* () const {
return undisguise(value);
}
T* operator -> () const {
return undisguise(value);
}
T& operator * () const {
return *undisguise(value);
}
T& operator [] (size_t i) const {
return undisguise(value)[i];
}
};
  • 简单的说, 你可以像指针Type *一样的使用DisguisedPtr,但是其成员变量value保存的是伪装后的指针值。伪装函数是disguise(),0 和 0x800…00 经过伪装之后还是本身,但指针值也不可能是这两个值,所以无所谓的。
  • 为了能像指针Type *一样的使用DisguisedPtr,在DisguisedPtr内部,重载了许多符号,例如 [], =, *, ()。具体就不分析了。
  • 该类的作用是为了避免直接引用指针, 防止被 leaks 内存检测工具检测出内存泄漏

class RefcountMapValuePurgeable的定义如下:

1
2
3
4
5
struct RefcountMapValuePurgeable {
static inline bool isPurgeable(size_t x) {
return x == 0;
}
};

它只有一个内联函数,根据 x 是否为 0 返回一个 bool 值。它的作用就是根据参数判断是否需要析构这个实例
好了,了解完了DisguisedPtrRefcountMapValuePurgeable,接下来我们来看DenseMap,它的定义如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
template <typename KeyT, typename ValueT,
typename ValueInfoT = DenseMapValueInfo<ValueT>,
typename KeyInfoT = DenseMapInfo<KeyT>,
typename BucketT = detail::DenseMapPair<KeyT, ValueT>>
class DenseMap : public DenseMapBase<DenseMap<KeyT, ValueT, ValueInfoT, KeyInfoT, BucketT>,
KeyT, ValueT, ValueInfoT, KeyInfoT, BucketT> {
friend class DenseMapBase<DenseMap, KeyT, ValueT, ValueInfoT, KeyInfoT, BucketT>;

using BaseT = DenseMapBase<DenseMap, KeyT, ValueT, ValueInfoT, KeyInfoT, BucketT>;

BucketT *Buckets;
unsigned NumEntries;
unsigned NumTombstones;
unsigned NumBuckets;

public:
explicit DenseMap(unsigned InitialReserve = 0) { init(InitialReserve); }
}
  • 代码friend class DenseMapBase的作用是声明一个友元类DenseMapBase,这样DenseMapBase在内存可以直接访问DenseMap private 的成员函数。
  • 代码using BaseT = DenseMapBase的作用是为DenseMapBase指定一个别名BaseT
  • 函数explicit DenseMap(), explicit的作用是用来声明类构造函数是显示调用的,而非隐式调用。

DenseMap里面有 4 个成员变量,Buckets 和 NumEntries,NumTombstones, NumBuckets。

  • Buckets, 存储引用计数的数组, 元素类似于字典, key 为指针, value 为引用计数. 数组可扩容
  • NumEntries 表示数组中用来存储引用计数的元素有几个
  • NumTombstones 数组中元素被用来存储一个对象的引用计数, 当这个对象被销毁后, 该元素被标记为墓碑. 该变量用来表示有多少个这种元素
  • NumBuckets 数组 Buckets 的长度

再来看一下模板里的几个类型 DenseMapInfo<KeyT>, detail::DenseMapPair<KeyT, ValueT>

DenseMapInfo 有很多的模板定义,可以适用于各种类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
template<typename T>
struct DenseMapInfo<DisguisedPtr<T>> {
static inline DisguisedPtr<T> getEmptyKey() {
return DisguisedPtr<T>((T*)(uintptr_t)-1);
}
static inline DisguisedPtr<T> getTombstoneKey() {
return DisguisedPtr<T>((T*)(uintptr_t)-2);
}
static unsigned getHashValue(const T *PtrVal) {
return ptr_hash((uintptr_t)PtrVal);
}
static bool isEqual(const DisguisedPtr<T> &LHS, const DisguisedPtr<T> &RHS) {
return LHS == RHS;
}
};

内联函数getEmptyKeygetTombstoneKey均返回一个特征值,用来标记 bucket 的状态 Empty 和 Tombstone。
静态函数getHashValue根据指针计算出哈希值。
ptr_hash()的实现如下:

1
2
3
4
5
6
7
static inline uint32_t ptr_hash(uint64_t key)
{
key ^= key >> 4;
key *= 0x8a970be7488fda55;
key ^= __builtin_bswap64(key);
return (uint32_t)key;
}

看不懂,知道是计算哈希值的就好了。

最后一个静态函数isEqual用来比较两个参数是否相等。

接着看detail::DenseMapPair<KeyT, ValueT> 它的定义如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
template <typename KeyT, typename ValueT>
struct DenseMapPair : public std::pair<KeyT, ValueT> {
DenseMapPair() {}

DenseMapPair(const KeyT &Key, const ValueT &Value)
: std::pair<KeyT, ValueT>(Key, Value) {}

DenseMapPair(KeyT &&Key, ValueT &&Value)
: std::pair<KeyT, ValueT>(std::move(Key), std::move(Value)) {}

template <typename AltKeyT, typename AltValueT>
DenseMapPair(AltKeyT &&AltKey, AltValueT &&AltValue,
typename std::enable_if<
std::is_convertible<AltKeyT, KeyT>::value &&
std::is_convertible<AltValueT, ValueT>::value>::type * = 0)
: std::pair<KeyT, ValueT>(std::forward<AltKeyT>(AltKey),
std::forward<AltValueT>(AltValue)) {}

template <typename AltPairT>
DenseMapPair(AltPairT &&AltPair,
typename std::enable_if<std::is_convertible<
AltPairT, std::pair<KeyT, ValueT>>::value>::type * = 0)
: std::pair<KeyT, ValueT>(std::forward<AltPairT>(AltPair)) {}

KeyT &getFirst() { return std::pair<KeyT, ValueT>::first; }
const KeyT &getFirst() const { return std::pair<KeyT, ValueT>::first; }
ValueT &getSecond() { return std::pair<KeyT, ValueT>::second; }
const ValueT &getSecond() const { return std::pair<KeyT, ValueT>::second; }
};

看起来很复杂的样子,但其实只要知道它的父类std::pair是干嘛的就好了。
std::pair的作用是将两个数据组合成一个数据,两个数据可以是同一类型或者不同类型。它是一个模板结构体,主要的两个成员变量是first和second,这两个变量可以直接使用。
再根据注释,可以推测出DenseMapPair作用是将 keyT 和 ValueT 两个类型的值保存在一起

继续之前的调试,在代码size_t& refcntStorage = table.refcnts[this];打个断点,然后点击 continue program execution 跳转过去。

注意这里的size_t&符号不是取内存地址意思,& 在这里表示引用的意思。举个例子

1
2
3
int a = 1;
int& b = a;
b = 10;

对 b 赋值 10 后,a 的值也变成了 10。好了,继续

继续 step into

首先会跳转到类DisguisedPtr的一个初始化方法里面,这里对指针进行伪装并赋值给了成员对象 value。
继续 step into

跳转到了这里。可以看出来DenseMap的父类DenseMapBase也重载了[]方法。
参数类型为KeyT &&,也就是DisguisedPtr<objc_object> &&类型。这里的&&符号以及下面的std::move(Key)函数都是为了确定参数是右值引用,实现移动语义,减少不必要的参数拷贝。

继续 step into,跳转到了下面这里

之前我们讲DenseMapPair<KeyT, ValueT>的时候说到过,这个类的作用是将两个类型的值的组合成一个整体,类似于我们平常使用的字典。key 对应 first, value 对应 second。在FindAndConstruct方法中,首先会通过LookupBucketFor()查找是否已经有存在的 bucket,如果没有则通过InsertIntoBucket函数生成并返回对应的 bucket。

下面是LookupBucketFor()函数的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
template<typename LookupKeyT>
bool LookupBucketFor(const LookupKeyT &Val,
const BucketT *&FoundBucket) const {
// 返回成员变量 Buckets
const BucketT *BucketsPtr = getBuckets();
// 返回成员变量 NumBuckets
const unsigned NumBuckets = getNumBuckets();

if (NumBuckets == 0) {
FoundBucket = nullptr;
return false;
}

// 保存在查找过程中碰到的墓碑
const BucketT *FoundTombstone = nullptr;
// DenseMapInfo 的内联函数 getEmptyKey
const KeyT EmptyKey = getEmptyKey();
// DenseMapInfo 的内联函数 getTombstoneKey
const KeyT TombstoneKey = getTombstoneKey();
assert(!KeyInfoT::isEqual(Val, EmptyKey) &&
!KeyInfoT::isEqual(Val, TombstoneKey) &&
"Empty/Tombstone value shouldn't be inserted into map!");

//DenseMapInfo 的静态函数 getHashValue 计算哈希值,并与 NumBuckets-1 进行与运算,得到对应 BucketT 实例的 index
unsigned BucketNo = getHashValue(Val) & (NumBuckets-1);
// 查找次数
unsigned ProbeAmt = 1;
while (true) {
const BucketT *ThisBucket = BucketsPtr + BucketNo;
// Found Val's bucket? If so, return it.
// 找到了对应的 BucketT 实例
if (LLVM_LIKELY(KeyInfoT::isEqual(Val, ThisBucket->getFirst()))) {
FoundBucket = ThisBucket;
return true;
}

// If we found an empty bucket, the key doesn't exist in the set.
// Insert it and return the default value.
// 如果对应的 bucket 是空的。如果是第一次查找的话则在这个 bucket 里面插入 key 和 value(在InsertIntoBucket函数中执行)。如果不是第一次并且之前找到了墓碑,则使用墓碑bucket插入key和value
if (LLVM_LIKELY(KeyInfoT::isEqual(ThisBucket->getFirst(), EmptyKey))) {
// If we've already seen a tombstone while probing, fill it in instead
// of the empty bucket we eventually probed to.
FoundBucket = FoundTombstone ? FoundTombstone : ThisBucket;
return false;
}

// If this is a tombstone, remember it. If Val ends up not in the map, we
// prefer to return it than something that would require more probing.
// Ditto for zero values.
// 如果找到一个墓碑,则使用 FoundTombstone 将这个墓碑记录下来
if (KeyInfoT::isEqual(ThisBucket->getFirst(), TombstoneKey) &&
!FoundTombstone)
FoundTombstone = ThisBucket; // Remember the first tombstone found.
// 如果 FoundTombstone 为空并且 对应的 bucket 的 second 是 0,则用FoundTombstone保存下这个bucket
if (ValueInfoT::isPurgeable(ThisBucket->getSecond()) && !FoundTombstone)
FoundTombstone = ThisBucket;

// Otherwise, it's a hash collision or a tombstone, continue quadratic
// probing.
// 如果查找次数多余 NumBuckets ,则报错
if (ProbeAmt > NumBuckets) {
FatalCorruptHashTables(BucketsPtr, NumBuckets);
}
// 重新计算 BucketNo,重新查找
BucketNo += ProbeAmt++;
BucketNo &= (NumBuckets-1);
}
}

在上面的代码里写了点注释帮助理解。
bucket 有两种状态,一种是 Empty,这个表示当前这个 bucket 还没有被使用的标记;另一个状态是 TombstoneKey,当一个 bucket 被使用了,当对象释放后该 bucket 被定义为这种状态

通过FindAndConstruct函数的分析我们知道,如果已经存在 bucket,则返回 YES,并将指针 TheBucket 指向这个 bucket。如果存在一个墓碑或者空的 bucket,则返回 false,并将指针 TheBucket 指向这个 bucket。

接着分析InsertIntoBucket(),下面是它的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
template <typename KeyArg, typename... ValueArgs>
BucketT *InsertIntoBucket(BucketT *TheBucket, KeyArg &&Key,
ValueArgs &&... Values) {
TheBucket = InsertIntoBucketImpl(Key, Key, TheBucket);

// 保存 key 到 bucket 的成员变量 first 中
TheBucket->getFirst() = std::forward<KeyArg>(Key);
// 初始化 ValueT 的实例并保存到 bucket 的成员变量 second 中
::new (&TheBucket->getSecond()) ValueT(std::forward<ValueArgs>(Values)...);
return TheBucket;
}

template <typename LookupKeyT>
BucketT *InsertIntoBucketImpl(const KeyT &Key, const LookupKeyT &Lookup,
BucketT *TheBucket) {
// If the load of the hash table is more than 3/4, or if fewer than 1/8 of
// the buckets are empty (meaning that many are filled with tombstones),
// grow the table.
//
// The later case is tricky. For example, if we had one empty bucket with
// tons of tombstones, failing lookups (e.g. for insertion) would have to
// probe almost the entire table until it found the empty bucket. If the
// table completely filled with tombstones, no lookup would ever succeed,
// causing infinite loops in lookup.
unsigned NewNumEntries = getNumEntries() + 1;
unsigned NumBuckets = getNumBuckets();
// 当 buckets 数组已经使用了超过 3/4 或者 空bucket的数量小于 1/8,则增加 buckets 数组的长度。
if (LLVM_UNLIKELY(NewNumEntries * 4 >= NumBuckets * 3)) {
// 扩增原来的 NumBuckets,新的 buckets 长度为旧的两倍,长度至少为 4。新的 buckets 生成后将旧数组里面的数据转移过去,并且旧数组删除
this->grow(NumBuckets * 2);
// 因为重新生成了 buckets 数组,所以需要使用LookupBucketFor重新查找对应bucket,并用指针 TheBucket 指向
LookupBucketFor(Lookup, TheBucket);
NumBuckets = getNumBuckets();
} else if (LLVM_UNLIKELY(NumBuckets-(NewNumEntries+getNumTombstones()) <=
NumBuckets/8)) {
this->grow(NumBuckets);
LookupBucketFor(Lookup, TheBucket);
}
ASSERT(TheBucket);

// Only update the state after we've grown our bucket space appropriately
// so that when growing buckets we have self-consistent entry count.
// If we are writing over a tombstone or zero value, remember this.
if (KeyInfoT::isEqual(TheBucket->getFirst(), getEmptyKey())) {
// Replacing an empty bucket.
// 使用空bucket,NumEntries + 1
incrementNumEntries();
} else if (KeyInfoT::isEqual(TheBucket->getFirst(), getTombstoneKey())) {
// Replacing a tombstone.
// 使用墓碑 bucket,NumEntries + 1, NumTombstones - 1
incrementNumEntries();
decrementNumTombstones();
} else {
// we should be purging a zero. No accounting changes.
// 如果 second 等于 0,则析构 second
ASSERT(ValueInfoT::isPurgeable(TheBucket->getSecond()));
TheBucket->getSecond().~ValueT();
}

return TheBucket;
}

通过InsertIntoBucket函数我们了解到,当 buckets 数组已经使用了超过 3/4 或者 空bucket的数量小于 1/8,则会生成一个新的 buckets,长度为原来的2倍(最小为4),并将旧数组的数据转移到新数组中,并重新查找 TheBucket 的 index。最后将伪装后的指针保存到bucket的first中,将引用计数保存到bucket的second中。

理论分析到这里。这几个函数的调试我就不贴出来了,比较长,你们可以自己调试下。

接下来在代码size_t oldRefcnt = refcntStorage;中打个断点,点击 continue program execution 跳转过去。

让我们回到objc_object::sidetable_addExtraRC_nolock函数中。因为我们刚刚在 sidetable 索引表里面插入了 bucket,所以这里 oldRefcnt 的值 为 0。
接下来会对 oldRefcnt 进行校验,这说明 oldRefcnt 并没有把所有的 bit 都用来保存引用计数。

  • 低 1 位(从低位往高位数第一位)用来标记是否是弱引用
  • 低 2 位用来标记该变量是否在析构
  • 高 1 位(从高位往低位数第一位)用来标记保存的引用计数是否已经溢出。

下面的代码我们好像看到过了,对,在将引用计数保存在 isa 指针那里

1
2
3
4
5
6
7
8
9
10
11
12
uintptr_t carry;
size_t newRefcnt =
addc(oldRefcnt, delta_rc << SIDE_TABLE_RC_SHIFT, 0, &carry);
if (carry) {
refcntStorage =
SIDE_TABLE_RC_PINNED | (oldRefcnt & SIDE_TABLE_FLAG_MASK);
return true;
}
else {
refcntStorage = newRefcnt;
return false;
}

这几句的代码的作用是将 newRefcnt = oldRefcnt + delta_rc,如果值太大了溢出了则将 carry 赋值为1,并将 refcntStorage 的高1位赋值为1,并返回true。如果没有溢出的话则将 newRefcnt 赋值到 refcntStorage,并返回 false。

到这里,我们探究了当引用计数增加时,如何保存多余的引用计数。简单的归纳下:

  • 有一个静态全局变量 SideTablesMap,实际上是 StripedMap 的实例。以便于在程序初始化的时候就能用到
  • 变量 SideTablesMap 里面有一个长度为 8,类型为 SideTable 的数组成员array,每个元素占用的内存为64(spinlock_t内存4, RefcountMap内存24,weak_table_t内存32)。在保存引用计数的时候,根据指针地址来计算出相应 SideTable 的 index。
  • 获取到相应的 SideTable 后,根据指针地址计算出相应的哈希值,然后查找 bucket。如果 bucket 不存在,则使用空 bucket 或者 墓碑 bucket。如果 bucket 的数量不够则扩容。将指针为 key 和 引用计数 为 value 保存到 bucket 当中。

注意:当一个对象创建后它的引用计数为 1,但是 isa 指针里的引用计数还是 0。因为初始化的时候并没有对引用计数进行操作。

减少引用计数

这里我们只讨论有额外引用计数存储在 sidetable,且 isa 指针里面存储的引用计数为 0 时的临界情况。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
void aoo(void) {
NSObject *o1 = [[NSObject alloc] init];
for (int i = 0; i < 512; i++) {
_objc_rootRetain(o1);
}
for (int i = 0; i < 129; i++) {
if (i == 128) {
int a = 1;
}
_objc_rootRelease(o1);
}
}

在代码int a = 1;处打个断点,此时 isa 保存的引用计数为 0。一直 step over,直到跳转到objc_object::rootRelease(bool performDealloc, bool handleUnderflow)函数。


一直点击 step over,直到停在newisa.bits = subc(newisa.bits, RC_ONE, 0, &carry);那一行

用调试命令打印看一下此时 isa 指针里面的内容

因为使用的是模拟器,所以 isa 里面只有最高的 8 个bit用来保存引用计数,之前我们减少它的引用计数 128。所以此时它 isa 指针里面保存的引用计数为 0。调试输出结果extra_rc也为0,符合预期(0b是二进制符号)

接下来系统要将引用保存在 isa 指针的引用计数 -1,并判断是否溢流。此刻它的引用计数已经为 0 了,那么对它进行 -1 操作会发生什么呢?我们接着调试,点击 step over


可以看到变量carry == 1,表示出现溢流。让我们再看一下 isa 指针里面发生了什么。


我们可以看到保存引用计数的 8 个 bit 都变成了 1,这其实就是 -1 在无符号整数中的表现形式,不懂的同学可以去了解一下补码。

接着一直 step over,我们可以看到因为溢流所以跳转到了underflow部分。一直 step over 直到跳转到如下的位置

rootRelease_underflow最终调用的还是objc_object::rootRelease()函数,只不过参数 handleUnderflow 变成了 YES。
进入该函数后,又会执行之前的那些代码,过程也是一样的,就不重复说了。接下来会来到这个位置

是的,因为没上锁,所以又会回到retry部分的代码。注释里写着是因为

Need to start over to avoid a race against the nonpointer -> raw pointer transition.

不是很明白,但是过了。继续 step over

之前引用计数溢出时,我们将RC_HALF数目的引用计数存储到了sidetable当中,此时因为溢流,所以我们从sidetable中取回同样数目的引用计数。

step into 进入objc_object::sidetable_subExtraRC_nolock()函数

在最开始的代码中,我们增加了变量o1 512 的引用计数, 在 isa 指针中保存了 128, 还剩余 384. 但是因为最低两位没有用来保存数据, 所以在 bucket 中存储的数字应该是 384 << 2, 即 1536, 符合预期

接下来的代码作用就是判断保存在 bucket 中的引用计数是否合理, 如何合理的话则将原有的数据减去RC_HALF并重新保存. 点击 step out 返回之前的函数

  • 代码块1的作用是将从 sidetable 取回了保存着的一部分引用计数, -1 保存到 isa 指针里面
  • 如果代码块1保存失败, 则用代码块2重新保存一次
  • 如果代码块2保存失败了, 则将从 sidetable 取出的引用计数重新放回去. 然后又跳回到 retry 部分
  • 保存成功则解锁, 并返回 false. 返回 true 表示这个对象需要 dealloc 了

另一种情况, 如果一个对象 isa 引用计数为 0, 且没有额外的引用计数. 那么它从 sidetable ‘借来的’ borrowed 为 0, 那么会将 isa 的 deallocating 赋值为 1, 并随后对其发送 delloc 消息进行内存消耗

最后

以上就是此次对引用计数的探究, 欢迎留言告诉我错的或者不明白的地方~

Objective-C引用计数原理

在 Objective-C 2.0 中,我们无需手动进行内存管理,因为ARC会自动帮我们在编译的时候,在合适的地方对对象进行retainrelease操作。
本文将结合runtime 750版本源码 探究 ARC 环境下引用计数的实现原理。

如何存储引用计数

从 5S 开始,iPhone 都采用了64位架构的处理器,为了节省内存和提高执行效率,苹果提出了Tagged Pointer的概念,专门用来存储小的对象,例如 NSNumber 和 NSDate。这一类变量本身的值需要占用的内存大小常常不需要8字节,拿整数来说,4个字节所能表示的有符号整数可以达到20多亿(2^31=2147483648,另外 1 位作为符号位),基本可以处理大多数情况。所以我们将一个对象的指针(64位下8字节)拆成两部分,一部分用来存储数据,另一部分作为特殊标记,表示这是一个 Tagged Pointer, 不指向任何一个地址。也就是当某些类使用 Tagged Pointer 来存储数据后,它就不是一个对象了,因为它并没有指向任何地址,变成了一个披着对象皮的普通变量而已,而对于这一类的‘对象’,它的内存是分配在中,由系统分配以及释放,所以它的引用计数也没有意义了,当然你仍然可以使用CFGetRetainCount方法去获取它的引用计数,返回的是它的指针地址。

而在某些平台中(比如arm64),isa 实例的一部分空间也会被用来存储引用计数,当引用计数超过一定值之后,runtime 会使用一张散列表(哈希表)来管理其引用计数;如果不使用 isa 存储引用计数则会直接存储到散列表中。

isa 指针

用64位(8字节)来存储一个内存地址显然是种浪费,于是可以将一部分的空间用来存储引用计数。当 isa 指针第一位为1时即表示使用优化的 isa 指针,这里列出64位环境下的 isa 结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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)
};

// SUPPORT_NONPOINTER_ISA
#endif

};

SUPPORT_NONPOINTER_ISA表示是否支持在 isa 指针内添加额外的信息,例如引用计数,析构状态,被__weak变量引用的情况等。目前仅支持 arm64架构的设备支持。

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

在64位环境下,isa 会存储引用计数,当 has_sidetable_rc 的值为1时,那么溢出的引用计数将会存储在一张全局散列表中,也就是引用计数 = isa保存的引用计数 + 哈希表保存的引用计数 + 1。后面会详细讲到。

哈希表 DenseMap

1
2
3
4
5
6
7
8
9
10
11
12
typedef objc::DenseMap<DisguisedPtr<objc_object>,size_t,true> RefcountMap;

template<typename KeyT, typename ValueT,
bool ZeroValuesArePurgeable = false,
typename KeyInfoT = DenseMapInfo<KeyT> >
class DenseMap
: public DenseMapBase<DenseMap<KeyT, ValueT, ZeroValuesArePurgeable, KeyInfoT>,
KeyT, ValueT, KeyInfoT, ZeroValuesArePurgeable>
{
// ...省略
}

runtime 使用 DenseMap 哈希表(也叫散列表,类似NSDictionary)的别名RefcountMap来存储引用计数。DenseMap 继承于 DenseMapBase 这个 C++ 类,通过观察 DenseMapBase 的内部实现我们可以发现以下几点:

  • 键 KeyT 的类型为DisguisedPtr<objc_object>,这个类是对objc_object *指针及其一些操作进行的封装,目的是不受内存泄漏工具leaks的检测
  • 值 ValueT 的类型为 size_t, size_t在64位环境下等同于 unsigned long。保存的值等于引用计数减一
  • 模板的 KeyInfoT 类型为 DenseMapInfo,在这里等同于DenseMapInfo<DisguisedPtr>。DenseMapInfo 封装了比较重要的方法,用于在哈希表中查找 key 映射的内容
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
template<typename T>
struct DenseMapInfo<T*> {
static inline T* getEmptyKey() {
uintptr_t Val = static_cast<uintptr_t>(-1);
return reinterpret_cast<T*>(Val);
}
static inline T* getTombstoneKey() {
uintptr_t Val = static_cast<uintptr_t>(-2);
return reinterpret_cast<T*>(Val);
}
static unsigned getHashValue(const T *PtrVal) {
return ptr_hash((uintptr_t)PtrVal);
}
static bool isEqual(const T *LHS, const T *RHS) { return LHS == RHS; }
};

指针哈希算法实现:

1
2
3
4
5
6
7
8
9
#if __LP64__
static inline uint32_t ptr_hash(uint64_t key)
{
key ^= key >> 4;
key *= 0x8a970be7488fda55;
key ^= __builtin_bswap64(key);
return (uint32_t)key;
}
#endif

虽然不完美,但是速度很快(注释说的。。。)


简单来讲,DenseMap 通过对象的指针地址来映射其引用计数

SideTable

1
2
3
4
5
6
struct SideTable {
spinlock_t slock;
RefcountMap refcnts;
weak_table_t weak_table;
// ...省略
}

介绍完存储引用计数的哈希表,那么这个哈希表是存储在哪里的呢?
答案是保存在一个叫做SideTable的结构体中,通过观察它的结构组成,我们可以可以看到有三个成员变量slock, refcntsweak_table

  • slock是一个自旋锁,保证线程安全
  • refcnts的类型是 RefcountMap,也就是上一节提到过的 DenseMap 类型的别名。用来保存引用计数
  • weak_table用来保存__weak修饰的指针。当一个对象 delloc 时,通过这个表将这些指向要释放对象的用__weak修饰的指针置为nil,避免野指针的情况出现。

StripedMap

知道引用计数的哈希表是保存在SideTable中,那么SideTable实例保存在哪里呢?
答案是在一个全局的StripedMap<SideTable *>类型的静态变量SideTableBuf

1
2
3
4
5
6
7
8
9
alignas(StripedMap<SideTable>) static uint8_t 
SideTableBuf[sizeof(StripedMap<SideTable>)];

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

之所以在初始化时将 SideTableBuf 定义成 uint8_t 是因为方便计算内存大小,在SideTables()方法中我们可以看到SideTableBuf会被强制转换成StripedMap<SideTable>*类型。实际上 SideTableBuf 也是哈希表,根据指针地址映射到相应的SideTable类型的变量。下面是StripedMap这个类的定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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;
}
// ...省略
}

StripedMap中有一个PaddedT类型的数组array,在模拟器中容量为64,在真机中为8。PaddedT结构体大小为64个字节,其成员变量 value 的类型实际是我们之前传入 SideTable。当系统调用SideTable& table = SideTables()[]时首先会执行SideTables()得到SideTableBuf, 然后在StripedMap中执行T& operator[] (const void *p)方法获取相应的SideTable。

1
2
3
4
5
6
7
8
T& operator[] (const void *p) { 
return array[indexForPointer(p)].value;
}

static unsigned int indexForPointer(const void *p) {
uintptr_t addr = reinterpret_cast<uintptr_t>(p);
return ((addr >> 4) ^ (addr >> 9)) % StripeCount;
}

indexForPointer()函数中返回相应 SideTable 的index。(addr >> 4) ^ (addr >> 9)这一步我也不是很懂,应该是类似于产生一个随机数,后面的% StripeCount返回一个 [0, StripeCount)的数,也就是相应 SideTable 的index。所以一个 SideTable 应该是对应许多的对象的。


保存引用计数的哈希表保存在SideTable结构体中,而SideTable保存在一个全局的静态变量StripedMap<SideTable> SideTableBuf中。在真机下,SideTableBuf能够储存8个SideTable实例。StripedMap的方法indexForPointer()通过对象的指针计算出相应 SideTable 的 index。一个 SideTable 对应多个对象

获取引用计数

在 ARC 环境下我们可以使用方法CFGetRetainCount得到对象的引用计数。在 runtime 中,通过调用objc_objectrootRetainCount()获取引用计数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
inline uintptr_t 
objc_object::rootRetainCount()
{
if (isTaggedPointer()) return (uintptr_t)this;

sidetable_lock();
isa_t bits = LoadExclusive(&isa.bits);
ClearExclusive(&isa.bits);
if (bits.nonpointer) {
uintptr_t rc = 1 + bits.extra_rc;
if (bits.has_sidetable_rc) {
rc += sidetable_getExtraRC_nolock();
}
sidetable_unlock();
return rc;
}

sidetable_unlock();
return sidetable_retainCount();
}
  1. isTaggedPointer在前面我们已经分析过了如果是Tagged Pointer类型的对象时是怎么样的。此时对象在栈中分配,由系统自动销毁内存(先进后出),所以此时对它求引用计数返回其地址。
    下面让我们重点看一下sidetable_retainCount()这个方法
  2. 当 isa 的 nonpointer = 1 的情况我们开头也分析过了,此时 isa 指针也用来存储引用计数,如果引用计数溢出则将溢出部分存储在哈希表中
  3. 下面让我们研究一下不使用isa优化是怎么从哈希表中获取引用计数的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
uintptr_t
objc_object::sidetable_retainCount()
{
SideTable& table = SideTables()[this];

size_t refcnt_result = 1;

table.lock();
RefcountMap::iterator it = table.refcnts.find(this);
if (it != table.refcnts.end()) {
// this is valid for SIDE_TABLE_RC_PINNED too
refcnt_result += it->second >> SIDE_TABLE_RC_SHIFT;
}
table.unlock();
return refcnt_result;
}
  1. 首先得到 SideTable 实例。
  2. 成员变量 refcnts 就是之前说的保存引用计数的哈希表,在哈希表中根据指针值查找引用计数。
  3. it->second >> SIDE_TABLE_RC_SHIFT 注意result从第三位才开始保存数据,所以需要将数据向右移动2位才能取到引用计数。第1位用来保存该对象是否被用__weak修饰的变量引用,第2位用来表示该对象是否正在析构
  4. 将右移后得到的数+1(refcnt_result)后返回。这也是为什么之前说哈希表保存的引用计数是实际值 -1 之后的值的原因。

Retain

在非 ARC 环境中可以使用retainrelease方法对引用计数进行加减操作,在 ARC 环境中我们无需也无法使用这两个方法操作引用计数,但是你可以使用CFRetain()对对象进行 retain 操作。最终会调用 objc_objectrootRetain方法

1
2
3
4
5
6
7
8
9
inline id 
objc_object::rootRetain()
{
assert(!UseGC);

if (isTaggedPointer()) return (id)this;
return sidetable_retain();
}

类似于上一节中获取引用计数的方法,当对象属于Tagged Pointer时则返回该对象。所以我们接着看sidetable_retain()方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
id objc_object::sidetable_retain()
{
#if SUPPORT_NONPOINTER_ISA
assert(!isa.nonpointer);
#endif
SideTable& table = SideTables()[this];

table.lock();
size_t& refcntStorage = table.refcnts[this];
if (! (refcntStorage & SIDE_TABLE_RC_PINNED)) {
refcntStorage += SIDE_TABLE_RC_ONE;
}
table.unlock();

return (id)this;
}

首先得到 SideTable 实例。从实例中得到存储引用技术的哈希表refcnts,在哈希表中根据对象的地址找到对应的引用计数refcntStorage,判断引用计数的值是否有溢出,如果没有则对引用计数 + 1,返回对象。
上一节我们讲过 refcntStorage 中第三位才开始用来存储引用计数,所以读数时需要先往右边移动两位,那为什么这里的代码没有呢?

1
2
3
4
5
6
#define SIDE_TABLE_WEAKLY_REFERENCED (1UL<<0)
#define SIDE_TABLE_DEALLOCATING (1UL<<1) // MSB-ward of weak bit
#define SIDE_TABLE_RC_ONE (1UL<<2) // MSB-ward of deallocating bit
#define SIDE_TABLE_RC_PINNED (1UL<<(WORD_BITS-1))

#define SIDE_TABLE_RC_SHIFT 2

注意观察SIDE_TABLE_RC_ONE的定义,是一个8字节的 unsigned long 类型,值为1,向左偏移了两位。refcntStorage += SIDE_TABLE_RC_ONE两者相加的话则直接从第三位开始相加了,所以可以使用 SIDE_TABLE_RC_ONE 对引用计数进行 +1 和 -1 操作。
同样的,在上面的代码中, SIDE_TABLE_RC_PINNED用来判断引用计数值是否有溢出。

Release

release 最终会调用 objc_object的方法rootRelease()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
inline bool 
objc_object::rootRelease()
{
if (isTaggedPointer()) return false;
return sidetable_release(true);
}

uintptr_t objc_object::sidetable_release(bool performDealloc)
{
#if SUPPORT_NONPOINTER_ISA
assert(!isa.nonpointer);
#endif
SideTable& table = SideTables()[this];

bool do_dealloc = false;

table.lock();
RefcountMap::iterator it = table.refcnts.find(this);
if (it == table.refcnts.end()) {
do_dealloc = true;
table.refcnts[this] = SIDE_TABLE_DEALLOCATING;
} else if (it->second < SIDE_TABLE_DEALLOCATING) {
// SIDE_TABLE_WEAKLY_REFERENCED may be set. Don't change it.
do_dealloc = true;
it->second |= SIDE_TABLE_DEALLOCATING;
} else if (! (it->second & SIDE_TABLE_RC_PINNED)) {
it->second -= SIDE_TABLE_RC_ONE;
}
table.unlock();
if (do_dealloc && performDealloc) {
((void(*)(objc_object *, SEL))objc_msgSend)(this, SEL_dealloc);
}
return do_dealloc;
}

在这个方法你可以知道为什么哈希表中保存的引用计数是实际值 -1 之后的值。
it->second < SIDE_TABLE_DEALLOCATING用来判断保存的引用计数值是否小于1,如果小于1的话则对该值标记为正在析构:it->second |= SIDE_TABLE_DEALLOCATING;,并且在随后对该对象发送 delloc 消息。
举个例子,一个对象 sark,实际的引用计数为1,在哈希表中保存的值为0,当这个对象进行release操作后,sark 的引用计数变成了0,也就是需要进行销毁操作了。而到了该方法中,会判断保存的引用计数的值是否小于1,如果是的话则进行 delloc 操作,并且将哈希表中存储的值标记为正在析构状态。而 sark 原先保存着的引用计数值就是 =0,这样设计避免了在哈希表存储的引用计数出现负数的情况。

alloc,new, copy 和 mutableCopy

copy 以及 mutableCopyNSCopyingNSMutableCopying 协议上的方法,需要在各类上自己去实现copyWithZone: mutableCopyWithZone:方法。无论是深拷贝还是浅拷贝都会增加引用计数。

1
2
3
4
5
6
+ (id)new {
return [callAlloc(self, false/*checkNil*/) init];
}
+ (id)alloc {
return _objc_rootAlloc(self);
}

[cls alloc]以及[cls allocWithZone:nil]方法最终会调用callAlloc()方法,所以 alloc 和 new 这两个方法后面都会调用callAlloc()这个方法,因为 Objective-C 2.0 忽视垃圾回收和 NSZone,那么后续的调用顺序依次是为:

1
2
3
4
callAlloc()
class_createInstance()
_class_createInstanceFromZone
calloc()

calloc()函数相比于malloc()函数的优点是它将分配的内存区域初始化为0,相当于malloc()后再用memset()方法初始化一遍。

单例

其实这一节是对上一节内容的补充。
记得我刚出来工作的时候,单例是这样写的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@implementation Son
+ (instancetype)shareManager
{
static Son *son;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
son = [super allocWithZone:nil];
});
return son;
}
+ (instancetype)allocWithZone:(struct _NSZone *)zone
{
return [self shareManager];
}
@end

当时组长问我为什么要这样子写(因为跟他们写的方式不一样),我也答不上来,因为这种代码都是直接google的。但是看了callAlloc()实现之后我明白为什么了。
在上一节我们已经知道了 alloc 和 new 都会接着调用callAlloc()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
static ALWAYS_INLINE id
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
if (slowpath(checkNil && !cls)) return nil;

#if __OBJC2__
if (fastpath(!cls->ISA()->hasCustomAWZ())) {
// No alloc/allocWithZone implementation. Go straight to the allocator.
// fixme store hasCustomAWZ in the non-meta class and
// add it to canAllocFast's summary
if (fastpath(cls->canAllocFast())) {
// No ctors, raw isa, etc. Go straight to the metal.
bool dtor = cls->hasCxxDtor();
id obj = (id)calloc(1, cls->bits.fastInstanceSize());
if (slowpath(!obj)) return callBadAllocHandler(cls);
obj->initInstanceIsa(cls, dtor);
return obj;
}
else {
// Has ctor or raw isa or something. Use the slower path.
id obj = class_createInstance(cls, 0);
if (slowpath(!obj)) return callBadAllocHandler(cls);
return obj;
}
}
#endif

// No shortcuts available.
if (allocWithZone) return [cls allocWithZone:nil];
return [cls alloc];
}

如果类重载了allocWithZone方法,那么cls->ISA()->hasCustomAWZ()将会返回YES,也就是说当我们用alloc或者new创建实例的时候,就不会走系统的方法,而会走重载的allocWithZone方法了。我们在重载allocWithZone方法时返回[self shareManager](注意此时的self代表Son类), 因为shareManager方法返回的是一个静态变量。

还有一个需要注意的点就是在shareManager中,我们使用son = [super allocWithZone:nil];初始化实例,为什么不使用son = [[super alloc] init];来初始化呢?
代码中的[super alloc];在编译后会变成objc_msgSendSuper(objc_super super, @selector(alloc))(大致意思是这样)。其中objc_super是一个结构体,只有两个成员变量id receiverClass class,receiver 仍是 self(Son类), class 为 Father类。当我们想通过[super alloc]创建实例的时候,会从 Father类中查找 +alloc 方法,如果没有实现则在 NSObject 中查找 +alloc 方法。而方法里面的参数 self 仍旧为 Son 类而不是 Father 类,所以还是会去调用重载的allocWithZone方法,导致死循环。

引用

Objective-C 引用计数原理