【iOS】Swift系列二十四 - 内存管理(一)

和OC一样,Swift也是采取基于引用计数的ARC内存管理方案(针对堆空间)。

一、引用介绍

Swift的ARC中有3中引用:

  • 强引用strong reference):默认情况下,引用都是强引用
  • 弱引用weak reference):通过weak定义弱引用
  • 无主引用unowned reference):通过unowned定义无主引用

1.1. 强引用

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Person {
deinit {
print("Person.deinit")
}
}
func test() {
let p = Person()
}
print("1")
test()
print("2")
/*
输出:
1
Person.deinit
2
*/

test函数中的p是强引用,会在函数调用结束后自动释放。

1.2. 弱引用

弱引用变量必须是可选类型的var,因为实例销毁后,ARC会自动将弱引用设置为nil

思考:为什么要设置为var?为什么必须是可选类型?
因为只有可选类型才能设置为nil,只有var才能改变内存。

示例代码一:

1
weak var p: Person? = Person()

ARC自动给弱引用设置nil时,不会触发属性观察器。

示例代码二:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Dog { }
class Person {
weak var dog: Dog? {
willSet {
print("dog property willSet")
}
didSet {
print("dog property didSet")
}
}
deinit {
print("Person.deinit")
}
}
var p = Person()
print("1")
p.dog = Dog()
print("2")
/*
输出:
1
dog property willSet
dog property didSet
2
*/

上面示例代码中Dog对象很快会被销毁并把dog属性自动置为nil,可以看出没有更多的属性观察器打印输出。

1.3. 无主引用

无主引用不会产生强引用,实例销毁后仍然存储着实例的内存地址(类似于OC中的unsafe_unretained)。

试图在实例销毁后访问无主引用,会产生运行时错误(野指针)。

1.4. weak、unowned的使用限制

weak、unowned只能用在类实例上面。

示例代码:

1
2
3
4
5
6
7
8
9
10
protocol Livable : AnyObject { }
class Person { }

weak var p0: Person?
weak var p1: AnyObject?
weak var p2: Livable?

unowned var p3: Person?
unowned var p4: AnyObject?
unowned var p5: Livable?

1.5. Autoreleasepool

官方定义:

1
public func autoreleasepool<Result>(invoking body: () throws -> Result) rethrows -> Result

使用示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Person {
var age: Int
var name: String
init(age: Int, name: String) {
self.age = age
self.name = name
}
func run() {
print("Person run")
}
}

autoreleasepool {
let p = Person(age: 20, name: "idbeny")
p.run()
}

只需要把释放的代码放到自动释放池的尾随闭包内即可。

二、循环引用(Reference Cycle)

weak、unowned都能解决循环引用的问题、unowned要比weak少一些性能消耗。

使用场景:

  • 在生命周期中可能会变为nil的使用weak
  • 初始化赋值后再也不会变为nil的使用unowned

官方示例

示例代码一(基础):

1
2
3
4
5
6
7
8
9
10
11
12
13
class Person {
let name: String
init(name: String) { self.name = name }
var apartment: Apartment?
deinit { print("\(name) is being deinitialized") }
}

class Apartment {
let unit: String
init(unit: String) { self.unit = unit }
var tenant: Person?
deinit { print("Apartment \(unit) is being deinitialized") }
}

示例代码二(循环引用):

1
2
3
4
5
6
7
8
var john: Person?
var unit4A: Apartment?

john = Person(name: "John Appleseed")
unit4A = Apartment(unit: "4A")

john!.apartment = unit4A
unit4A!.tenant = john

即使两个变量被释放,变量指向的对象之间还是会存在互相强引用的关系,不会被销毁。

示例代码三(解决循环引用):
要想解决循环引用,只需要把其中一个变量设置为nil

1
2
john = nil
// 输出:John Appleseed is being deinitialized

1
2
unit4A = nil
// 输出:Apartment 4A is being deinitialized

示例代码四(使用unowned解决循环引用):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Customer {
let name: String
var card: CreditCard?
init(name: String) {
self.name = name
}
deinit { print("\(name) is being deinitialized") }
}

class CreditCard {
let number: UInt64
unowned let customer: Customer
init(number: UInt64, customer: Customer) {
self.number = number
self.customer = customer
}
deinit { print("Card #\(number) is being deinitialized") }
}

