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
打印是最新值 -
- 对于捕获列表中的每个常量,闭包会利⽤周围范围内具有相同名称的常量或变量,来初始化捕获列表中定义的常量。
-
- 捕获列表中的变量是
值拷贝
,且不可修改
- 捕获列表中的变量是