我正在参与掘金创作者训练营第4期,点击了解活动详情,一起学习吧!
在iOS开发中,OC和Swift都是通过ARC进行内存管理的,本篇文章我们将一起探索Swift中的引用计数。
三种引用计数
在Swift对象一文中,我们探索了Swift对象的本质,其数据结构如下,里面的InlineRefCounts为该对象的引用计数值。
#define SWIFT_HEAPOBJECT_NON_OBJC_MEMBERS \
InlineRefCounts refCounts
struct HeapObject {
/// This is always a valid pointer to a metadata object.
HeapMetadata const *metadata; // 1
SWIFT_HEAPOBJECT_NON_OBJC_MEMBERS; // 2
}
先来看看苹果的官方是来怎么来介绍引用计数规则的:
通常来说,一个Swift对象有3种引用计数,存储在紧挨着对象isa的地方或者isa指向的side table entry。
-
1,
stong RC:强引用计数,计算强引用的计数。 当strong RC等于0的时候,对象就会deinit,使用无主引用访问该对象,就会发生错误。使用弱引用访问该对象会返回nil。 -
2,
unowned RC:无主引用计数,计算无主引用计数,无主引用计数会额外+1,来代表强引用计数,在 对象deinit完成后,会自动-1。当无主引用计数等于0的时候,该对象的内存空间,已经被释放。 -
3,
weak RC:弱引用计数,计算弱引用的计数,弱引用计数会额外+1,代表无主引用计数值,当对象的内存空间被释放后,会自动-1,当弱引用计数值为0,这个对象的side table entry已经被释放。
对象在初始化的时候,是没有side table的,当出现以下情况的时候,就会有side table。
- 1,有一个
弱引用指向它。 - 2,
强引用计数或无主引用计数在数值溢出的时候(小于32位). - 3,对象有
关联对象。
获取side table entry是一个单向操作,这样可以保证不会丢失,且防止了一些线程竞争的问题。
其内存结构如下
HeapObject {
isa
InlineRefCounts {
atomic<InlineRefCountBits> {
strong RC + unowned RC + flags
OR
HeapObjectSideTableEntry*
}
}
}
HeapObjectSideTableEntry {
SideTableRefCounts {
object pointer
atomic<SideTableRefCountBits> {
strong RC + unowned RC + weak RC + flags
}
}
}
没有使用side table entry,称为InlineRefCount。使用side table entry存储引用计数的称为SideTableRefCounts
这段话摘自Swift源码的注释。
接下来,我们结合源码来探索Swift的引用计数管理。
源码分析
强引用计数和无主引用计数
当初始化Swift对象的时候,在refCounts函数中,会初始化引用计数值
LLVM_ATTRIBUTE_ALWAYS_INLINE
constexpr
RefCountBitsT(uint32_t strongExtraCount, uint32_t unownedCount)
: bits((BitsType(strongExtraCount) << Offsets::StrongExtraRefCountShift) |
(BitsType(1) << Offsets::PureSwiftDeallocShift) |
(BitsType(unownedCount) << Offsets::UnownedRefCountShift))
{ }
传入两个参数强引用计数(0)和无主引用(1),通过位运算,将数值存放到对应的空间上。
对应的偏移数值如下所示
根据内存偏移量,我们可以分析出,引用计数的存储规则如图所示
- 1,第
0位,用来标记是否是纯Swift对象。 - 2,第
1~31位,存储无主引用计数。 - 3,第
32位,表示是否是deiniting状态。 - 4,第
33~62位,存储强引用计数。 - 5,第
63位,标记是否有散列表,若有弱引用该位值为1。
接下来,我们使用lldb来动态查看其内存分布。
lldb调试
有如下代码
import Foundation
class Person {
var age = 10
}
var p: Person? = Person()
//1
var p1 = p
// 2
var p2 = p
// 3
unowned var p3 = p
// 4
print(p1?.age)
在断点1处,我们先输出p变量的内存地址,然后查看内存分布情况
(lldb)p Unmanaged.passUnretained(p!)
(Unmanaged<LYCSwift.Person>) $R0 = {
_value = 0x0000000101f1d180 (age = 10)
}
(lldb)x/8gx 0x0000000101f1d180
0x101f1d180: 0x00000001000081f8 0x0000000000000003
0x101f1d190: 0x000000000000000a 0x0000000000000023
0x101f1d1a0: 0x0000000000000000 0x0000000000000000
0x101f1d1b0: 0x0000000000000002 0x00020000101f1f3e
此时refCounts的值为0x0000000000000003,通过计算器将其转化为二进制
前两位分别是1,其他位是0,就代表着此时
为纯swift对象,强引用计数为0,无主引用计数为1
咦?等等......
这里和我们平常理解的不一样,OC对象初始化完成后,isa指针的extra_rc值为1,其强引用计数值为1,为什么swift对象初始化完成,里面强引用计数值为0了?
翻了下苹果的注释,前面已经提到了无主引用会额外+1,代表强引用计数,所以此时存储的强引用计数值为0。
我们接着往下看: 在断点3处
(lldb)x/8gx 0x0000000101f1d180
0x101f1d180: 0x00000001000081f8 0x0000000400000003
0x101f1d190: 0x000000000000000a 0x0000000000000023
0x101f1d1a0: 0x0000000000000000 0x0000000000000000
0x101f1d1b0: 0x0000000000000002 0x00020000101f1f3e
经过两个强引用p1、p2后,此时,强引用计数值为0x10即2,无主引用计数值为0x1即1。
在断点4处
(lldb)x/8gx 0x0000000101f1d180
0x101f1d180: 0x00000001000081f8 0x0000000400000005
0x101f1d190: 0x000000000000000a 0x0000000000000023
0x101f1d1a0: 0x0000000000000000 0x0000000000000000
0x101f1d1b0: 0x0000000000000002 0x00020000101f1f3e
经过
unown修饰的变量引用后,会使无主引用计数值+1,此时 无主引用计数为2,强引用计数为 2。
弱引用计数
当我们使用 weak var p4 = p时,就会产生成一个WeakReference对象,并且此时对象的引用计数类型也会发生变化,会由InlineRefCounts转变为SideTableRefCounts。
此时HeapObject的结构为
HeapObject {
isa
InlineRefCounts {
atomic<InlineRefCountBits> {
HeapObjectSideTableEntry*
}
}
}
class HeapObjectSideTableEntry {
std::atomic<HeapObject*> object;
SideTableRefCounts refCounts;
}
class alignas(sizeof(void*) * 2) SideTableRefCountBits : public RefCountBitsT<RefCountNotInline>
{
uint32_t weakBits;
}
我们通过查看运行时的汇编代码,我们发现会执行swift_weakInit函数,来处理弱引用
其内部实现如下
WeakReference *swift::swift_weakInit(WeakReference *ref, HeapObject *value) {
ref->nativeInit(value);
return ref;
}
主要实现代码
template <>
HeapObjectSideTableEntry* RefCounts<InlineRefCountBits>::allocateSideTable(bool failIfDeiniting)
{
auto oldbits = refCounts.load(SWIFT_MEMORY_ORDER_CONSUME); // 1
// Preflight failures before allocating a new side table.
if (oldbits.hasSideTable()) {
// Already have a side table. Return it.
return oldbits.getSideTable(); // 2
}else if (failIfDeiniting && oldbits.getIsDeiniting()) {
// Already past the start of deinit. Do nothing.
return nullptr;
}
// Preflight passed. Allocate a side table.
// FIXME: custom side table allocator**
HeapObjectSideTableEntry *side = new HeapObjectSideTableEntry(getHeapObject());
auto newbits = InlineRefCountBits(side);
do {
if (oldbits.hasSideTable()) {
// Already have a side table. Return it and delete ours.
// Read before delete to streamline barriers.
auto result = oldbits.getSideTable();
delete side;
return result;
failIfDeiniting && oldbits.getIsDeiniting()) {
// Already past the start of deinit. Do nothing.
return nullptr;
}
side->initRefCounts(oldbits); // 3
} while (! refCounts.compare_exchange_weak(oldbits, newbits,std::memory_order_release,
std::memory_order_relaxed));
return side;
}
// 获取Side Table
LLVM_ATTRIBUTE_ALWAYS_INLINE
HeapObjectSideTableEntry *getSideTable() const {
assert(hasSideTable());
// Stored value is a shifted pointer.
return reinterpret_cast<HeapObjectSideTableEntry *>
(uintptr_t(getField(SideTable)) << Offsets::SideTableUnusedLowBits);
}
- 1,获取
InlineRefcounts oldbit,通过标志位判断是否已存在HeapObjectSideTableEntry。 - 2,如果有
SideTable,通过HeapObjectSideTableEntry *地址指针,左移3位,即可得到HeapObjectSideTableEntry对象值。 - 3,将
InlineRefCounts中的强引用计数和无主引用计数存入散列表中。
得到HeapObjectSideTableEntry之后,在进行 ide->incrementWeak();对32位的弱引用计数+1。
此时HeapObjectSideTableEntry和 HeapObject的关系如下图所示
lldb调试
我们看下如下代码的引用计数变化
// 强引用计数 无主引用 弱引用
var s: Person? = Person() // 0 1
var s1 = s // 1 1
var s2 = s1 // 2 1
print(2)
weak var s3 = s2 // 2 1 2
print(4)
weak var s4 = s3 // 2 1 3
print(s4?.age)
// 后面的数值分别代表: 强引用计数 无主引用计数 和 弱引用计数
在print(2)函数处,我们看下引用计数
(lldb)p Unmanaged.passUnretained(s!)
(Unmanaged<LYCSwift.Person>) $R0 = {
_value = 0x000000010384e8f0 (age = 10)
}
(lldb)x/8gx 0x000000010384e8f0
0x10384e8f0: 0x0000000100008200 0x0000000400000003
0x10384e900: 0x000000000000000a 0x0000000000000023
0x10384e910: 0x0000000080080000 0x0000000100a4f3a8
0x10384e920: 0x0000000000000000 0x0000000100a3dce8
此时,
强引用计数为2,无主引用计数为1。
在print(4)处
(lldb) x/8gx 0x000000010384e8f0
0x10384e8f0: 0x0000000100008200 0xc000000020748016
0x10384e900: 0x000000000000000a 0x0000000000000023
0x10384e910: 0x0000000080080000 0x0000000100a4f3a8
0x10384e920: 0x0000000000000000 0x0000000100a3dce8
0xc000000020748016为HeapObjectSideTableEntry的指针地址,将其左移3位得到地址0x103A400B0,就得到了 HeapObjectSideTableEntry的值,我们读取该值
(lldb)x/8gx 0x103A400B0
0x103a400b0: 0x000000010384e8f0 0x0000000000000000
0x103a400c0: 0x0000000400000003 0x0000000000000002
0x103a400d0: 0x0000000000000000 0x0000000000000000
0x103a400e0: 0x00007fff84990008 0x00007fff84995200
0x000000010384e8f0是HeapObject的内存指针,0x0000000400000003存储的是强引用计数和弱引用计数。
0x0000000000000002前32位存储的是弱引用计数.
此时,强引用,无主引用和弱引用计数值分别为2 1 2,弱引用计数会额外+1,代表无主引用计数值。
⚠️⚠️⚠️ 注意:为什么会额外+1,可以看文章的开始部分的苹果注释。
在print(s4?.age)处
(lldb) x/8gx 0x103A400B0
0x103a400b0: 0x000000010384e8f0 0x0000000000000000
0x103a400c0: 0x0000000400000003 0x0000000000000003
0x103a400d0: 0x0000000100a4f2e8 0x0000000100000003
0x103a400e0: 0x08d0eef4ea1dadab 0x08d0eef4ea1dadab
此时,强引用,无主引用和弱引用计数值分别为2 1 3
总结
Swift的引用计数有三种,强引用计数、无主引用计数和弱引用计数。当对象初始化的时候强引用计数为0,无主引用为 1, 当第一次使用weak修饰的时候,弱引用计数值为2,原因在文章的开头有阐述。
如果觉得有收获请按如下方式给个
爱心三连:👍:点个赞鼓励一下。🌟:收藏文章,方便回看哦!。💬:评论交流,互相进步!。