How to strongly apply self in the closure in Swift to prevent the delayed operation in the closure from not being able to obtain self

Weak-Strong Dance In Swift - How to gracefully handle circular references caused by closures in Swift

As a very old language, Objective-C is deeply loved by iOS developers after adding the Block feature. In Swift, the corresponding concept is called Closure, which is closure. Although the name has been changed, the concept and usage are still similar, even if the side effects are the same, which may lead to circular references.

Let's take a look at an example, first we need the first controller ( FirstViewController), all it does is simply push the second controller ( 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)
    }
}

Below is  SecondViewController the code. SecondViewController What it does is launch a third controller ( ThirdViewController), which, differently, thirdViewController exists as a property, and it also has a closure  closure , which we use to test for circular reference issues. Also implements  deinit methods to print a statement to see if the controller has been released.

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 方法")
    }
}

Next we look at  ThirdViewController the code. There  ThirdViewController is a button in there, and a click triggers the closure. We also implemented  deinit methods to print a statement to see if the controller was released.

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?()
    }
}

When we continuously push to the third controller, click the button (trigger the closure), then go back to the first controller and look at the life cycle of the three controllers. When the process is finished, it is found that there is only one statement in the console:

调用 test 方法

This means that the closure has caused a circular reference problem, causing the second controller to not be released (memory leak). It is precisely because the closure will cause circular references, so when calling the method inside the object in the closure, it must be used explicitly  self, reminding us to pay attention to the possible memory leak problem. The  Objective-C difference is that we don't need to write cumbersomely before each use of the closure  __weak typeof(self) weakSelf = self; , but instead use the concept of a capture list:

@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 的烦躁感了~

原文地址:https://github.com/yangxiaoju/Blogs/blob/master/iOS/Swift/Weak-Strong%20Dance%20In%20Swift——如何在%20Swift%20中优雅的处理闭包导致的循环引用.md

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=324839462&siteId=291194637