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

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

闭包中的引用

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

我们从这个简单的实例开始: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 。其结果类似下图:

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

隐藏的循环

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

下面我们来看最常见 UITableView 中隐藏的循环(最常见的往往越容易被忽略)。一般情况下我们都是在 UIViewController 中新建 UITableView 实例少数情况下也会使用 UITableViewController ,但是不管哪种情形我们都会新建自定义的 UITableViewCell

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

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 实例。

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

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为我们提供的调试工具,在工程运行的情况下,我们在调试区域可以找到如下图所示按键:

UITableView 的示例中,如果我们移除闭包中的 uNowned 或者 weak 的话,你就能在左侧看见下图

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

Swift闭包中的内存泄漏的更多相关文章

  1. ios – Xcode找不到Alamofire,错误:没有这样的模块’Alamofire’

    我正在尝试按照github(https://github.com/Alamofire/Alamofire#cocoapods)指令将Alamofire包含在我的Swift项目中.我创建了一个新项目,导航到项目目录并运行此命令sudogeminstallcocoapods.然后我面临以下错误:搜索后我设法通过运行此命令安装cocoapodssudogeminstall-n/usr/local/bin

  2. ios – UITableView和Cell Reuse

    这是我的CustomCell类的init方法解决方法如果没有要显示的图像,则必须清除图像视图:

  3. ios – fetchedResultsController.fetchedObjects.count = 0但它充满了对象

    我正在使用相当标准的fetchedResultsController实现来输出tableView.在-viewDidLoad的最后,我正在进行第一次调用:这是我的fetchedResultsController:我的tableView方法:所以,问题是:在_fetchedResultsController.fetchedobjects.count的日志中等于0,但在视觉上tableView充满了对

  4. ios – UITableView在滚动时阻止重新加载

    或者你能想象一个防止这种行为的好方法吗?解决方法抱歉,我没有足够的声誉来添加评论,因此在单独的答案中回答您的上一个问题.-performSelector:withObject:afterDelay:延迟为0.0秒不会立即执行给定的选择器,而是在当前的RunloopCycle结束后和给定的延迟之后执行它.-performSelector:withObject:添加到当前Runloop循环中并执行.这与直接调用该方法相同.因此,使用-performSelector:withObject:afterDelay:

  5. ios – 在Swift中通过标记访问UITableViewCell内部的不同视图

    我正在尝试使用swift为iOS8制作应用程序.这里的目标是制作一种新闻源.此Feed显示来自用户的帖子,其遵循特定模式.我想过使用UITableView,其中每个单元格都遵循自定义布局.当我尝试访问其中的文本标签时出现问题.我尝试通过它的标签访问它,但是当我这样做时,整个应用程序崩溃了.报告的错误是“Swift动态转换失败”,我使用以下代码访问视图:难道我做错了什么?解决方法我认为问题是标签0.所有视图都是默认值0.所以尝试另一个标签值.

  6. ios – 如何实现`prepareForReuse`?

    解决方法尝试将此添加到您的MGSwipeTableCell.m:

  7. ios – Swift中的UIView动画不起作用,错误的参数错误

    我正在尝试制作动画并使用下面的代码.我得到“无法使用类型’的参数列表调用’animateWithDuration'(FloatLiteralConvertible,延迟:FloatLiteralConvertible,选项:UIViewAnimationoptions,动画:()–>()–>$T4,完成:(Bool)–>(Bool)–>$T5)’“错误.这意味着我使用了错误的参数.我错了.请

  8. ios – 在UITableView上轻扫以删除以使用UIPanGestureRecognizer

    我使用以下代码将UIPanGuestureRecognizer添加到整个视图中:在主视图中我有一个UITableView,它有这个代码来启用滑动删除功能:只有RUNNING1打印到日志中,并且“删除”按钮不会显示.我相信其原因是UIPanGestureRecognizer,但我不确定.如果这是正确的,我该如何解决这个问题.如果这不正确,请提供原因并解决.谢谢.解决方法从document:Ifage

  9. viewWillAppear vs Viewdidload ios

    使用iOS导航应用程序的代码时,我遇到了麻烦:我在哪里可以为UITableView设置方法“initdata”?请帮帮我.解决方法您可以根据应用程序的需求放置initData,如果您的表需要每次使用新数据加载数据,那么它应该在否则,如果表需要通过单个数据重新加载,该数据不会发生变化或者没有对数据执行任何编辑操作,则应使用

  10. ios tableView reloadRowsAtIndexPaths无效

    解决方法包裹它怎么样?希望这可以帮助.

随机推荐

  1. Swift UITextField,UITextView,UISegmentedControl,UISwitch

    下面我们通过一个demo来简单的实现下这些控件的功能.首先,我们拖将这几个控件拖到storyboard,并关联上相应的属性和动作.如图:关联上属性和动作后,看看实现的代码:

  2. swift UISlider,UIStepper

    我们用两个label来显示slider和stepper的值.再用张图片来显示改变stepper值的效果.首先,这三个控件需要全局变量声明如下然后,我们对所有的控件做个简单的布局:最后,当slider的值改变时,我们用一个label来显示值的变化,同样,用另一个label来显示stepper值的变化,并改变图片的大小:实现效果如下:

  3. preferredFontForTextStyle字体设置之更改

    即:

  4. Swift没有异常处理,遇到功能性错误怎么办?

    本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容,请发送邮件至dio@foxmail.com举报,一经查实,本站将立刻删除。

  5. 字典实战和UIKit初探

    ios中数组和字典的应用Applicationschedule类别子项类别名称优先级数据包contactsentertainment接触UIKit学习用Swift调用CocoaTouchimportUIKitletcolors=[]varbackView=UIView(frame:CGRectMake(0.0,0.0,320.0,CGFloat(colors.count*50)))backView

  6. swift语言IOS8开发战记21 Core Data2

    上一话中我们简单地介绍了一些coredata的基本知识,这一话我们通过编程来实现coredata的使用。还记得我们在coredata中定义的那个Model么,上面这段代码会加载这个Model。定义完方法之后,我们对coredata的准备都已经完成了。最后强调一点,coredata并不是数据库,它只是一个框架,协助我们进行数据库操作,它并不关心我们把数据存到哪里。

  7. swift语言IOS8开发战记22 Core Data3

    上一话我们定义了与coredata有关的变量和方法,做足了准备工作,这一话我们来试试能不能成功。首先打开上一话中生成的Info类,在其中引用头文件的地方添加一个@objc,不然后面会报错,我也不知道为什么。

  8. swift实战小程序1天气预报

    在有一定swift基础的情况下,让我们来做一些小程序练练手,今天来试试做一个简单地天气预报。然后在btnpressed方法中依旧增加loadWeather方法.在loadWeather方法中加上信息的显示语句:运行一下看看效果,如图:虽然显示出来了,但是我们的text是可编辑状态的,在storyboard中勾选Editable,再次运行:大功告成,而且现在每次单击按钮,就会重新请求天气情况,大家也来试试吧。

  9. 【iOS学习01】swift ? and !  的学习

    如果不初始化就会报错。

  10. swift语言IOS8开发战记23 Core Data4

    接着我们需要把我们的Rest类变成一个被coredata管理的类,点开Rest类,作如下修改:关键字@NSManaged的作用是与实体中对应的属性通信,BinaryData对应的类型是NSData,CoreData没有布尔属性,只能用0和1来区分。进行如下操作,输入类名:建立好之后因为我们之前写的代码有些地方并不适用于coredata,所以编译器会报错,现在来一一解决。

返回
顶部