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 中优雅的处理闭包导致的循环引用

总结

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

(0)

相关推荐

  • Swift中闭包实战案例详解

    前言 无论苹果的官方文档还是由官方文档衍生出来的一些文章和书籍都比较重视基础语法知识的讲解,对于实战中的应用提及的都很少,所以当我们想使用"闭包"解决一些问题的时候,会忽然出现看着一堆理论知识却不知从何下手的尴尬感,这就是理论和时实战的区别了. 本文不赘述Swift闭包的的基本语法了,百度或者Google下有很多资料.如题所示本文着重讲述Swift闭包的一些实战案例,有需要的小伙伴可以参考下,经验丰富的大神也请指教. 关于如何理解闭包 学习闭包的第一个难点就是理解闭包,可能很多人用了很

  • IOS swift3.0 下闭包语法整理

    IOS swift3.0 下闭包语法整理 一.闭包的概念 有oc基础的都知道,闭包其实是oc里面的block,语法格式不一样,但作用是一样的.主要是用于callBack(异步回调)或者两个类之间的通信.它的本质一个函数,一个可执行的代码块,只是这个函数是没有名字的,也就是匿名函数.你也可以把他看作如 int.float一样,是一种数据类型,一种可以作为参数传递的数据类型. 二.基本语法 1.闭包的声明 //定义一个求和闭包 //闭包类型:(Int,Int)->(Int) let add:(Int

  • Swift 中闭包的简单使用

    本文主要是介绍Swift中闭包的简单使用,将从"闭包的定义"."闭包的创建.赋值.调用"."闭包常见的几种使用场景","使用闭包可能引起的循环强引用" 四个方面入手,重点介绍闭包如何使用,没有高深的概念,只是专注于实际使用,属于入门级水平,后面还会有关于闭包更加详细和深入理解的文章.希望大家在阅读完本文后能够对闭包有一个整体的理解以及能够简单的使用它. 闭包的定义 在Swift开发文档中是这样介绍闭包的:闭包是可以在你的代码中

  • 深入理解Swift语言中的闭包机制

    在 Swift 中的闭包类似于结构块,并可以在任何地方调用,它就像 C 和 Objective C 语言内置的函数. 函数内部定义的常数和变量引用可被捕获并存储在闭包.函数被视为封闭的特殊情况,它有 3 种形式. 在 Swift 语言闭合表达式,如下优化,重量轻语法风格,其中包括: 推导参数并从上下文菜单返回值的类型 从单封表达的隐性返回 简略参数名称 尾部闭包语法 语法 下面是一个通用的语法定义用于闭包,它接受参数并返回数据的类型: 复制代码 代码如下: {(parameters) -> re

  • Swift教程之闭包详解

    闭包(Closures)是独立的函数代码块,能在代码中传递及使用.Swift中的闭包与C和Objective-C中的代码块及其它编程语言中的匿名函数相似. 闭包可以在上下文的范围内捕获.存储任何被定义的常量和变量引用.因这些常量和变量的封闭性,而命名为"闭包(Closures)".Swift能够对所有你所能捕获到的引用进行内存管理. NOTE 假如你对"捕获(capturing)"不熟悉,请不要担心,具体可以参考Capturing Values(捕获值). 全局函数

  • Objective-C中的block与Swift中的尾随闭包使用教程

    前言 在项目开发中经常会去查iOS闭包怎么写,因为它的语法太古怪,两种语言写法不一,经常搞混,干脆记录下常用的写法算了 闭包定义 闭包是指可以包含自由(未绑定到特定对象)变量的代码块:这些变量不是在这个代码块内或者任何全局上下文中定义的,而是在定义代码块的环境中定义(局部变量)."闭包" 一词来源于以下两者的结合:要执行的代码块(由于自由变量被包含在代码块中,这些自由变量以及它们引用的对象没有被释放)和为自由变量提供绑定的计算环境(作用域). OC中的block与Swift中的尾随闭包

  • swift闭包和OC block类型的使用

    之前看过一段swift,一直不知道OC中的block,即swift中的闭包是怎么实现的.今天就在网上搜索了一下,同时对比了一下OC中block类型的实现方法,然后写了一个Demo测试一下. 使用说明: swift版本 1.声明类型 typealias hideShowView = (Int) -> Void 2.声明属性 var muFunc:hideShowView? 3.传递参数 func didSelectedToHideView(hideFunc:@escaping (Int)->Vo

  • Swift 3.0基础学习之闭包

    前言 闭包是功能性自包含模块,可以在代码中被传递和使用. Swift 中的闭包与 C 和 Objective-C中的 blocks 以及其他一些编程语言中的 lambdas 比较相似.下面这篇文章就来详细介绍了关于Swift 3.0中的闭包,感兴趣的一起来看看吧. 开始 闭包的书写格式如下: { (parameters) -> return type in statements } 如 reversedNames = names.sorted(by: { (s1: String, s2: Str

  • 详解Swift中的函数及函数闭包使用

    一.引言 函数是有特定功能的代码段,函数会有一个特定的名称调用时来使用.Swift提供了十分灵活的方式来创建与调用函数.事实上在Swift,每个函数都是一种类型,这种类型由参数和返回值来决定.Swift和Objective-C的一大区别就在于Swift中的函数可以进行嵌套. 而Swift中的闭包是有一定功能的代码块,这十分类似于Objective-C中的block语法.Swift中的闭包语法风格十分简洁,其作用和函数的作用相似. 二.函数的创建与调用 函数通过函数名,参数和返回值来定义,参数和返

  • iOS开发中Swift逃逸闭包知识

    逃逸闭包必须满足下面2个条件: 1.闭包作为一个参数传到函数中 2.闭包在函数返回之后才执行 需要在参数前面加入标注: @escaping,用来指明这个闭包是允许"逃逸"出这个函数的. 注意:将一个闭包标记为 @escaping 意味着你必须在闭包中显式地引用 import UIKit /** 逃逸闭包满足下面2个条件: * 1.handle闭包作为一个参数传到函数payRequest中 * 2.并且handle闭包在函数返回之后才执行 * 需要在参数前面加入标注: @escaping

随机推荐