var john: Customer?
john = Customer(name: "John Appleseed")
john!.card = CreditCard(number: 1234_5678_9012_3456, customer: john!)

由于使用的是unowned引用,所以当断开john变量的强引用时,就不会有强引用Customer实例。

1
2
3
john = nil
// 输出:John Appleseed is being deinitialized
// 输出:Card #1234567890123456 is being deinitialized

三、闭包的循环引用

闭包表达式默认会对用到的外层对象产生额外的强引用(对外层对象进行了retain操作)。

下面代码会产生循环引用,导致Person对象无法释放(看不到Persondeinit被调用):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Person {
var fn: (() -> ())?
func run() {
print("run")
}
deinit {
print("deinit")
}
}
func test() {
let p = Person()
p.fn = {
p.run()
}
}
test()

Person对象pfn有强引用,p.fn的闭包表达式对Person对象有强引用,两者之间形成循环引用,所以无法释放。

引用计数最终等于1,没有释放:

3.1. 闭包表达式

在闭包表达式的捕获列表声明weakunowned引用,解决循环引用问题:

1
2
3
4
5
6
7
8
9
10
// 使用weak
// p.fn = {
// [weak p] in
// p?.run()
// }
// 使用unowned
p.fn = {
[unowned p] in
p.run()
}

示例代码(带参数):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Person {
var fn: ((Int) -> ())?
func run() {
print("run")
}
deinit {
print("deinit")
}
}
func test() {
let p = Person()
p.fn = {
[unowned p](Int) in
p.run()
}
}
test()

示例代码(变量别名):

1
2
3
4
p.fn = {
[weak wp = p, unowned up = p, a = 10 + 20](Int) in
wp?.run()
}

3.2. self和lazy

如果想在定义闭包属性的同时引用self,这个闭包必须是lazy的(因为在实例初始化完毕之后才能引用self)。

为什么不能使用self

因为self只有在实例初始化完毕后才能调用,在初始化属性的同时使用self肯定是不行的,除非在属性前面加上lazy(允许在实例初始化完毕之后第一次使用属性时再初始化属性)。

示例代码一:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Person {
lazy var fn: (() -> ()) = {
self.run()
}
func run() {
print("run")
}
deinit {
print("deinit")
}
}
func test() {
var p = Person()
}
test()
// 输出:deinit

为什么对象被释放了?

因为属性fn没有被用到,所以属性没有对实例进行强引用。

如果加上下面的代码就会造成强引用:

1
2
3
4
func test() {
var p = Person()
p.fn()
}

怎样解决强引用呢?

在闭包表达式中使用weakunowned即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Person {
lazy var fn: (() -> ()) = {
[weak weakself = self] in
weakself?.run()
}
func run() {
print("run")
}
deinit {
print("deinit")
}
}
func test() {
let p = Person()
p.fn()
}
test()
/*
输出:
run
deinit
*/

注意:上面的闭包fn内部如果用到了实例成员(属性、方法),编译器会强制要求明确写出self(主要目的是为了提醒开发者循环引用问题)。

如果lazy属性是闭包调用的结果,那么不用考虑循环引用的问题(因为闭包调用后,闭包的生命周期就结束了)。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Person {
var age: Int = 0
lazy var getAge: Int = {
self.age
}()
deinit {
print("deinit")
}
}

func test() {
let p = Person()
print(p.getAge)
}
test()
/*
输出:
0
deinit
*/

为什么这里没有循环引用?

因为属性getAge后面是一个立即执行的函数,函数执行完成后会立即释放self,只把返回值给到getAge,所以没有造成循环引用(该示例中闭包函数体内可以不写self)。

四、逃逸闭包

非逃逸闭包、逃逸闭包,一般都是当做参数传递给函数。

非逃逸闭包: 闭包调用发生在函数结束前,闭包调用在函数作用域内。

1
2
3
4
5
6
7
8
typealias Fn = () -> ()
func test(_ fn: Fn) {
fn()
}
test {
print("1")
}
// 输出:1

fn是非逃逸闭包。

逃逸闭包: 闭包有可能在函数结束后调用,闭包调用逃离了函数的作用域,需要通过@escaping声明。

