如何在 Swift 中优雅的处理闭包致使的循环引用

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

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

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 方法,用来打印一条语句,看该控制器是否被释放了。bash

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; 了,取而代之的是捕获列表的概念:async

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

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

调用 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-被释放了
复制代码

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

既然知道了 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 的方法,好烦啊……那么这一点仍是能够进一步被优化的,SwiftObjective-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 中优雅的处理闭包致使的循环引用

若是以为我写的还不错,请关注个人微博@小橘爷,最新文章即时推送~

相关文章
相关标签/搜索