Swift中优雅处理闭包导致的循环引用详解
前言
Objective-C作为一门资历很老的语言,添加了Block这个特性后深受广大iOS开发者的喜爱。在Swift中,对应的概念叫做Closure,即闭包。虽然更换了名字,但是概念和用法还是相似的,就算是副作用也一样,有可能导致循环引用。
下面我们用一个例子看一下,首先我们需要第一个控制器(FirstViewController),它所做的就是简单的推出第二个控制器(SecondViewController)。
classFirstViewController:UIViewController{
privateletbutton:UIButton={
letbutton=UIButton()
button.setTitleColor(UIColor.black,for:.normal)
button.setTitle("跳转到SecondViewController",for:.normal)
button.sizeToFit()
returnbutton
}()
overridefuncviewDidLoad(){
super.viewDidLoad()
button.center=view.center
view.addSubview(button)
button.addTarget(self,action:#selector(buttonClick),for:.touchUpInside)
}
@objcprivatefuncbuttonClick(){
letsecondViewController=SecondViewController()
navigationController?.pushViewController(secondViewController,animated:true)
}
}
下面是SecondViewController的代码。SecondViewController所做的事情是推出第三个控制器(ThirdViewController),不同的是,thirdViewController是作为一个属性存在的,同时它还有一个闭包closure,这是我们用来测试循环引用问题的。还实现了deinit方法,用来打印一条语句,看该控制器是否被释放了。
classSecondViewController:UIViewController{
privateletthirdViewController=ThirdViewController()
privateletbutton:UIButton={
letbutton=UIButton()
button.setTitleColor(UIColor.black,for:.normal)
button.setTitle("跳转到ThirdViewController",for:.normal)
button.sizeToFit()
returnbutton
}()
overridefuncviewDidLoad(){
super.viewDidLoad()
button.center=view.center
view.addSubview(button)
button.addTarget(self,action:#selector(buttonClick),for:.touchUpInside)
}
deinit{
print("SecondViewController-被释放了")
}
@objcprivatefuncbuttonClick(){
thirdViewController.closure={
self.test()
}
navigationController?.pushViewController(thirdViewController,animated:true)
}
privatefunctest(){
print("调用test方法")
}
}
接下来我们看一下ThirdViewController的代码。在ThirdViewController中有一个按钮,点击一下就会触发闭包。同时我们还实现了deinit方法,用来打印一条语句,看该控制器是否被释放了。
classThirdViewController:UIViewController{
privateletbutton:UIButton={
letbutton=UIButton()
button.setTitleColor(UIColor.black,for:.normal)
button.setTitle("点击按钮",for:.normal)
button.sizeToFit()
returnbutton
}()
varclosure:(()->Void)?
overridefuncviewDidLoad(){
super.viewDidLoad()
button.center=view.center
view.addSubview(button)
button.addTarget(self,action:#selector(buttonClick),for:.touchUpInside)
}
deinit{
print("ThirdViewController-被释放了")
}
@objcprivatefuncbuttonClick(){
closure?()
}
}
当我们连续推到第三个控制器,点击按钮(触发闭包)后,再回到第一个控制器,看一下三个控制器的生命周期。当流程走完后,发现控制台只有一条语句:
调用test方法
这说明闭包已经引起了循环引用问题,导致第二个控制器没能被释放(内存泄漏)。正是因为闭包会导致循环引用,所以在闭包中调用对象内部的方法时,都要显式的使用self,提醒我们要注意可能引起的内存泄漏问题。与Objective-C不同的是,我们不需要在每一次使用闭包之前再繁琐的写上__weaktypeof(self)weakSelf=self;了,取而代之的是捕获列表的概念:
@objcprivatefuncbuttonClick(){
thirdViewController.closure={[weakself]in
self?.test()
}
navigationController?.pushViewController(thirdViewController,animated:true)
}
再重复一次上面的流程,可以看到控制台多了两条语句:
调用test方法
SecondViewController-被释放了
ThirdViewController-被释放了
只要在捕获列表中声明了你想要用弱引用的方式捕获的对象,就可以及时的规避由闭包导致的循环引用了。但是同时可以看到,闭包中对于方法的调用从常规的self.test()变为了可选链的self?.test()。这是因为假设闭包在子线程中执行,执行过程中self在主线程随时有可能被释放。由于self在闭包中成为了一个弱引用,因此会自动变为nil。在Swift中,可选类型的概念让我们只能以可选链的方式来调用test。下面修改一下ThirdViewController中的代码:
@objcprivatefuncbuttonClick(){
//模拟网络请求
DispatchQueue.global().asyncAfter(deadline:DispatchTime.now()+5){
self.closure?()
}
}
再次执行相同的操作步骤,这次我们发现test方法没能正确的得到调用:
SecondViewController-被释放了
ThirdViewController-被释放了
在实际的项目中,这可能会导致一些问题,闭包中捕获的self是weak的,有可能在闭包执行的过程中就被释放了,导致闭包中的一部分方法被执行了而一部分没有,应用的状态因此变得不一致。于是这个时候就要用到Weak-StrongDance了。
既然知道了self在闭包中成为了可选类型,那么除了可选链,还可以使用可选绑定来处理可选类型:
@objcprivatefuncbuttonClick(){
thirdViewController.closure={[weakself]in
ifletstrongSelf=self{
strongSelf.test()
}else{
//处理self被释放时的情况。
}
}
navigationController?.pushViewController(thirdViewController,animated:true)
}
但这样总是会让我们在闭包中的代码多出两句甚至更多,于是还有更优雅的方法,就是使用guard语句:
@objcprivatefuncbuttonClick(){
thirdViewController.closure={[weakself]in
guardletstrongSelf=selfelse{return}
strongSelf.test()
}
navigationController?.pushViewController(thirdViewController,animated:true)
}
一句代码搞定~
当然,有人看到这里会说,每次都要使用strongSelf来调用self的方法,好烦啊……那么这一点还是可以进一步被优化的,Swift与Objective-C不同,是可以使用部分关键字来声明变量的,于是我们可以:
@objcprivatefuncbuttonClick(){
thirdViewController.closure={[weakself]in
guardlet`self`=selfelse{return}
self.test()
}
navigationController?.pushViewController(thirdViewController,animated:true)
}
这样就可以避免每次书写strongSelf的烦躁感了~
原文地址:Weak-StrongDanceInSwift——如何在Swift中优雅的处理闭包导致的循环引用
总结
以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,谢谢大家对毛票票的支持。