Swift中优雅处理闭包导致的循环引用详解

 更新时间:2019年08月06日 09:29:49   作者:小橘爷  
这篇文章主要给大家介绍了关于Swift中优雅的处理闭包导致的循环引用的相关资料,文中通过示例代码介绍的非常详细,对大家学习或者使用Swift具有一定的参考学习价值,需要的朋友们下面来一起学习学习吧

前言

Objective-C 作为一门资历很老的语言,添加了 Block 这个特性后深受广大 iOS 开发者的喜爱。在 Swift 中,对应的概念叫做 Closure,即闭包。虽然更换了名字,但是概念和用法还是相似的,就算是副作用也一样,有可能导致循环引用。

下面我们用一个例子看一下,首先我们需要第一个控制器(FirstViewController),它所做的就是简单的推出第二个控制器(SecondViewController)。

class FirstViewController: UIViewController {
 
 private let button: UIButton = {
  let button = UIButton()
  button.setTitleColor(UIColor.black, for: .normal)
  button.setTitle("跳转到 SecondViewController", for: .normal)
  button.sizeToFit()
  return button
 }()
 
 override func viewDidLoad() {
  super.viewDidLoad()
  
  button.center = view.center
  view.addSubview(button)
  button.addTarget(self, action: #selector(buttonClick), for: .touchUpInside)
 }
 
 @objc private func buttonClick() {
  let secondViewController = SecondViewController()  
  navigationController?.pushViewController(secondViewController, animated: true)
 }
}

下面是 SecondViewController 的代码。SecondViewController 所做的事情是推出第三个控制器(ThirdViewController),不同的是,thirdViewController 是作为一个属性存在的,同时它还有一个闭包 closure ,这是我们用来测试循环引用问题的。还实现了 deinit 方法,用来打印一条语句,看该控制器是否被释放了。

class SecondViewController: UIViewController {
 
 private let thirdViewController = ThirdViewController()
 private let button: UIButton = {
  let button = UIButton()
  button.setTitleColor(UIColor.black, for: .normal)
  button.setTitle("跳转到 ThirdViewController", for: .normal)
  button.sizeToFit()
  return button
 }()
 
 override func viewDidLoad() {
  super.viewDidLoad()
  
  button.center = view.center
  view.addSubview(button)
  button.addTarget(self, action: #selector(buttonClick), for: .touchUpInside)
 }
 
 deinit {
  print("SecondViewController-被释放了")
 }
 
 @objc private func buttonClick() {
  thirdViewController.closure = {
   self.test()
  }
  navigationController?.pushViewController(thirdViewController, animated: true)
 }
 
 private func test() {
  print("调用 test 方法")
 }
}

接下来我们看一下 ThirdViewController 的代码。在 ThirdViewController 中有一个按钮,点击一下就会触发闭包。同时我们还实现了 deinit 方法,用来打印一条语句,看该控制器是否被释放了。

class ThirdViewController: UIViewController {
 
 private let button: UIButton = {
  let button = UIButton()
  button.setTitleColor(UIColor.black, for: .normal)
  button.setTitle("点击按钮", for: .normal)
  button.sizeToFit()
  return button
 }()
 
 var closure: (() -> Void)?
 