示例代码一:

1
2
3
4
5
6
7
8
9
typealias Fn = () -> ()
var gFn: Fn?
func test(_ fn: @escaping Fn) {
gFn = fn
}
test {
print("1")
}
// 无输出

fn是逃逸闭包。

示例代码二:

1
2
3
4
5
func test(_ fn: @escaping Fn) {
DispatchQueue.global().async {
fn()
}
}

fn也是逃逸闭包。

示例代码三:

1
2
3
4
5
6
7
8
9
10
11
12
typealias Fn = () -> ()
class Person {
var fn: Fn
init(fn: @escaping Fn) {
self.fn = fn
}
func run() {
DispatchQueue.global().async {
self.fn()
}
}
}

fn是逃逸闭包。DispatchQueue.global().async也是一个逃逸闭包。它用到了实例成员(属性、方法),编译器会强制要求明确写出self。这里不会产生循环引用,因为仅仅是异步方法对Person做了强引用,而Person没有对异步方法做强引用。

如果Person对象被释放后不需要再调用fn函数,则需要使用弱引用:

1
2
3
4
DispatchQueue.global().async {
[weak weakself = self] in
weakself?.fn()
}

逃逸闭包@escaping主要是编译器让开发者知道该函数是有风险的。假设闭包用到了宿主的成员,而宿主在闭包调用前已经被销毁,这时候有可能程序会运行异常。

注意点:
逃逸闭包不可以捕获inout参数。

五、内存访问冲突

内存访问冲突会在两个访问满足下列条件时发生:

  • 至少一个是写入操作
  • 它们访问的是同一块内存
  • 它们的访问时间重叠(比如在同一个函数内)

示例代码一(没有冲突):

1
2
3
4
5
func plus(_ num: inout Int) -> Int {
num + 1
}
var number = 1
number = plus(&number)

示例代码二(存在冲突):

1
2
3
4
5
var step = 1
func increment(_ num: inout Int) {
num += step
}
increment(&step)

解决冲突:

1
2
3
4
5
6
7
var step = 1
func increment(_ num: inout Int) {
num += step
}
var copyofStep = step
increment(&copyofStep)
step = copyofStep

示例代码三(函数):

1
2
3
4
5
6
7
8
func balance(_ x: inout Int, _ y: inout Int) {
let sum = x + y
x = sum / 2
y = sum - x
}
var num1 = 40
var num2 = 30
balance(&num1, &num2)

如果传入的是同一个变量就会报错:

示例代码四(结构体):

1
2
3
4
5
6
7
8
9
10
11
struct Player {
var name: String
var health: Int
var energy: Int
mutating func shareHealth(with teammate: inout Player) {
balance(&teammate.health, &health)
}
}
var oscar = Player(name: "Oscar", health: 10, energy: 10)
var maria = Player(name: "Maria", health: 5, energy: 10)
oscar.shareHealth(with: &maria)

如果传入的是同一个health主体就会报错:

示例代码五(元组):

1
2
var tulpe = (health: 10, energy: 20)
balance(&tulpe.health, &tulpe.energy)

报错:Simultaneous accesses to 0x10000c338, but modification requires exclusive access.

虽然元组内部的两个变量地址不同,但元组是一块内存,所以会报错。

示例代码五(结构体):

1
2
var holly = Player(name: "Holly", health: 10, energy: 10)
balance(&holly.health, &holly.energy)

报错:Simultaneous accesses to 0x10000c338, but modification requires exclusive access.

和上面的元组一样,结构体也是一整块内存,访问时会报错。

避免冲突:
如果下面的条件可以满足,就说明重叠访问结构体的属性是安全的:

  • 你只访问实例存储属性,不是计算属性或者类属性
  • 结构体是局部变量而非全局变量
  • 结构体要么没有被闭包捕获,要么只被非逃逸闭包捕获

示例代码:

1
2
3
4
5
6
7
8
func test() {
var tulpe = (health: 10, energy: 20)
balance(&tulpe.health, &tulpe.energy)

var holly = Player(name: "Holly", health: 10, energy: 10)
balance(&holly.health, &holly.energy)
}
test()

把上面报错的示例代码放到函数体内就可以避免内存访问冲突,因为放到函数体后tulpeholly就变成了局部变量。