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