Swift 循环引用
[TOC]
前言
本本主要研究的是Swift
中闭包捕获外部变量时产生的循环引用。全部示例是在macOS
命令行工程中。
1. 循环引用
1.1 闭包捕获外部变量
举个简单的例子
var age = 10
let clourse = {
age += 1
}
print(age)
clourse()
print(age)
打印结果:

此处即使闭包捕获外部变量的最简单的例子,闭包内部对变量的修改将会改变变量的原始的值,这个与OC
的block
是一致的,但是OC
在block
中修改外部变量的值需要使用__block
修饰,Swift
则不需要。
1.2 循环引用
1.2.1 deinit
定义一个类,代码如下:
class Person {
var age = 18
//反初始化器
deinit {
print("Person deinit")
}
}
类中的deinit
方法是Swift
中的反初始化器,当前实例对象即将回收的时候会调用该方法,所以说我们可以通过该方法是否被调用来判断当前实例对象是否被正常释放。如果没被释放就可能存在循环引用的情况。
声明一个方法,并调用它。
func test() {
var p = Person()
}
test()
运行结果:

我们可以看到在以上的代码中,正常打印了Person deinit
也就是说执行了deinit
方法,所以说此时p
这个实例对象正常释放了,不存在循环引用。
1.2.2 在闭包中修改属性值
还是上面的类,此时我们在闭包中修改实例对象的属性值,代码如下:
let clourse = {
p.age += 1
}
clourse()
运行,打印结果:

此时依旧打印了Person deinit
,说明并没有造成循环引用。
1.2.3 循环引用
在上面的两个例子中都没有造成循环引用,下面在看个例子,我们在类中增加一个无参无返回值的闭包属性。并且在这个闭包修修改age
属性的值。代码如下:
class Person {
var age = 18
var myClourse: (()->())?
//反初始化器
deinit {
print("Person deinit")
}
}
func test() {
var p = Person()
p.myClourse = {
p.age += 1
}
}
test()
运行,打印结果:

此时我们并没有看到Person deinit
的打印,说明此时就造成了循环引用,此时我们的myClourse
持有着p
对象,而myClourse
作为p
对象的属性,也被p
对象持有着,p
对象需要等待闭包释放后才能释放,而闭包也在等待内部的p
对象释放,所以就造成了循环引用。
此处引入循环引用的过程虽长,但也一步一步的解释了出现循环引用的条件,在开发过程中我们最常见的循环引用就是self
强持有闭包,而闭包中又要持有self
,以此来实现对self
中某些属性的修改,或者方法的调用。
2. 解决循环引用的方法
循环引用会使实例对象不能够正常释放,此时就会出现一些不可预估的问题,也会造成内存的浪费,所以我们需要解决循环引用,在Swift
中有两种解决循环引用的方法。分别是weak
和unowned
。关于这两个方法的底层实现及相关知识点可以参考我的另一篇文章Swift 内存管理,本篇注重应用,对原理的介绍偏弱。
2.1 weak
func test() {
var p = Person()
p.myClourse = { [weak p] in
p?.age += 1
}
}
weak
也是我们OC
中的一种解决循环引用的方式,在Swift
中使用weak
修饰的实例变量默认为可选类型,所以在使用weak
后p
对象会成为可选类型,在使用的时候需要使用?
或者!
。
2.2 unowned
func test() {
var p = Person()
p.myClourse = { [unowned p] in
p.age += 1
}
}
unowned
也是Swift
中独有的一种解决循环引用的方式,相较于weak
,使用unowned
修饰后的对象不是可选类型,也不可置为nil
。unowned
的意思是假定当前对象有值,所以就可能存在野指针的情况,所以在使用的过程中要注意使用的位置。而weak
修饰的对象一旦为nil
的时候,由于swift
特性,可选值为nil
就不会执行后续的代码,相较于unowned
比较安全。
3. 捕获列表
在上文中我们提到的[weak p]和[unowned p]在swift
中被称为捕获列表。
捕获列表:
- 定义在闭包的参数列表之前
- 使用
[]
括起来 - 即使是省略参数和返回值的时候也要使用
in
关键字 -
[weak p]
和[unowned p]
是分别使用weak
和unowned
关键字修饰p
- 如果不需要修饰,我们可以写成
[p]
下面我们来看个例子,以下代码的执行结果是什么?
func test() {
var age = 0
var height = 0.0
let clourse = { [age] in
print(age)
print(height)
}
age = 10
height = 1.85
clourse()
}
test()
打印结果:

根据以上的打印结果我们可以知道,对于闭包中捕获列表中的常量,闭包会捕获外部相同名称的变量或常量来初始化捕获列表中定义的常量,其有如下特点:
- 捕获列表中的是常量,不可修改
- 该常量是值拷贝,相当于复制了一份外部变量或常量
- 既然是常量就是不可修改的
如果我们修改会报编译错误:

关于此处的原理将会在闭包相关文章中进行分析,也可以先看看OC
中Block底层原理
网友评论