Swift闭包中的内存泄漏

Cover

内存泄漏在 iOS 中是永恒的话题,若是你在开发过程当中不当心对待的话,那么总有一天他会以 Crash 的形式提醒你它的存在。内存泄漏不只破坏用户体验,并且会影响性能甚至应用的安全。既然内存泄漏如此的重要,因此这篇文章在这篇文章将说一说 Swift 闭包中的内存泄漏问题。html

Apple 在文章中详细介绍了循环强引用的概念、何为内存泄漏、如何避免。可是文章中的实例太过于简单,在真正的应用过程当中状况远比这个复杂,接下来的内容就是介绍其中最为复杂的闭包中的泄露分析。api

闭包中的引用

首先,咱们须要清楚的理解闭包的概念:闭包是自包含的函数代码块,能够在代码中被传递和使用。简单来讲:闭包是一段可执行的代码块而且它能自动捕获上下文的变量和常量,而后在须要的时候被执行。详细内容可参见地址安全

咱们从这个简单的实例开始:ViewController 中有一个 CustomView 类型的成员属性变量,同时 CustomView 有一个点击事件的闭包函数 onTap网络

class CustomView:UIView{ 
    var onTap:(()->Void)?
    ...
}

class ViewController:UIViewController{ 
    let customView = CustomView() 
    var buttonClicked = false

    func setupCustomView(){
        var timesTapped = 0
        customView.onTap = { _ in 
            timesTapped += 1 
            print("button tapped \(timesTapped) times")
            self.buttonClicked = true
        }
    }
}

在给闭包函数 onTap 赋值的语句中咱们对 buttonClicked 进行了赋值,这就致使了对 self 的强引用。可是咱们仔细思考后就不难发现其中的问题: self 引用了 customView 变量,而后 customView 变量的饮用了 onTap 闭包,最后 onTap 闭包引用了 self 。其结果相似下图:闭包

Reference cycles with closures

上图中你能清晰的看见循环结构,这致使程序退出的时候不能正常的销毁内存致使内存泄漏的发生。app

隐藏的循环

除了上面那种明显的循环引用有些闭环隐藏的更深也更隐蔽。解决这个问题的关键就是:在对闭包赋值的时候问本身谁是闭包的拥有者,而后向上溯源到根节点。异步

下面咱们来看最多见 UITableView 中隐藏的循环(最多见的每每越容易被忽略)。通常状况下咱们都是在 UIViewController 中新建 UITableView 实例少数状况下也会使用 UITableViewController ,可是无论哪一种情形咱们都会新建自定义的 UITableViewCellasync

下面的代码中咱们新建了一个名为 CustomCellUITableViewCell 子类,该类中包含了一个 UIButton实例属性以及按键点击事件的闭包属性 onButtonTapide

class CustomCell: UITableViewCell {

    @IBOutlet weak var customButton: UIButton!
    var onButtonTap:(()->Void)?

    @IBAction func buttonTap(){
        onButtonTap?()
    }
}

而后咱们在 ViewController 对该闭包赋值:函数

class ViewController: UITableViewController {
    
    ...
    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "CustomCell", for: indexPath) as! CustomCell
        cell.onButtonTap = { _ in
            self.navigationController?.pushViewController(NewViewController(), animated: true)
        }
    }        
}

这里咱们对 onButtonTap闭包进行溯源:谁拥有该闭包?毫无疑问是 CustomCell类的实例 cell。而 cell 又是属于 tableViewtableView又属于 self 所表明的UITableViewController 实例。

正以下图表现的那样,这里也有一个循环引用,只不过度析路线更长因此显得更隐蔽。

TableViewController retain cycle

GCD 中的闭包分析

若是你之前用过 GCD 的话,那么你能一眼判断下面代码是否有循环引用。

override func viewDidLoad() {
    super.viewDidLoad()
    DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
        self.navigationController?.pushViewController(NewViewController())
    }
}

一样的使用溯源法分析闭包:拥有闭包的对象是 DispatchQueue 单例,该单例并不被 ViewController 任何属性所引用,但DispatchQueue 单例的闭包中却持有了 self。虽然咱们不知道该单例的具体实现,可是咱们清楚该异步闭包会在2s后被执行一次,执行完成以后该闭包就会释放对 self 的引用。因此咱们由此能够判定这段闭包代码是不存在循环引用问题的。

这部分的代码逻辑和分析一样适用于 UIView 的动画闭包函数中

Alamofire 中的闭包

Alamofire 能够说是 Swift 网络处理中最经常使用的第三方库了,其中的请求处理中一样涉及到闭包函数。下面这段代码是请求登录接口:

Alamofire.request("https://yourapi.com/login", method: .post, parameters: ["email":"test@gmail.com","password":"1234"]).responseJSON { (response:DataResponse<Any>) in
    if response.response?.statusCode == 200 {
        self.navigationController?.pushViewController(NewViewController(), animated: true)
    } else {
        //Show alert
    }
}

上诉代码中的闭包又是属于哪一个对象?这里咱们须要深刻 Alamofire 的实现中去探寻。首先 request方法会返回一个 DataRequest类型对象,而该对象的 responseJSON方法中将闭包做为参数 completionHandler传入,最后该闭包存入了 OperationQueue 类型的队列 queue 中,闭包执行完成后会自动从队列中移除。由此咱们可知:闭包被 queue所持有而且一次执行后就移除了,此处不存在循环引用。

循环引用的解决

为了打破循环引用带来的内存泄漏问题,根本途径就是破坏该循环,将某个对象对另外一个对象的强引用去除。在闭包环境的循环问题,咱们都倾向于将闭包中的强引用去除,毕竟这简单并且看起来更直观。

为了实现该目的,咱们在闭包捕获的上下文变量中作文章。咱们使用关键词 weakunowned 来打破循环。例如上文中提到的 UITableView

cell.onButtonTap = { [unowned self] in
    self.navigationController?.pushViewController(NewViewController(), animated: true)
}

上诉两个关键词存在着明显的区别 weak 是可选值而 unowned 则必定不为可选值,换句话说 weak 关键词所指对象可能为 nilunowned 则必定不能是 nil,所以在选用的时候须要认真考虑一下。通常来讲若是闭包生命周期不长于其捕获的上下文变量的生命周期咱们会使用 unowned,不然咱们选择 weak

内存泄漏的调试

上面咱们分析了大部分闭包中的循环引用问题,咱们得知并非全部的状况下都会致使内存泄漏。若是在咱们使用了第三方库尤为是一些私有实现库的状况下,这部分的分析在代码层面将变的很困难而且工做量很大。好在Xcode为咱们提供的调试工具,在工程运行的状况下,咱们在调试区域能够找到以下图所示按键:

Debug

UITableView 的示例中,若是咱们移除闭包中的 unowned 或者 weak 的话,你就能在左侧看见下图

Debug Leak

上图中的左侧感叹号代表了这里存在着内存泄漏的状况,这样你就要去查看代码了。固然你又内存泄漏可是没有感叹号标记的状况也是彻底有可能的,此时你就要启用内存分析工具了而且分析内存中的对象,这些对象是否应该存在。

相关文章
相关标签/搜索