 override func viewDidLoad() {
  super.viewDidLoad()
  
  button.center = view.center
  view.addSubview(button)
  button.addTarget(self, action: #selector(buttonClick), for: .touchUpInside)
 }
 
 deinit {
  print("ThirdViewController-被释放了")
 }
 
 @objc private func buttonClick() {
  closure?()
 }
}

当我们连续推到第三个控制器,点击按钮(触发闭包)后,再回到第一个控制器,看一下三个控制器的生命周期。当流程走完后,发现控制台只有一条语句:

调用 test 方法

这说明闭包已经引起了循环引用问题,导致第二个控制器没能被释放(内存泄漏)。正是因为闭包会导致循环引用,所以在闭包中调用对象内部的方法时,都要显式的使用 self,提醒我们要注意可能引起的内存泄漏问题。与 Objective-C 不同的是,我们不需要在每一次使用闭包之前再繁琐的写上 __weak typeof(self) weakSelf = self; 了,取而代之的是捕获列表的概念:

@objc private func buttonClick() { 
 thirdViewController.closure = { [weak self] in 
  self?.test()
 }
 navigationController?.pushViewController(thirdViewController, animated: true)
}

再重复一次上面的流程,可以看到控制台多了两条语句:

调用 test 方法
SecondViewController-被释放了
ThirdViewController-被释放了

只要在捕获列表中声明了你想要用弱引用的方式捕获的对象,就可以及时的规避由闭包导致的循环引用了。但是同时可以看到,闭包中对于方法的调用从常规的 self.test() 变为了可选链的 self?.test()。这是因为假设闭包在子线程中执行,执行过程中 self 在主线程随时有可能被释放。由于 self 在闭包中成为了一个弱引用,因此会自动变为 nil。在 Swift 中,可选类型的概念让我们只能以可选链的方式来调用 test。下面修改一下 ThirdViewController 中的代码:

@objc private func buttonClick() {
 // 模拟网络请求
 DispatchQueue.global().asyncAfter(deadline: DispatchTime.now() + 5) {
  self.closure?()
 }
}

再次执行相同的操作步骤,这次我们发现 test 方法没能正确的得到调用:

SecondViewController-被释放了
ThirdViewController-被释放了

在实际的项目中,这可能会导致一些问题,闭包中捕获的 self 是 weak 的,有可能在闭包执行的过程中就被释放了,导致闭包中的一部分方法被执行了而一部分没有,应用的状态因此变得不一致。于是这个时候就要用到 Weak-Strong Dance 了。

既然知道了 self 在闭包中成为了可选类型,那么除了可选链,还可以使用可选绑定来处理可选类型:

@objc private func buttonClick() { 
 thirdViewController.closure = { [weak self] in 
  if let strongSelf = self {
   strongSelf.test()
  } else {
   // 处理 self 被释放时的情况。
  }
 }
 navigationController?.pushViewController(thirdViewController, animated: true)
}

但这样总是会让我们在闭包中的代码多出两句甚至更多,于是还有更优雅的方法,就是使用 guard 语句:

@objc private func buttonClick() { 
 thirdViewController.closure = { [weak self] in 
  guard let strongSelf = self else { return } 
  strongSelf.test()
 }
 navigationController?.pushViewController(thirdViewController, animated: true)
}

一句代码搞定~

当然,有人看到这里会说,每次都要使用 strongSelf 来调用 self 的方法,好烦啊……那么这一点还是可以进一步被优化的,Swift 与 Objective-C 不同,是可以使用部分关键字来声明变量的,于是我们可以:

@objc private func buttonClick() { 
 thirdViewController.closure = { [weak self] in 
  guard let `self` = self else { return } 
  self.test()
 }
 navigationController?.pushViewController(thirdViewController, animated: true)
}

这样就可以避免每次书写 strongSelf 的烦躁感了~

原文地址:Weak-Strong Dance In Swift——如何在 Swift 中优雅的处理闭包导致的循环引用

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,谢谢大家对脚本之家的支持。

相关文章

  • Swift并发系统并行运行多个任务使用详解

    Swift并发系统并行运行多个任务使用详解

    这篇文章主要为大家介绍了Swift并发系统并行运行多个任务使用详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-06-06
  • Swift使用编解码库Codable的过程详解

    Swift使用编解码库Codable的过程详解

    Codable 是 Swift 引入的全新的编解码库,使开发者更方便的解析JSON 或 plist 文件,支持枚举、结构体和类,这篇文章主要介绍了Swift使用编解码库Codable,需要的朋友可以参考下
    2023-09-09
  • Swift Error重构优化详解

    Swift Error重构优化详解

    这篇文章主要为大家介绍了Swift Error的问题解决及重构优化方案详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-11-11
  • 深入理解Swift中的变量与常量

    深入理解Swift中的变量与常量

    本文主要是介绍Swift中最常用的常量和变量,将从“变量常量的定义”、"如何声明变量常量"、“变量和常量的命名”,"变量常量的本质区别"四个方面入手,重点介绍变量和常量的使用以及区别,希望大家在阅读完本文后都可以熟练使用它们。有需要的朋友们下面来一起学习吧。
    2017-01-01
  • Swift设计思想Result<T>与Result<T, E: Error>类型解析

    Swift设计思想Result<T>与Result<T, E: Error>类型解析

    这篇文章主要为大家介绍了Swift设计思想Result<T>与Result<T, E: Error>的类型示例解析,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-11-11
  • Swift条件判断中逗号的使用方法示例

    Swift条件判断中逗号的使用方法示例

    判断语句是我们日常开发经常会遇到的一个功能,下面这篇文章主要给大家介绍了关于Swift条件判断中逗号的使用方法,文中给出了详细的示例代码供大家参考学习,需要的朋友们下面随着小编来一起学习学习吧。
    2017-10-10
  • OpenStack的Swift组件详解

    OpenStack的Swift组件详解

    这篇文章主要介绍了OpenStack的Swift组件,对swift感兴趣的同学,可以参考下
    2021-04-04
  • 理解二叉堆数据结构及Swift的堆排序算法实现示例

    理解二叉堆数据结构及Swift的堆排序算法实现示例

    二插堆即是完全二叉树,对于排序可以按构建最大堆或最小堆的方式来实现,这里我们就来共同理解二叉堆数据结构及Swift的堆排序算法实现示例
    2016-07-07
  • Swift教程之闭包详解

    Swift教程之闭包详解

    这篇文章主要介绍了Swift教程之闭包详解,闭包可以在上下文的范围内捕获、存储任何被定义的常量和变量引用,因这些常量和变量的封闭性,而命名为“闭包(Closures)”,需要的朋友可以参考下
    2015-01-01
  • Combine中错误处理和Scheduler使用详解

    Combine中错误处理和Scheduler使用详解

    这篇文章主要为大家介绍了Combine中错误处理和Scheduler使用示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-12-12

最新评论