Swift之深入解析内存管理的底层原理
Posted Forever_wj
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Swift之深入解析内存管理的底层原理相关的知识,希望对你有一定的参考价值。
一、Swift 内存管理
① ARC
- 跟 OC 一样,Swift 也是采用基于引用计数的 ARC 内存管理方案(针对堆空间);
- Swift 的 ARC 中有三种引用:
- 强引用(strong reference):默认情况下,引用都是强引用;
- 弱引用(weak reference):通过 weak 定义弱引用;
- 必须是可选类型的 var,因为实例销毁后,ARC 会自动将弱引用设置为 nil;
- ARC 自动给弱引用设置 nil 时,不会触发属性观察器;
- 无主引用(unowned reference):通过 unowned 定义无主引用;
- 不会产生强引用,实例销毁后仍然存储着实例的内存地址(类似 OC 中的 unsafe_unretained);
- 视图在实例销毁后访问无主引用,会产生运行时错误(野指针)。
② weak/unowned 使用限制
- weak、unowned 只能用在类实例上面,这是因为一般只有类实例放堆空间,结构体、枚举一般都是不放在堆空间的;
class Car {
}
protocol Actions : AnyObject {
}
weak var c0: Car?
weak var c1: AnyObject?
weak var c3: Actions?
unowned var c4: Car?
unowned var c5: AnyObject?
unowned var c6: Actions?
- 上面代码编译都是没问题的,AnyObject 是可以代表任意类类型,协议 Actions 也是可以的,因为它后面是 Actions :AnyObject,意思就是它的协议只能被类类型遵守。
- 若协议 Actions 去掉后面的冒号,c3 和 c6 是编译不通过的,因为此协议有可能被结构体、枚举遵守,而 weak、unowned 只能用在类实例上面,所以编译器提前抛出错误,Swift 是强安全语言。
③ Autoreleasepool
- 在 Swift 中,Autoreleasepool 是保留的,变成了一个全局的函数:
public func autoreleasepool<Result>(invoking body: () throws -> Result) rethrows -> Result
- 使用如下:
class Car {
var name : String?
init(name :String?) {
self.name = name;
}
func drive() {
}
}
autoreleasepool {
let car = Car(name: "宝马")
car.drive()
}
- 内存开销较大的场景(比如数千对经纬度数据在地图上绘制公交路线轨迹),可以使用自动释放池。
④ 循环引用(Reference Cycle)
- weak/unowned 都可以解决循环引用的问题,但是 unowned 要比 weak 少一些性能消耗;weak 在实例销毁的时候设置了一遍 weak 引用为 nil,所以在性能上会多一部分消耗;
- 在生命周期中可能会变为 nil 的对象,使用 weak;初始化赋值后再也不会改变为 nil 的对象,使用 unowned;
- 闭包表达式默认会对用到的外层对象产生额外的强引用(对外层对象进行了 retain 操作),如下:
class Person {
var read:(() -> ())?
func listen() {
print("listen")
}
deinit {
print("deinit")
}
}
func test() {
let p = Person()
p.read = {
p.listen()
}
}
print(1)
test()
print(2)
- 运行后,打印结果只有 1 和 2,没有 deinit,说明 p 对象一直没有被销毁,可以看到问题出在这里:
p.read = {
p.listen()
}
- 对象 p 里的 read 方法强引用了闭包,而闭包里也强引用了对象 p,两者形成循环引用,对象 p 也就无法被释放销毁。
- 在 p.read 处打上断点,进入汇编调试,看看是否有强引用(retain),强引用下引用计数器变化如下所示:
- 注释掉上面代码里的 p.read = { p.listen() },再看汇编:
- 可以看出,在 p.read = { p.listen() } 里,闭包对 p 对象进行了强引用也就是 retain 操作,构成了引用计数始终为 1 的情况,无法释放对象。
- 在闭包表达式的捕获列表声明 weak 或 unowned 引用,解决循环引用问题,如下所示:
func test() {
let p = Person()
p.read = {
[weak p] in
p?.listen()
}
}
func test() {
let p = Person()
p.read = {
[unowned p] in
p.listen()
}
}
func test() {
let p:Person? = Person()
p?.read = {
[weak p] in
p?.listen()
}
}
func test() {
let p:Person? = Person()
p?.read = {
[unowned p] in
p?.listen()
}
}
- weak 弱引用必须是可选类型,所以,对象 p 后面需要加上 ? ;
- 若是 unowned 修饰 p,p 后面不加 ?,因为 p 本身就是非可选类型,unowned 默认情况下也就是非可选类型,是跟着 p 执行的。
- 如下所示,[weak p] 是捕获列表,(age) 是参数列表,捕获列表一般是写在参数列表前面的,in 后面的就是函数体:
class Person {
var read :((Int) -> ())?
func listen() {
print("listen")
}
deinit {
print("deinit")
}
}
func test() {
let p = Person()
p.read = {
[weak wp = p](age) in
wp?.listen()
}
}
- 如果想在定义闭包属性的同时引用 self,那么闭包必须是 lazy 的(因为在实例初始化完毕之后才能引用 self):
class Person {
lazy var read :(() -> ()) = {
self.listen()
}
func listen() {
print("listen")
}
deinit {
print("deinit")
}
}
func test() {
let p = Person()
}
- 运行上面的代码,可以看到会打印 deinit,这说明对象 p 被释放。常规来说,对象 p 有个强引用 read 引用了闭包表达式,闭包表达式里也强引用 self,两者形成循环引用,无法释放对象 p,可现在却被释放了,这是为什么呢?这是因为 read 是 lazy 修饰的,在未调用 p.read 的时候是没有值的,那么它后面的闭包表达也就不存在,自然就无法引用 self,也就不能造成循环引用。
- 当第一次调用 p.read() 后,才会触发 read 的初始化,创建闭包表达式赋值给 read,就形成了循环引用。解决如下:
lazy var read:(() -> ()) = {
[weak weakSelf = self] in
weakSelf?.listen()
}
lazy var read:(() -> ()) = {
[unowned weakSelf = self] in
weakSelf.listen()
}
- 如果 lazy 属性是闭包调用的结果,则不用考虑循环引用的问题(因为闭包调用后,闭包的生命周期也就结束):
class Person {
var age: Int = 10
lazy var getAge: Int = {
self.age
}()
deinit {
print("deinit")
}
}
func test() {
let p = Person()
print(p.getAge)
}
test()
⑤ 内存访问冲突(Conflicting Access to Memory)
- 内存访问冲突会在两个访问满足下面条件时发生:
- 至少一个是写入操作;
- 它们访问的是同一块内存;
- 它们的访问时间重叠(比如在同一个函数内)。
- 如下所示:
// 不存在内存访问冲突
func plus(_ num: inout Int) -> Int {
num + 1
}
var number = 1
number = plus(&number)
// 存在内存访问冲突
var step = 1
func increment(_ num: inout Int) {
// 此处编译没问题,但运行报错
// Simultaneous accesses to 0x100008178, but modification requires exclusive access
num += step
}
increment(&step)
- increment 函数内的 num += step 产生内存冲突,因为 num 虽然是形参,但外面传的值还是 step 的内存地址,+= 就造成了同一时间对同一份内存进行既读又写的操作,所以造成内存冲突。
- 上面的解决方式如下:
var step = 1
func increment(_ num: inout Int) {
num += step
}
var temp = step
increment(&temp)
step = temp
- 如果下面条件可以满足,说明重叠访问结构体的属性是安全的:
- 只访问实例存储属性,不是计算属性或者类属性;
- 结构体是局部变量而非全局变量;
- 结构体要么没有被闭包捕获要么只被非逃逸闭包捕获。
func test() {
var aa = AA(x: 1, y: 2)
sum(&aa.x, &aa.y)
}
二、Swift 内存管理底层分析
① 强引用
- 现有如下示例代码:
class YDWTeacher {
var age : Int = 18
var name : String = "DW"
}
var t = YDWTeacher()
var t1 = t
var t2 = t
- 查看 t 的内存情况,为什么其中的 refCounts 是 0x0000000600000003?调试如下:
po t
<YDWTeacher 0x10065bb60>
x/8g 0x10065bb60
0x10065bb60: 0x10000000100008178 0x0000000600000003
0x10065bb70: 0x10000000100000012 0x00000000004c4a43
0x10065bb80: 0xe3000000000000000 0x000000000000005f
0x10065bb90: 0x000000009a0080001 0x00007fff80bfb718
- 了解过 Swift 类的底层原理,应该知道类有一个 HeapObject,具体请参考我之前的博客:Swift之深入解析“类”的底层原理,我们通过 HeapObject 类来分析 t 的引用计数。
- 分析源码 HeapObject -> InlineRefCounts:
struct HeapObject {
HeapMetadata const *metadata;
SWIFT_HEAPOBJECT_NON_OBJC_MEMBERS;
...
}
#define SWIFT_HEAPOBJECT_NON_OBJC_MEMBERS \\
InlineRefCounts refCounts
- 进入 InlineRefCounts 定义,可以看到是 RefCounts 类型的别名,而 RefCounts 是模板类,真正决定的是传入的类型 InlineRefCountBits:
typedef RefCounts<InlineRefCountBits> InlineRefCounts;
template <typename RefCountBits>
class RefCounts {
std::atomic<RefCountBits> refCounts;
...
}
- 分析 InlineRefCountBits,它是 RefCountBitsT 类的别名:
typedef RefCountBitsT<RefCountIsInline> InlineRefCountBits;
- 分析 RefCountBitsT,有 bits 属性,bits 其实质是将 RefCountBitsInt 中的 type 属性取了一个别名,所以 bits 的真正类型是 uint64_t,即 64 位整型数组:
template <RefCountInlinedness refcountIsInline>
class RefCountBitsT {
...
typedef typename RefCountBitsInt<refcountIsInline, sizeof(void*)>::Type
BitsType;
...
BitsType bits;
...
}
template <>
struct RefCountBitsInt<RefCountNotInline, 4> {
// 类型
typedef uint64_t Type;
typedef int64_t SignedType;
};
- 继续查看初始化源码 swift_allocObject:
static HeapObject *_swift_allocObject_(HeapMetadata const *metadata,
size_t requiredSize,
size_t requiredAlignmentMask) {
...
new (object) HeapObject(metadata);
...
}
// 构造函数
constexpr HeapObject(HeapMetadata const *newMetadata)
: metadata(newMetadata)
, refCounts(InlineRefCounts::Initialized) {
}
- 进入 Initialized 定义,是一个枚举,其对应的 refCounts 方法中:
enum Initialized_t { Initialized };
// 对应的RefCounts方法
// Refcount of a new object is 1.
constexpr RefCounts(Initialized_t)
: refCounts(RefCountBits(0, 1)) {}
- 进入 Initialized 定义,可以看到是一个枚举,其对应的 refCounts 方法,执行 RefCountBits:
enum Initialized_t { Initialized };
// 对应的RefCounts方法
// Refcount of a new object is 1.
constexpr RefCounts(Initialized_t)
: refCounts(RefCountBits(0, 1)) {}
- 进入RefCountBits 定义,它也是一个模板定义:
template <typename RefCountBits>
class RefCounts {
std::atomic<RefCountBits> refCounts;
...
}
- 因此初始化实际上执行的是 RefCountBitsT,根据 Offsets 做了一个位域操作:
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))
{
}
- RefCountsBit 的结构如下:
- 将上面 t 的 refCounts 用二进制展示,其中强引用计数为 3:
- SIL 分析
-
- 当只有 t 实例变量时,SIL 分析如下:
-
- 当有t + t1 两个变量的时候,查看是否有 strong_retain 操作:
// SIL 中的 main
alloc_global @main.t1 : main.YDWTeacher // id: %8
%9 = global_addr @main.t1 : main.YDWTeacher : $*YDWTeacher // user: %11
%10 = begin_access [read] [dynamic] %3 : $*YDWTeacher // users: %12, %11
copy_addr %10 to [initialization] %9 : $*YDWTeacher // id: %11
// 其中 copy_addr 等价于
- %new = load s*YDWTeacher
- strong_retain %new
- store %new to %9
-
- 其中,strong_retain 对应的就是 swift_retain,内部是一个宏定义,实现的是对 object 的引用计数作 +1 操作;
// 内部宏定义
HeapObject *swift::swift_retain(HeapObject *object) {
CALL_IMPL(swift_retain, (object));
}
// 本质调用 _swift_retain_
static HeapObject *_swift_retain_(HeapObject *object) {
SWIFT_RT_TRACK_INVOCATION(object, swift_retain);
if (isValidPointerForNativeRetain(object))
object->refCounts.increment(1);
return object;
}
void increment(uint32_t inc = 1) {
auto oldbits = refCounts.load(SWIFT_MEMORY_ORDER_CONSUME);
// constant propagation will remove this in swift_retain, it should only
// be present in swift_retain_n
if (inc != 1 && oldbits.isImmortal(true)) {
return;
}
// 64位bits
RefCountBits newbits;
do {
newbits = oldbits;
bool fast = newbits.incrementStrongExtraRefCount(inc);
if (SWIFT_UNLIKELY(!fast)) {
if (oldbits.isImmortal(false))
return;
return incrementSlow(oldbits, inc);
}
} while (!refCounts.compare_exchange_weak(oldbits, newbits,
std::memory_order_relaxed));
}
- 回到 HeapObject,从 InlineRefCounts 进入,其中是 c++ 中的模板定义,是为了更好的抽象,在其中查找 bits(即 decrementStrongExtraRefCount 方法):
LLVM_NODISCARD LLVM_ATTRIBUTE_ALWAYS_INLINE
bool incrementStrongExtraRefCount(uint32_t inc) {
// This deliberately overflows into the UseSlowRC field.
// 对inc做强制类型转换为 BitsType
// 其中 BitsType(inc) << Offsets::StrongExtraRefCountShift 等价于 1<<33位,16进制为 0x200000000
// 这里的 bits += 0x200000000,将对应的33-63转换为10进制,为
bits += BitsType(inc) << Offsets::StrongExtraRefCountShift;
return (SignedBitsType(bits) >= 0);
}
- 例如以 t 的 refCounts 为例(其中 62-33 位是 strongCount,每次增加强引用计数,都是在 33-62 位上增加的,固定的增量为 1 左移 33 位,即 0x200000000);
- 只有 t 时的 refCounts 是 0x0000000200000003;
- t + t1 时的 refCounts 是 0x0000000400000003 = 0x0000000200000003 + 0x200000000;
- t + t1 + t2 时的 refCounts 是 0x0000000600000003 = 0x0000000400000003 + 0x200000000;
- 通过 CFGetRetainCount 获取 t 的引用计数,发现依次是 2、3、4,默认增加 1:
class YDWTeacher {
var age : Int = 18
var name : String = "DW"
}
var t = YDWTeacher()
print(CFGetRetainCount(t as AnyObject))
var t1 = t
print(CFGetRetainCount(t as AnyObject))
var t2 = t1
print(CFGetRetainCount(t as AnyObject))
// 打印如下
2
3
4
- 为什么强引用计数是增加 0x200000000 呢?这是因为 1 左移 33 位,其中 4 位为一组,换算成 16 进制,剩余的 33-32 位 0x10,转换为 10 进制为 2,其实际增加引用计数就是 1。
- 需要注意的是:OC 中创建实例对象时引用计数为 0,swift 中创建实例对象时引用计数默认为 1。
② 弱引用
- 现有以下调试代码:
class YDWTeacher {
var age : Int = 25
var name : String = "DW"
var student : YDWStudent?
}
class YDWStudent {
var age : Int = 15
var teacher : YDWTeacher?
}
func test() {
var t = YDWTeacher()
weak var t1 = t
}
- 查看 t 的引用计数变化:
po t
<YDWTeacher 0x10182bff0>
x/8g 0x10182bff0
0x10182bff0: 0x00000000100008280 0xc000000020809a6c
0x10182c000: 0x00000000000000012 0x00000000004c4a43
0x10182c010: 0xe3000000000000000 0x0000000000000000
0x10182c020: 0x00000000000000000 0x0000000000000000
- 弱引用声明的变量是一个可选值,因为在程序运行过程中是允许将当前变量设置为 nil 的;
- 在 t1 处加断点,汇编调试,可以看到执行了:symbol stub for:swift_weakInit 函数;
- 查看 swift_weakInit 函数,它是由 WeakReference 来调用的,相当于 weak 字段在编译器声明过程中就自定义了一个 WeakReference 的对象,其目的在于管理弱引用:
WeakReference *swift::swift_weakInit(WeakReference *ref, HeapObject *value) {
ref->nativeInit(value);
return ref;
}
- 继续进入 nativeInit:
void nativeInit(HeapObject *object) {
auto side = object ? object->refCounts.formWeakReference() : nullptr;
nativeValue.store(WeakReferenceBits(side), std::memory_order_relaxed);
}
- 进入 formWeakReference,它创建了 sideTable:
template <>
HeapObjectSideTableEntry* RefCounts<InlineRefCountBits>::formWeakReference() {
// 创建 sideTable
auto side = allocateSideTable(true);
if (side)
// 如果创建成功,则增加弱引用
return side->incrementWeak();
else
return nullptr;
}
- 进入 allocateSideTable 方法:
template <>
HeapObjectSideTableEntry* RefCounts<InlineRefCountBits>::allocateSideTable(bool failIfDeiniting)
{
// 先拿到原本的引用计数
auto oldbits = refCounts.load(SWIFT_MEMORY_ORDER_CONSUME);
// Preflight failures before allocating a new side table.
if (oldbits.hasSideTable()) {
// Already have a side table. Return it.
return oldbits.getSideTable();
}
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 allo以上是关于Swift之深入解析内存管理的底层原理的主要内容,如果未能解决你的问题,请参考以下文章
iOS之深入解析内存管理Tagged Pointer的底层原理
iOS之深入解析内存管理的引用计数retainCount的底层原理