Swift是使用引用计数来进行内存管理,本文将从refCount结构进行深入分析,进而对强引用,弱引用,循环引用进行分析
refCount结构分析
-
在 Swift进阶-类&对象&属性 中我们分析得知
refCount是用来记录引用计数,下面从一个案例中来查看refCount:class WSPerson { var age: Int = 18 } var ws = WSPerson() var ws1 = ws var ws2 = ws- 运行打印
refCount结果如下:
- 得到的
refCount并不是一个数,像是两个数的组合,具体的结构就得去 Swift源码 分析
- 运行打印
-
在 Swift进阶-类&对象&属性 中,我们在源码中得知
HeapObject的构造中有refCounts:- 可以看到
refCounts的构造方法都是通过InlineRefCountBits来调用相关的方法进行的,所以此处可以真正起作用的是InlineRefCountBits
- 可以看到
-
在查看
InlineRefCountBits:typedef RefCounts<InlineRefCountBits> InlineRefCounts;InlineRefCountBits是此处RefCounts中传入的类型,那么此时核心就变成了RefCounts
-
继续跟踪
RefCounts,得到它是一个模版类,在运行时真正起作用的是传入的类型:- 通过对
RefCounts类的阅读发现里面起作用的是RefCountBits,也就是说我们研究的核心实质是传入的InlineRefCountBits
- 通过对
-
在进入
InlineRefCountBits阅读发现它也是个模版类型:typedef RefCountBitsT<RefCountIsInline> InlineRefCountBits;-
下面再来看看
RefCountIsInline是什么:enum RefCountInlinedness { RefCountNotInline = false, RefCountIsInline = true };RefCountIsInline是RefCountInlinedness类型的枚举,RefCountIsInline代表true,RefCountNotInline代表false
-
于是再来分析
RefCountBitsT,它也是个模版类:- 分析得知
bits是它的成员变量,它的类型为uint64_t占用8字节 RefCountBitsT的初始化都是通过内存平移来实现的:
- 而内存平移的核心方法是
Offsets,它是RefCountBitOffsets<sizeof(BitsType)>的别名,再继续点击进入RefCountBitOffsets可以查看所有的位移的信息:
- 根据位移信息我们可以得到
64位下的refCount的内存分布:
UnownedRefCount:是无主引用(Unowned)的引用计数StrongExtraRefCount:是强引用的引用计数
- 分析得知
-
-
拿到
refCount的内存分布后,我们再回到前面的案例,这明显是一个强引用,而强引用在refCount中占用的是33~62位,所以refCount的内存0x0000000600000003中,强引用个数的是3,可以通过计算器验证:
强引用
-
下面来分析下强引用,首先来看下没有强引用的案例
Sil文件分析:class WSPerson { var age: Int = 18 } var ws = WSPerson()
Sil分析
-
生成的
Sil文件主要内容如下: -
再添加强引用
var ws1 = ws -
然后生成
Sil文件- 通过阅读
Sil文件得知,强引用是读取%3的内存,并调用copy_addr函数拷贝一份,并存储到地址%9中 - 在 Swift Intermediate Language 文档中有对
copy_addr的解释:
- 在文档中,
copy_addr相当于做了一下几件事:-
load内存%0,并赋值给%new
-
- 对
%new进行strong_retain
- 对
-
- 将
%new存储到%1
- 将
-
- 通过阅读
-
再去汇编查看,发现强引用核心调用的方法是
swift_retain:
下面再去Swift源码中分析swift_retain做了什么
swift_retain
-
swift_retain在源码中的代码不多,代码如下:- 主要是调用
increment函数进行引用计数加1
- 主要是调用
-
在查看
increment代码:- 主要是获取
oldbits然后将赋值给newbits,再用newbits调用incrementStrongExtraRefCount进行增加引用计数
- 主要是获取
-
再继续阅读
incrementStrongExtraRefCount函数:SWIFT_NODISCARD SWIFT_ALWAYS_INLINE bool incrementStrongExtraRefCount(uint32_t inc) { // This deliberately overflows into the UseSlowRC field. bits += BitsType(inc) << Offsets::StrongExtraRefCountShift; return (SignedBitsType(bits) >= 0); }-
此处的核心是将传入的
inc(1),进行左移StrongExtraRefCountShift (33)位,从上面分析我们知道,33位刚好是强引用位数的第一位,计算器验证1<<33结果如下: -
1<<33的16进制刚好是0x200000000,所以没多一个强引用,refCount地址都会增加0x200000000,使用案例验证如下:
-
-
通过案例可知,
Swift在创建实例对象时的默认引用计数是1,而OC在alloc创建对象时是没有引用计数的,此处是Swift与OC的 不同点
弱引用
-
下面来看看弱引用:
class WSPerson { var age: Int = 18 } var ws = WSPerson() var ws1 = ws var ws2 = ws weak var ws3 = ws -
通过查看
weak修饰的变量,发现weak变量是可选类型: -
再在
weak前后打印对象内存分布,发现reCount地址发生了变化: -
再在汇编代码中查看
weak修饰的变量,发现最终调用了swift_weakInit函数:
下面我们再重点去分析swift_weakInit函数
swift_weakInit
-
swift_weakInit函数在源码实现如下:WeakReference *swift::swift_weakInit(WeakReference *ref, HeapObject *value) { ref->nativeInit(value); return ref; }- 函数主要是
WeakReference的实例ref去调用nativeInit方法
- 函数主要是
-
nativeInit的核心代码如下:void nativeInit(HeapObject *object) { auto side = object ? object->refCounts.formWeakReference() : nullptr; nativeValue.store(WeakReferenceBits(side), std::memory_order_relaxed); }- 主要是判断
weak对象是否存在,如果存在则调用对象的refCounts.formWeakReference函数,不存在则为nullptr,然后将结果进行存储
- 主要是判断
-
在继续查看
formWeakReference函数的代码template <> HeapObjectSideTableEntry* RefCounts<InlineRefCountBits>::formWeakReference() { auto side = allocateSideTable(true); if (side) return side->incrementWeak(); else return nullptr; }- 这里主要是创建了一个散列表,然后使用创建的散列表调用
incrementWeak函数
- 这里主要是创建了一个散列表,然后使用创建的散列表调用
-
继续查看
incrementWeak函数,最终找到如下代码:void incrementWeak() { auto oldbits = refCounts.load(SWIFT_MEMORY_ORDER_CONSUME); RefCountBits newbits; do { newbits = oldbits; assert(newbits.getWeakRefCount() != 0); newbits.incrementWeakRefCount(); if (newbits.getWeakRefCount() < oldbits.getWeakRefCount()) swift_abortWeakRetainOverflow(); } while (!refCounts.compare_exchange_weak(oldbits, newbits, std::memory_order_relaxed)); }- 主要是在
compare_exchange_weak条件中判断oldbits和newbits,- 该函数在这里传入
期待值和新值,它们对比变量的值和期待的值是否一致,如果是,则替换为用户指定的一个新的数值。如果不是,则将变量的值和期待的值交换
- 该函数在这里传入
- 满足条件则将旧值赋给新值,然后使用
newbits调用incrementWeakRefCount函数
- 主要是在
-
incrementWeakRefCount函数最终调用是bits自增:void incrementWeakRefCount() { weakBits++; }- 这里出现了新的名词
weakBits,在上面的文中我们知道RefCountBitsT中有uint64_t的bits,那么这个weakBits肯定是在创建散列表时产生的,然后我们再去看看创建散列表时都做了些什么
- 这里出现了新的名词
-
allocateSideTable的代码如下: -
现在我们分析的主线是
weakBits是什么,所以我们需要关注的代码是创建处,也就是HeapObjectSideTableEntry,再来跟进HeapObjectSideTableEntry发现它是一个类:-
HeapObjectSideTableEntry中的成员变量refCounts是SideTableRefCounts类型,它是模版函数RefCounts<SideTableRefCountBits>的别名,实际的内容是根据SideTableRefCountBits来确定的 -
再去查看
SideTableRefCountBits:- 这里可以看出
SideTableRefCountBits是继承RefCountBitsT,而且有自己的成员变量weakBits,也就是说SideTableRefCountBits有继承过来uint64_t位的成员变量bits和weakBits - 在
SideTableRefCountBits初始化时,weakBits默认值为1
- 这里可以看出
-
-
此时我们找到了
weakBits,但它的结构我们不清楚,然后再回到allocateSideTable函数查看创建newbits的函数InlineRefCountBits:- 该函数的主要作用是将
bits进行右移3位,然后将第63位与62位置为1,此时我们可以得到散列表在bits中的位置:
- 那么我们想要拿到原来的引用计数,只需要先将
第63位与62位置为0,再左移3位,就可以拿到原来的引用计数。
- 该函数的主要作用是将
-
下面去验证:
- 在打印的分块内存中,我们可以看到继承过来的强引用的引用计数,也就是
3,而由于weakBits初始值是1,所以此时显示的弱引用值为2,得以验证
- 在打印的分块内存中,我们可以看到继承过来的强引用的引用计数,也就是
闭包的循环引用
-
闭包的循环引用有如下案例:
class WSPerson { var age: Int = 18 var birthday: (() ->Void)? deinit { print("WSPerson deinit ~~~") } } func test() { let p = WSPerson() p.birthday = { p.age += 1 } p.birthday!() } test()- 当
test()函数执行完,WSPerson中的deinit(反初始化,相当于dealloc)并不会调用,因为p->birthday->p导致循环引用,可以使用weak和unowned来解决循环引用的问题
- 当
-
使用
weak:- 使用
weak后,发现deinit函数得以执行,所以解决了循环引用 - 使用
weak修饰后,变量变成可选类型,使用时 需要解包,写法上稍微有些麻烦
- 使用
unowned(无主引用)
-
使用
unowned:- 执行结果,
deinit函数可以执行 unowned不允许被设置为nil,它是假定有值的,这一点与weak不同,它也不是强引用。unowned由于总是假定有值,所以当对象释放后再调用的话会产生野指针。
- 执行结果,
-
在上面
refCount分析中得知unownedRefCount类型的引用计数在1~31位,例子如下- 由于
unownedRefCount是从第一位开始,所以每增加一个,在16进制上增加0x2,在二进制上是0x10 - 当没有
unowned时,发现在第一位默认是1:
- 所以无主类型引用计数的个数,是
UnownedRefCount值 减1
- 由于
捕获列表
-
在上面解决闭包循环引用时
[xxx]的写法,叫做捕获列表,先来看看案例:var age : Int = 18 var height: CGFloat = 180 let clouse = { [age] in print(age) print(height) } age = 19 height = 190 clouse()- 打印结果如下:
- 结果捕获列表中的
age在闭包中打印的是原始值,而height打印是最新值 -
- 对于捕获列表中的每个常量,闭包会利⽤周围范围内具有相同名称的常量或变量,来初始化捕获列表中定义的常量。
-
- 捕获列表中的变量是
值拷贝,且不可修改
- 捕获列表中的变量是