0%

OC内存管理之对象的引用计数

源码版本:objc4-781

我们都知道OC的内存管理是通过引用计数来管理的,对象刚创建时引用计数为1,retain后+1,release后-1,当引用计数减为0时就会被销毁。那么问题就来了,操作的这个引用计数值是保存在哪的?今天就来研究一下这个问题。

先说结论:早期OC对象的引用计数都是存储在引用计数表中的,但是后来Apple的工程师可能觉得每次都从表中查找修改效率有点低,于是做了一些优化,如果引用计数值较小时就保存在 struct objc_object 的isa成员变量里,只有当isa里装不下时才存放到引用计数表里,这一点可以从isa的类型变迁看出。除此之外,苹果还引入了Tagged pointer对象,这种对象实际上已经没有在堆上分配内存了,它的值就保存在指针里,这样的对象由于并没有真正在堆上分配内存因此讨论它的引用计数也就没有什么意义了。

之前的文章 runtime源码分析(一)--类型 里专门分析了 struct objc_objectstruct objc_classisa 类型的定义,这里不再赘述,仅做简单说明。

isa的定义

旧版本的定义:

isa是一个指针类型 struct objc_class *

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
/// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;

/// Represents an instance of a class.
struct objc_object {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
};

struct objc_class {
Class _Nonnull isa OBJC_ISA_AVAILABILITY; //旧版本isa为一个指针指向objc_class结构体

#if !__OBJC2__
Class _Nullable super_class OBJC2_UNAVAILABLE;
const char * _Nonnull name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list * _Nullable ivars OBJC2_UNAVAILABLE;
struct objc_method_list * _Nullable * _Nullable methodLists OBJC2_UNAVAILABLE;
struct objc_cache * _Nonnull cache OBJC2_UNAVAILABLE;
struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE;
#endif

} OBJC2_UNAVAILABLE;
/* Use `Class` instead of `struct objc_class *` */

新版本oc 2.0定义:

isa是一个联合类型 union isa_t

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
69
70
71
72
//objc-private.h

struct objc_object {
private:
isa_t isa; //objc_object中有一个isa,但类型已经变成isa_t(union类型)

public:

// ISA() assumes this is NOT a tagged pointer object
Class ISA();

// rawISA() assumes this is NOT a tagged pointer object or a non pointer ISA
Class rawISA();

// getIsa() allows this to be a tagged pointer object
Class getIsa();

uintptr_t isaBits() const;

...
}

union isa_t {
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }

Class cls;
uintptr_t bits;
#if defined(ISA_BITFIELD)
struct {
ISA_BITFIELD; // defined in isa.h
};
#endif
};

//展开isa_t
union isa_t {
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }

Class cls; //一个指针指向objc_class结构体。这样就可以兼容旧版本。
uintptr_t bits;
//这里展开的是arm64的
struct {
uintptr_t nonpointer : 1; //LSB。0 is raw isa, 1 is non-pointer isa.
uintptr_t has_assoc : 1; //对象是否有关联对象
uintptr_t has_cxx_dtor : 1; //对象是否有C++或ARC析构函数
uintptr_t shiftcls : 33; /*MACH_VM_MAX_ADDRESS 0x1000000000*/ //Class地址。
uintptr_t magic : 6; //一个魔法数。用于区分是否初始化
uintptr_t weakly_referenced : 1; //是否有弱引用指针。
uintptr_t deallocating : 1; //是否正在被销毁
uintptr_t has_sidetable_rc : 1; //是否有sidetable引用计数。对象的引用计数太大而不能在内部保存时会将引用计数保存到sidetable
uintptr_t extra_rc : 19 //MSB。对象的引用计数超过1时会存在这里。因此extra_rc加1后就是对象的引用计数。由于只有19位所以只有对象的引用计数不太大的时候才保存在这里
};
};

# if __arm64__
# 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) //刚好位于extra_rc的最低位,因此+RC_ONE,就表示extra_rc+1
# define RC_HALF (1ULL<<18)

