我们都想写得一手好代码。这包含防止内存泄露,当在写闭包时业务如果需要用到self,我们会使用[weak self] 修饰。但为什么需要弱捕获呢?是否任何情况下在闭包里都要用weak? 接下来我们将用捕获列表来回答这几个问题。我们将讲解到不同的捕获,以及什么时候用合适的捕获去处理问题。

什么是捕获列表

我们先看以下代码片段,调用闭包后显示的结果:

1
2
3
4
5
6
7
8
var name = "qiu"
var appendToName = { (string: String) -> String in
return name.appending(string)
}

let one = appendToName("gaoying") //qiugaoying
name = "liu"
let two = appendToName("gaoying") //liugaoying

闭包可对同一作用域范围内的属性做引用,也就是闭包内持有的属性在同一上下文作用域内变更受影响。我们再观察以下代码:

1
2
3
4
5
6
7
8
var name = "qiu"
var appendToName = { [name] (string: String) -> String in
return name.appending(string)
}

let one = appendToName("gaoying") //qiugaoying
name = "liu"
let two = appendToName("gaoying") //qiugaoying

这时输出的结果都为qiugaoying。[name] 将属性name放在闭包的捕获列表,明确地告诉闭包强持有闭包首次调用时的name值。如果要在闭包捕获列表内捕获多个值,则用逗号隔开,如[property, anotherProperty] in 方式。

关于内存泄露

1
2
3
4
5
6
7
8
9
10
class MyClass {}
var instance: MyClass? = MyClass()

var aClosure = { [instance] in
print(instance)
}

aClosure() // MyClass
instance = nil
aClosure() // MyClass

如上代码片段,当instance = nil之后,闭包再次调用,此时instance仍然在闭包内被强捕获。即虽然instance置为nil,但闭包里强捕获的属性没有被释放。此刻instance捕获的是引用类型,那么闭包强指针引用该对象。我们知道在内存管理时候,引用类型对象只要引用计数大于1,那么再内存中是不会被回收的,此刻,即造成了内存泄露。 如果强捕获的属性是值类型(结构体或者枚举),那么强捕获的时候将发生copy。

通常情况下,我们是不希望这种情况发生的。举一个场景:当我们在ViewController执行网络请求的时候,在请求结果回来之前,我们dismissed了控制器,如果网络请求回调的必要内强捕获了控制器,那么内存中是仍然存在该ViewController。当来回切换的时候,是不是造成了很多内存泄露呢?那么我们怎么去解决这个问题呢?把强捕获改成弱捕获就行了,使用weak关键字。

1
2
3
4
5
6
7
8
9
10
11
12
class MyClass {}
var instance: MyClass? = MyClass()

var aClosure = { [weak instance] in
print(instance)
guard strongSelf = instance else { return }
//do something
}

aClosure() // MyClass
instance = nil
aClosure() // nil

在闭包调用之前,如果对象销毁了继续调用闭包可能会出一些潜在的业务bug。如果你想立即抛出异常或直接让程序crash掉,这样有助于及早地发现问题。你也可以用关键字unowned关键字进行捕获。
1
2
3
4
5
6
7
8
9
10
11
12
class MyClass {}
var instance: MyClass? = MyClass()

var aClosure = { [unowned instance] in
print(instance)
guard strongSelf = instance else { return }
//do something
}

aClosure() // MyClass
instance = nil
aClosure() // Crash

闭包内何时直接用 self

  • 当self 指的是值类型(结构体、枚举)时候,可以直接用self. 因为结构体没有指针引用他们,在闭包内也不会出现内存泄露一说。
  • 当闭包和self对象无直接持有关系时候,比如DispatchQueue.
    1
    2
    3
    4
    5
    6
    class MyClass {
    func dispatchSomething() {
    DispatchQueue.global().async {
    }
    }
    }
    在调用dispatchSomething方法后,异步闭包体会立即执行,在闭包体可以放心的使用self。如果闭包是对象的某一事件回调你需要留意self是否弱引用避免引起循环引用。
    有个最简单的方法判断调用的函数闭包是否被持有:判断是否有关键字@escaping。

闭包里何时用强捕获变量

强捕获用的场景比较少。以下有一个例子,如在网络请求后,通过本地存储CoreData或者Sqlite把数据持久化。

1
2
3
4
5
6
7
8
9
10
11
struct SomeDataSource {
var storage: MyDataStore //class
let networking: NetworkingLayer
// more properties

func refreshData() {
networking.refresh { [storage] data in
storage.persist(data)
}
}
}

这个时候,无论页面销毁SomeDataSource,只要网络请求结果回来,都能够保证数据被持久化以便下次使用。而且storage只捕获首次调用refreshData时候的storage变量。(假设)后期无论对storage更改,storage仍然是之前旧的对象。

何时使用Weak 或者 unowned 关键字

弱捕获是我们比较常用且最好默认的一种捕获选择。当你不想让一个已释放对象还在闭包内执行时候,或者当你调用的函数闭包被应用时,这个时候需考虑用弱捕获。弱捕获的属性记得给可选值,因为可能是nil。这样给nil发送消息的时候就不至于崩溃。使用weak或者unowned能100%确保引用类型在销毁后也能从内存中回收,不会出现内存泄露的情况。如果你实在不知道用哪种捕获方式的时候,那就用它吧😝。

评论