Swift中如何在闭包中在对self进行强应用防止闭包中的延时操作获取不到self

Weak-Strong Dance In 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 的烦躁感了~

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

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Swift闭包是一种自包含的功能块,类似于函数,但与函数不同,它们可以在任何地方使用,并且可以捕获并存储其所在上下文的任何常量和变量。闭包可以作为参数传递给函数,也可以从函数返回,并且它们可以在不创建命名函数的情况下使用。 闭包的语法通常包含花括号,它们用于包装一个或多个语句,表示代码块。闭包可以带有参数,这些参数可以是具有类型标注的常量或变量。闭包可以返回值,这些值可以是任何类型,包括函数类型。 下面是一个简单的闭包示例,它接受两个Int类型的参数并返回它们的和: ``` let sum = { (a: Int, b: Int) -> Int in return a + b } ``` 在上面的例子,我们使用花括号表示一个代码块,然后使用参数列表`(a: Int, b: Int)`定义参数,最后使用`-> Int`定义返回类型。代码块的最后一行使用`return`语句返回结果。 闭包还有另外一种更简洁的语法形式,称为尾随闭包。尾随闭包是在函数调用的括号之外写的闭包,这样可以提高代码的可读性。例如: ``` func doSomething(completion: () -> Void) { // 做一些事情... completion() } doSomething { // 这是一个尾随闭包 print("完成") } ``` 在上面的示例,我们将一个没有参数和返回值的闭包作为`doSomething`函数的参数传递,然后使用尾随闭包的语法形式将闭包作为函数调用的最后一个参数传递。在这个闭包,我们只是简单地打印一条消息。 闭包还可以捕获并存储其所在上下文的常量和变量。这被称为闭包的捕获语义。例如: ``` func makeCounter() -> () -> Int { var count = 0 return { count += 1 return count } } let counter1 = makeCounter() let counter2 = makeCounter() print(counter1()) // 输出: 1 print(counter1()) // 输出: 2 print(counter2()) // 输出: 1 print(counter2()) // 输出: 2 ``` 在上面的示例,我们定义了一个`makeCounter`函数,该函数返回一个没有参数和返回值的闭包。该闭包有一个`count`变量,该变量在每次调用闭包时递增。由于闭包可以捕获其所在上下文的变量,因此每个返回的闭包都会共享相同的`count`变量。所以,当我们通过`counter1`和`counter2`调用闭包时,它们都会递增`

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值