Swift 逃逸闭包和非逃逸闭包有什么区别?
闭包的本质
闭包是自包含的函数代码块,能捕获和存储所在上下文中的常量和变量。Swift 里的闭包就是匿名函数,和 OC 的 Block、JS 的箭头函数本质相同。
swift// 最简闭包 let add: (Int, Int) -> Int = { a, b in a + b } add(1, 2) // 3 // 闭包捕获外部变量 var counter = 0 let increment = { counter += 1 // 捕获了 counter 的引用,不是值拷贝 } increment() print(counter) // 1
闭包是引用类型——赋值给新变量不会拷贝,而是共享同一个闭包实例。这一点和 class 一样,和 struct 不同。
逃逸闭包 vs 非逃逸闭包
这是面试最爱问的区分点。核心区别就一个:闭包的执行时机在函数返回之前还是之后。
非逃逸闭包(默认)
闭包在函数体内就被调用了,函数返回时闭包已经执行完毕,生命周期不会超出函数作用域。Swift 3 之后闭包参数默认就是非逃逸的,不用加任何标注。
swiftfunc doWork(closure: () -> Void) { closure() // 函数返回前就执行了 // 函数结束,closure 被释放 }
逃逸闭包(@escaping)
闭包被存储到函数外部(属性、数组、异步回调),在函数返回之后才被调用。必须显式标注 @escaping,否则编译报错。
swiftvar completions: [() -> Void] = [] func doAsyncWork(closure: @escaping () -> Void) { completions.append(closure) // 存到外部,函数返回后才执行 // 函数结束了,但 closure 还活着 }
最常见的场景是异步网络请求回调——函数发起请求后立刻返回,回调在响应回来后才执行,这就是逃逸。
面试必考的三个区别
| 维度 | 非逃逸 | 逃逸 (@escaping) |
|---|---|---|
| 执行时机 | 函数返回前 | 函数返回后 |
| self 引用 | 可以隐式引用 | 必须显式写 self |
| 循环引用风险 | 无(函数结束就释放) | 有(闭包持有 self,self 持有闭包) |
self 引用的区别是编译器强制的:
swiftclass ViewModel { var data: String = "" func load() { // 非逃逸:隐式引用 self,不需要写 self doWork { data = "updated" } // 逃逸:必须显式写 self,提醒你注意循环引用 doAsyncWork { self.data = "updated" } } }
逃逸闭包必须写 self 是 Swift 的安全设计——强制你意识到这里可能产生循环引用,该用 [weak self] 就得用。
逃逸闭包的循环引用
swiftclass NetworkManager { var result: String? func fetch() { API.request { [weak self] response in // 必须用 weak self self?.result = response.data } } }
不用 [weak self] 的话:NetworkManager 持有闭包(作为 API 回调),闭包捕获了 self(强引用 NetworkManager),谁也释放不了。
非逃逸闭包不存在这个问题,因为函数执行完闭包就释放了,捕获的引用也会跟着释放。
性能差异
非逃逸闭包比逃逸闭包快一点点——编译器可以省去一些 retain/release 调用,闭包上下文可以分配在栈上而不是堆上。但这个差异在绝大多数场景下可以忽略,不用为了性能特意选非逃逸。
真正重要的是语义:能用非逃逸就用非逃逸,它给编译器和读者都传达了更明确的信息——这个闭包不会跑到函数外面去。
捕获列表
闭包默认以引用方式捕获变量。如果需要值拷贝,用捕获列表:
swiftvar value = 10 let closure = { [value] in // 拷贝当前值 print(value) // 10,不会随外部 value 变化 } value = 20 closure() // 仍然打印 10
捕获列表的语法:[弱引用/强引用/值拷贝],可以混用:
swiftlet closure = { [weak self, unowned delegate = self.delegate, copy = self.data] in self?.doSomething(copy) delegate?.notify() }
追问
可选闭包是逃逸的吗?
是的。(() -> Void)? 即使没标 @escaping 也是逃逸的,因为可选值本质是枚举,闭包被包了一层,生命周期超出了函数范围。
swift// 编译通过,可选闭包天然逃逸 func doWork(closure: (() -> Void)?) { DispatchQueue.main.async { closure?() } }
什么时候必须用 @escaping?
三种典型场景:
- 异步回调(网络请求、延迟执行)
- 存储闭包到属性或集合中
- 闭包作为可选参数
autoreleasepool 在闭包里需要用吗?
逃逸闭包如果捕获了大量临时对象,可以在闭包内部用 autoreleasepool 包裹关键代码段,及时释放不需要的对象,降低内存峰值。