上面的注释已经写得很明白了,至于isa为什么可以用结构体位域定义,主要是因为在64位系统中指针的长度为8个字节,64位,而类的地址通常占不满64位导致会空出许多bit,于是苹果的工程师充分利用这些bit,极大的提高了引用计数的操作效率。(至于为啥类的地址达不到64位,我也不知道,从上面的定义看应该只有33位有效位。)

接下来看一下retain和release的实现,进一步验证。

retain

源码如下:

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
69
70
71
72
73
74
75
76
77
78
79
80
// Replaced by ObjectAlloc
- (id)retain {
return _objc_rootRetain(self);
}

NEVER_INLINE id
_objc_rootRetain(id obj)
{
ASSERT(obj);

return obj->rootRetain();
}

ALWAYS_INLINE id
objc_object::rootRetain()
{
return rootRetain(false, false);
}

NEVER_INLINE id
objc_object::rootRetain_overflow(bool tryRetain)
{
return rootRetain(tryRetain, true);
}

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;
oldisa = LoadExclusive(&isa.bits);
newisa = oldisa;
if (slowpath(!newisa.nonpointer)) { //纯pointer的则调用sidetable_retain,
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;
}
uintptr_t carry;
newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry); // extra_rc++

if (slowpath(carry)) {
// newisa.extra_rc++ overflowed
if (!handleOverflow) {
ClearExclusive(&isa.bits);
return rootRetain_overflow(tryRetain);
}
// Leave half of the retain counts inline and
// prepare to copy the other half to the side table.
if (!tryRetain && !sideTableLocked) sidetable_lock();
sideTableLocked = true;
transcribeToSideTable = true;
newisa.extra_rc = RC_HALF;
newisa.has_sidetable_rc = 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.
sidetable_addExtraRC_nolock(RC_HALF);
}

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

大致流程:

  1. 首先判断是不是TaggedPointer对象,如果是则直接返回。上面也说了TaggedPointer对象并没有真正在堆上分配内存存储对象,所以这些retain,release操作都是无意义的。
  2. 如果isa是纯pointer类型则调用sidetable_retain,查询到对象的引用计数表后+1。另:slowpath表示该条件一般不会成立,用于编译器优化。
  3. 如果是tryRetain并且正在销毁则返回nil,即tryRetain失败。这里稍微提一句 objc_loadWeakRetained 函数里面有调用 obj->rootTryRetain() ,如果tryRetain失败, objc_loadWeakRetained 会返回nil。
  4. 如果isa是nonpointer,则调用addc(newisa.bits, RC_ONE, 0, &carry) ,extra_rc++。由于extra_rc只有19bit所以需要处理进位的问题。同样进位也不是经常发生,所以进位的处理也是slowpath。
  5. 处理进位情况,当发生进位时,就需要把一部分引用计数加到引用计数表里去了,所以先加好锁sidetable_lock。更新transcribeToSideTable为true表示需要将一部分引用计数加到sidetable,更新isa.extra_rc=RC_HALF, 更新has_sidetable_rc为true表明使用到了引用计数表。

从retain的处理逻辑不难看出,引用计数值的保存与操作是有经过优化的。接下来看一下sidetable_retain的操作。

sidetable_retain

源码:

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

static objc::ExplicitInit<StripedMap<SideTable>> SideTablesMap;

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

大致流程:

  1. 将对象地址映射为索引,然后从SideTables哈希数组里取得对象的SideTable
  2. 将对象地址映射为索引,从SideTable的refcnts哈希数组里取得当前引用计数值
  3. +1

这里稍微介绍下refcnts:

1
2
3
4
5
6
7
8
9
10
11
struct SideTable {
spinlock_t slock;
RefcountMap refcnts; //指向一个引用计数表
weak_table_t weak_table; //指向一个弱引用表

//方法
...
}
// RefcountMap disguises its pointers because we
// don't want the table to act as a root for `leaks`.
typedef objc::DenseMap<DisguisedPtr<objc_object>,size_t,RefcountMapValuePurgeable> RefcountMap;

