编程iOS/移动开发人员

闭包(Closure)在Swift中是如何工作的,它们与函数的区别是什么,以及与其使用相关的内存管理方面有哪些?

用 Hintsage AI 助手通过面试

答案。

问题背景:

闭包作为概念来源于函数式语言,允许将代码块作为值传递。在Swift中,闭包(类似于其他语言中的lambda)自一开始就出现了。因此,可以优雅地实现异步调用、委托行为、排序集合、过滤等。

问题:

闭包会捕获周围上下文中的变量。如果这些变量中有指向类对象的引用,可能会出现retain cycle,特别是当闭包作为该类的属性时。还重要的是理解escaping和non-escaping闭包之间的区别,并意识到值捕获是如何工作的。

解决方案:

Swift将闭包实现为独立的对象,可以捕获上下文,而闭包的所有者在架构上需要小心。

代码示例:

class Performer { var onComplete: (() -> Void)? func doWork() { onComplete = { print("工作的完成!") } } } // 在闭包内捕获self class Test { var value = 0 func start() { DispatchQueue.global().async { [weak self] in self?.value = 1 } } }

关键特性:

  • 闭包可以通过引用或值捕获值
  • escaping/non-escaping影响闭包的生命周期
  • 作为存储属性的闭包容易创建retain cycle

反向问题。

如果闭包没有显式捕获self,会导致retain cycle吗?

是的,如果闭包作为类的strong property保存并在内部引用self,即使你没有显式写出self,也会导致retain cycle。使用[weak self]/[unowned self],或者在可能的情况下使闭包本地。

escaping和non-escaping闭包之间有什么区别?

escaping闭包可以在函数结束后被调用,通常用于异步操作。non-escaping在函数执行结束前执行。在escaping中,self的捕获顺序是不同的。

代码示例:

func asyncWork(completion: @escaping () -> Void) { DispatchQueue.global().async { completion() } }

闭包可以修改捕获变量的值吗?

可以,如果闭包捕获了一个声明为var的变量,它可以改变其值(对于值类型)。对于类来说,这是一个引用,属性始终可以被修改。

代码示例:

var value = 1 let closure = { value += 1 } closure() print(value) // 2

常见错误和反模式

  • 将闭包设置为类的属性并直接引用self而不使用[weak self]。
  • 声明过于庞大的闭包——这会使理解和调试变得困难。
  • 在可以使用non-escaping的地方使用escaping。

生活中的案例

负面案例

ViewController将闭包作为属性(例如,completionHandler)并直接引用self。结果形成了retain cycle:ViewController => 闭包 => ViewController。屏幕断开不会释放内存。

优点: 代码简洁且看起来很短。

缺点: 内存泄漏,ViewController在内存中“悬挂”,潜在的bug。

正面案例

在闭包内使用[weak self]或[unowned self],或闭包的生命周期不超过对象的生命周期。在代码审查中审核类似的地方。

优点: 资源正确释放,无不可预测的泄漏。

缺点: [weak self]在解引用时需要小心,错误使用可能导致隐性崩溃。