和OC一样,Swift也是采取基于引用计数的ARC内存管理方案(针对堆空间)。
一、引用介绍
Swift的ARC中有3中引用:
- 强引用(strong reference):默认情况下,引用都是强引用
- 弱引用(weak reference):通过
weak
定义弱引用 - 无主引用(unowned reference):通过
unowned
定义无主引用
1.1. 强引用
示例代码:
1 | class Person { |
test
函数中的p
是强引用,会在函数调用结束后自动释放。
1.2. 弱引用
弱引用变量必须是可选类型的var
,因为实例销毁后,ARC会自动将弱引用设置为nil
。
思考:为什么要设置为
var
?为什么必须是可选类型?
因为只有可选类型才能设置为nil
,只有var
才能改变内存。
示例代码一:
1 | weak var p: Person? = Person() |
ARC自动给弱引用设置nil
时,不会触发属性观察器。
示例代码二:
1 | class Dog { } |
上面示例代码中Dog
对象很快会被销毁并把dog
属性自动置为nil
,可以看出没有更多的属性观察器打印输出。
1.3. 无主引用
无主引用不会产生强引用,实例销毁后仍然存储着实例的内存地址(类似于OC中的unsafe_unretained
)。
试图在实例销毁后访问无主引用,会产生运行时错误(野指针)。
1.4. weak、unowned的使用限制
weak、unowned
只能用在类实例上面。
示例代码:
1 | protocol Livable : AnyObject { } |
1.5. Autoreleasepool
官方定义:
1 | public func autoreleasepool<Result>(invoking body: () throws -> Result) rethrows -> Result |
使用示例:
1 | class Person { |
只需要把释放的代码放到自动释放池的尾随闭包内即可。
二、循环引用(Reference Cycle)
weak、unowned
都能解决循环引用的问题、unowned
要比weak
少一些性能消耗。
使用场景:
- 在生命周期中可能会变为
nil
的使用weak
- 初始化赋值后再也不会变为
nil
的使用unowned
示例代码一(基础):
1 | class Person { |
示例代码二(循环引用):
1 | var john: Person? |
即使两个变量被释放,变量指向的对象之间还是会存在互相强引用的关系,不会被销毁。
示例代码三(解决循环引用):
要想解决循环引用,只需要把其中一个变量设置为nil
。
1 | john = nil |
1 | unit4A = nil |
示例代码四(使用unowned
解决循环引用):
1 | class Customer { |
由于使用的是unowned
引用,所以当断开john
变量的强引用时,就不会有强引用Customer
实例。
1 | john = nil |
三、闭包的循环引用
闭包表达式默认会对用到的外层对象产生额外的强引用(对外层对象进行了retain
操作)。
下面代码会产生循环引用,导致Person
对象无法释放(看不到Person
的deinit
被调用):
1 | class Person { |
Person
对象p
对fn
有强引用,p.fn
的闭包表达式对Person
对象有强引用,两者之间形成循环引用,所以无法释放。
引用计数最终等于1,没有释放:
3.1. 闭包表达式
在闭包表达式的捕获列表声明weak
或unowned
引用,解决循环引用问题:
1 | // 使用weak |
示例代码(带参数):
1 | class Person { |
示例代码(变量别名):
1 | p.fn = { |
3.2. self和lazy
如果想在定义闭包属性的同时引用self
,这个闭包必须是lazy
的(因为在实例初始化完毕之后才能引用self
)。
为什么不能使用self
?
因为self
只有在实例初始化完毕后才能调用,在初始化属性的同时使用self
肯定是不行的,除非在属性前面加上lazy
(允许在实例初始化完毕之后第一次使用属性时再初始化属性)。
示例代码一:
1 | class Person { |
为什么对象被释放了?
因为属性fn
没有被用到,所以属性没有对实例进行强引用。
如果加上下面的代码就会造成强引用:
1 | func test() { |
怎样解决强引用呢?
在闭包表达式中使用weak
或unowned
即可。
1 | class Person { |
注意:上面的闭包
fn
内部如果用到了实例成员(属性、方法),编译器会强制要求明确写出self
(主要目的是为了提醒开发者循环引用问题)。
如果lazy
属性是闭包调用的结果,那么不用考虑循环引用的问题(因为闭包调用后,闭包的生命周期就结束了)。
示例代码:
1 | class Person { |
为什么这里没有循环引用?
因为属性getAge
后面是一个立即执行的函数,函数执行完成后会立即释放self
,只把返回值给到getAge
,所以没有造成循环引用(该示例中闭包函数体内可以不写self
)。
四、逃逸闭包
非逃逸闭包、逃逸闭包,一般都是当做参数传递给函数。
非逃逸闭包: 闭包调用发生在函数结束前,闭包调用在函数作用域内。
1 | typealias Fn = () -> () |
fn
是非逃逸闭包。
逃逸闭包: 闭包有可能在函数结束后调用,闭包调用逃离了函数的作用域,需要通过@escaping
声明。
示例代码一:
1 | typealias Fn = () -> () |
fn
是逃逸闭包。
示例代码二:
1 | func test(_ fn: @escaping Fn) { |
fn
也是逃逸闭包。
示例代码三:
1 | typealias Fn = () -> () |
fn
是逃逸闭包。DispatchQueue.global().async
也是一个逃逸闭包。它用到了实例成员(属性、方法),编译器会强制要求明确写出self
。这里不会产生循环引用,因为仅仅是异步方法对Person
做了强引用,而Person
没有对异步方法做强引用。
如果Person
对象被释放后不需要再调用fn
函数,则需要使用弱引用:
1 | DispatchQueue.global().async { |
逃逸闭包
@escaping
主要是编译器让开发者知道该函数是有风险的。假设闭包用到了宿主的成员,而宿主在闭包调用前已经被销毁,这时候有可能程序会运行异常。
注意点:
逃逸闭包不可以捕获inout
参数。
五、内存访问冲突
内存访问冲突会在两个访问满足下列条件时发生:
- 至少一个是写入操作
- 它们访问的是同一块内存
- 它们的访问时间重叠(比如在同一个函数内)
示例代码一(没有冲突):
1 | func plus(_ num: inout Int) -> Int { |
示例代码二(存在冲突):
1 | var step = 1 |
解决冲突:
1 | var step = 1 |
示例代码三(函数):
1 | func balance(_ x: inout Int, _ y: inout Int) { |
如果传入的是同一个变量就会报错:
示例代码四(结构体):
1 | struct Player { |
如果传入的是同一个health
主体就会报错:
示例代码五(元组):
1 | var tulpe = (health: 10, energy: 20) |
报错:Simultaneous accesses to 0x10000c338, but modification requires exclusive access.
虽然元组内部的两个变量地址不同,但元组是一块内存,所以会报错。
示例代码五(结构体):
1 | var holly = Player(name: "Holly", health: 10, energy: 10) |
报错:Simultaneous accesses to 0x10000c338, but modification requires exclusive access.
和上面的元组一样,结构体也是一整块内存,访问时会报错。
避免冲突:
如果下面的条件可以满足,就说明重叠访问结构体的属性是安全的:
- 你只访问实例存储属性,不是计算属性或者类属性
- 结构体是局部变量而非全局变量
- 结构体要么没有被闭包捕获,要么只被非逃逸闭包捕获
示例代码:
1 | func test() { |
把上面报错的示例代码放到函数体内就可以避免内存访问冲突,因为放到函数体后tulpe
和holly
就变成了局部变量。