它是SideTable的一个成员变量,是一个哈希表DenseMap,DenseMap继承自DenseMapBase。DenseMapBase里实现了扩容操作,所以不用担心引用计数表容量不够的情况。

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
class DenseMapBase {
...

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();
if (LLVM_UNLIKELY(NewNumEntries * 4 >= NumBuckets * 3)) {
this->grow(NumBuckets * 2); //扩容
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.
incrementNumEntries();
} else if (KeyInfoT::isEqual(TheBucket->getFirst(), getTombstoneKey())) {
// Replacing a tombstone.
incrementNumEntries();
decrementNumTombstones();
} else {
// we should be purging a zero. No accounting changes.
ASSERT(ValueInfoT::isPurgeable(TheBucket->getSecond()));
TheBucket->getSecond().~ValueT();
}

return TheBucket;
}

}

如果对SideTablesMap感兴趣可以参考

iOS SideTable

这里简单说一下,SideTablesMap就是一个全局的哈希表,它的元素个数是固定的8个或64个,里面装的就是SideTable对象。

release

实现:

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
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
// Replaced by ObjectAlloc
- (oneway void)release {
_objc_rootRelease(self);
}

NEVER_INLINE void
_objc_rootRelease(id obj)
{
ASSERT(obj);

obj->rootRelease();
}

ALWAYS_INLINE bool
objc_object::rootRelease()
{
return rootRelease(true, false);
}

ALWAYS_INLINE bool
objc_object::rootRelease(bool performDealloc, bool handleUnderflow)
{
if (isTaggedPointer()) return false; //TaggedPointer对象不处理

bool sideTableLocked = false;

isa_t oldisa;
isa_t newisa;

retry:
do {
oldisa = LoadExclusive(&isa.bits);
newisa = oldisa;
if (slowpath(!newisa.nonpointer)) {
ClearExclusive(&isa.bits);
if (rawISA()->isMetaClass()) return false;
if (sideTableLocked) sidetable_unlock();
return sidetable_release(performDealloc);
}
// don't check newisa.fast_rr; we already called any RR overrides
uintptr_t carry;
newisa.bits = subc(newisa.bits, RC_ONE, 0, &carry); // extra_rc--
if (slowpath(carry)) {
// don't ClearExclusive()
goto underflow;
}
} while (slowpath(!StoreReleaseExclusive(&isa.bits,
oldisa.bits, newisa.bits)));

if (slowpath(sideTableLocked)) sidetable_unlock();
return false;

underflow:
// newisa.extra_rc-- underflowed: borrow from side table or deallocate

// abandon newisa to undo the decrement
newisa = oldisa; //发生下溢时就保持isa不变了。

if (slowpath(newisa.has_sidetable_rc)) {
if (!handleUnderflow) {
ClearExclusive(&isa.bits);
return rootRelease_underflow(performDealloc);
}

// Transfer retain count from side table to inline storage.

if (!sideTableLocked) {
ClearExclusive(&isa.bits);
sidetable_lock();
sideTableLocked = true;
// Need to start over to avoid a race against
// the nonpointer -> raw pointer transition.
goto retry;
}

// Try to remove some retain counts from the side table.
size_t borrowed = sidetable_subExtraRC_nolock(RC_HALF);

// To avoid races, has_sidetable_rc must remain set
// even if the side table count is now zero.

if (borrowed > 0) {
// Side table retain count decreased.
// Try to add them to the inline count.
newisa.extra_rc = borrowed - 1; // redo the original decrement too
bool stored = StoreReleaseExclusive(&isa.bits,
oldisa.bits, newisa.bits);
if (!stored) {
// Inline update failed.
// Try it again right now. This prevents livelock on LL/SC
// architectures where the side table access itself may have
// dropped the reservation.
isa_t oldisa2 = LoadExclusive(&isa.bits);
isa_t newisa2 = oldisa2;
if (newisa2.nonpointer) {
uintptr_t overflow;
newisa2.bits =
addc(newisa2.bits, RC_ONE * (borrowed-1), 0, &overflow);
if (!overflow) {
stored = StoreReleaseExclusive(&isa.bits, oldisa2.bits,
newisa2.bits);
}
}
}

if (!stored) {
// Inline update failed.
// Put the retains back in the side table.
sidetable_addExtraRC_nolock(borrowed);
goto retry;
}

// Decrement successful after borrowing from side table.
// This decrement cannot be the deallocating decrement - the side
// table lock and has_sidetable_rc bit ensure that if everyone
// else tried to -release while we worked, the last one would block.
sidetable_unlock();
return false;
}
else {
// Side table is empty after all. Fall-through to the dealloc path.
}
}

// Really deallocate.

if (slowpath(newisa.deallocating)) {
ClearExclusive(&isa.bits);
if (sideTableLocked) sidetable_unlock();
return overrelease_error();
// does not actually return
}
newisa.deallocating = true;
if (!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)) goto retry;

if (slowpath(sideTableLocked)) sidetable_unlock();

__c11_atomic_thread_fence(__ATOMIC_ACQUIRE);

if (performDealloc) {
((void(*)(objc_object *, SEL))objc_msgSend)(this, @selector(dealloc));
}
return true;
}

代码很多但逻辑并不复杂:

  1. 将引用计数-1
  2. 当引用计数减为“0”时,调用对象的dealloc方法。

为啥这里0打了引号,这是因为对象刚创建出来不管是引用计数表里的值还是extra_rc的值其实都是0,这时候调用release,那就是0-1会发生underflow,所以这种情况下实际上要处理underflow,所以源码里有注释:

1
newisa.extra_rc-- underflowed: borrow from side table or deallocate

因此实际上是引用计数先-1,当发生下溢时可能就会调用deallocate,当然这种情况下也就不更新引用计数值为负数了。

sidetable_release

实现:

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
// rdar://20206767
// return uintptr_t instead of bool so that the various raw-isa
// -release paths all return zero in eax
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();
auto it = table.refcnts.try_emplace(this, SIDE_TABLE_DEALLOCATING);
auto &refcnt = it.first->second;
if (it.second) {
do_dealloc = true;
} else if (refcnt < SIDE_TABLE_DEALLOCATING) {
// SIDE_TABLE_WEAKLY_REFERENCED may be set. Don't change it.
do_dealloc = true;
refcnt |= SIDE_TABLE_DEALLOCATING;
} else if (! (refcnt & SIDE_TABLE_RC_PINNED)) {
refcnt -= SIDE_TABLE_RC_ONE;
}
table.unlock();
if (do_dealloc && performDealloc) {
((void(*)(objc_object *, SEL))objc_msgSend)(this, @selector(dealloc));
}
return do_dealloc;
}

retainCount

实现:

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
- (NSUInteger)retainCount {
return _objc_rootRetainCount(self);
}

uintptr_t
_objc_rootRetainCount(id obj)
{
ASSERT(obj);

return obj->rootRetainCount();
}

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; //加1后才是真正的引用计数,因为对象刚创建时extra_rc为0
if (bits.has_sidetable_rc) {
rc += sidetable_getExtraRC_nolock();
}
sidetable_unlock();
return rc;
}

sidetable_unlock();
return sidetable_retainCount(); //纯pointer的情况直接从sidetable获取
}

sidetable_retainCount

实现:

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; //默认是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;
}

总结

如果对象是一个nonpointer对象,则引用计数较小时会直接保存在对象的isa成员变量里,只有当isa里装不下时才存放到引用计数表里,引用计数表是一个哈希表key为对象的地址,value就是引用计数值。

如果对象是一个pointer对象,则引用计数就存储在引用计数表里。

本文没有对上溢和下溢的处理细节做过多分析,有兴趣的可以细细研究。

觉得文章有帮助可以打赏一下哦!