Swift中的内存管理

作者: Sheepy | 来源:发表于2015-12-20 16:00 被阅读6286次

    之前用Swift写了一个App,已经在App Store上架了。前两天更新了一些功能,然后用Instruments检查的时候,发现有内存泄漏问题。有些同学可能觉得奇怪,Swift不是使用ARC自动管理内存的么,怎么也会发生内存泄漏呢。是会的,但几乎都是由于操作不当造成循环引用(strong reference cycle/retain cycle)导致的。

    ARC与GC

    很多人分不清ARC(Automatic Reference Counting,自动引用计数)跟GC(Garbage Collection,垃圾收集)的区别。其实“引用计数法”也算是一种GC策略,只不过我们现在提到GC的时候一般是指基于“标记-整理”策略的垃圾收集器,譬如主流的JVM(Java虚拟机)几乎都是采用“标记-整理”+“分代收集”的策略来进行自动内存管理的。标记算法一般是从全局对象图的“根”出发进行可达性分析,对象的生死会被批量地标记出来,之后再在某个时间批量地释放死对象。显然,这是一种“全局+延时”的管理策略。

    而与之相对的,引用计数是一种“局部+即时”的内存管理策略。它不需要全局的对象信息,一般每个被管理的对象都会跟一个引用计数器关联,这个计数器保存着当前对象被引用的次数,一旦创建一个新的引用指向该对象,引用计数就加1,每当指向该对象的某个引用失效引用计数就减1,直到引用计数为0,就立即释放该对象。使用引用计数法管理内存的语言也不止OC和Swift,还有诸如CPython之类的GC也是基于引用计数的。

    早年OC是采用MRC(手动引用计数)的,当然其实现在也有人还在用,它跟ARC的主要区别在于它需要手动管理引用计数器,而ARC是自动管理的。所以其实MRC也不能让你直接释放对象的,只是控制引用罢了。

    循环引用

    上面解释了一下ARC的运作方式,从中不难看出这种策略的缺陷,就是循环引用问题。看下图:

    reference_cycle.png

    object1和object2之间形成了循环引用,它们的引用计数始终为1,始终不会被释放,这就造成了内存泄漏。“标记-整理”策略并不会出现这种问题,因为哪怕两个对象相互引用,但只要它们和“根”对象失去了联系,照样会被标记为死对象,然后在合适的时间被释放。

    实例分析

    接下来看一个稍微复杂一点的实例,分析一下出现循环引用的原因然后给出解决方法。

    class SimpleRefreshCtrl: UIRefreshControl {
        typealias Action = () -> ()
        
        var action: Action!
        
        init(action: Action) {
            super.init()
            
            tintColor = UIColor.navigationBarColor()
            self.action = action
            self.addTarget(self, action: "refresh", forControlEvents: UIControlEvents.ValueChanged)
        }
    
        required init?(coder aDecoder: NSCoder) {
            fatalError("init(coder:) has not been implemented")
        }
        
        func refresh() {
            self.action()
            delay(seconds: 1) {
                self.endRefreshing()
            }
        }
    }
    

    这是我自己封装的一个下拉刷新控制器,它继承自UIRefreshControl,可以在UITableViewController中直接使用,如下:

    class HouseTableCtrl: UITableViewController {
        //...
        func getPageData() {
            getListFromApi(urlString) { json, nextLink in
                self.houseData = json
                self.page = nextLink
            }
        }
        override func viewDidLoad() {
            super.viewDidLoad()
    
            let refreshCtrl = SimpleRefreshCtrl(action: getPageData)
            self.refreshControl = refreshCtrl
            //...
        }
    }
    

    这样,当你下拉列表的时候,旋转的菊花就会出现旋转1秒,同时执行getPageData方法,刷新页面数据。

    但是这里出现了循环引用问题,我们来看看它是怎么发生的。在getPageData方法中我调用了一个全局函数getListFromApi,而这个全局函数需要一个闭包作为参数,而这个闭包又捕获了当前对象的两个属性,也就持有了当前对象的引用。到这里为止并没有什么问题,虽然闭包捕获外部变量从而持有外部对象的引用经常是造成循环引用的一大元凶,但在这里,该闭包是个匿名闭包,我们的HouseTableCtrl对象并没有持有该闭包的引用,所以问题并不是出在这里。

    接下来,在初始化SimpleRefreshCtrl对象的时候,getPageData作为参数被传递了过去,并被赋值给SimpleRefreshCtrl的实例属性action。注意,getPageData是在HouseTableCtrl中定义的一个实例方法,是跟当前的HouseTableCtrl对象关联的,作为参数传递过去的实际上是self.getPageData。如此一来,SimpleRefreshCtrl对象就持有了当前HouseTableCtrl对象的引用。然后接下来这一句self.refreshControl = refreshCtrl,持有HouseTableCtrl对象引用的SimpleRefreshCtrl对象被赋值给了HouseTableCtrl的实例属性refreshControl,于是HouseTableCtrl对象也持有了SimpleRefreshCtrl对象的引用。这就造成了循环引用。

    要如何打破僵局呢,其实也很简单,使用weak或者unowned就行了:

    //refreshCtrl指向的对象只持有当前HouseTableCtrl对象的一个弱引用
    let refreshCtrl = SimpleRefreshCtrl { [weak self] in
        self?.getPageData()
    }
    //这一句强引用
    self.refreshControl = refreshCtrl
    

    这样SimpleRefreshCtrl对象就只是持有当前HouseTableCtrl对象的一个弱引用,弱引用是不算在HouseTableCtrl对象的引用计数中的,也就是说当没有其他引用指向HouseTableCtrl对象时,HouseTableCtrl对象能被正常释放,一旦HouseTableCtrl对象被释放了,那SimpleRefreshCtrl对象也就能被正常释放了:

    weak_reference.png

    至于weakunowned该用哪个么,看情况了,weak修饰的属性或变量是一个optional类型,也就是说是可以为nil的。而unowned则是修饰一个nonoptional,是不能为nil的,一旦这个属性或变量指向的对象被释放了(这是有可能发生的,因为unowned引用也是不算在引用计数中的,如果除了unowned引用外没有其他引用指向那个对象,那它将被释放),而你还想使用该对象的话,将会触发runtime error,程序也就crash了。所以个人来说,我是更推荐使用weak的。

    相关文章

      网友评论

      • 幸福的脚步2016:let refreshCtrl = SimpleRefreshCtrl(action: getPageData)
        self.refreshControl = refreshCtrl

        let refreshCtrl = SimpleRefreshCtrl { [weak self] in
        self?.getPageData()
        }这两种写法没太看懂,你这是把传值改成block了么,不可以直接用传值的做法做么?
      • 下雨就好:swift还是引用计数的管理方式啊
      • jonqinwu:var action: Action!
        刚开始学swift,不知道这里可不可以加个weak解决问题。
        Sheepy:@n1p_ 不可以,weak 只能用在 class 类型,struct 和 closure 并不是通过 ARC 进行内存管理的。
      • Wws:类似于Oc中的block循环引用?
        Sheepy:@Wws 嗯,ARC的问题都是一样的,OC中也不只是block会引起循环引用问题。
      • 5e17e4fdf014:收藏!!
      • 火星的蝈蝈:赞一个
      • 刀背藏身:从C++带过来的内存泄露的问题,到最后还是用了相同的方式解决。C++里的智能指针也是一样的方式~~
      • 酱油葱:总结很到位,补充介绍一下延时操作使用weak转strong就更好了
        Sheepy:@be47022ef6ea 把异步操作叫做延时操作不太合适啊……你说的这种情况我觉得一般都不太需要特别去考虑,我在文章里也说了weak修饰的变量是可以为nil的,调用的时候像文中一样用可选链就好了,这样的话就算它指向的对象被释放了,程序也不会崩溃,无非是什么都不做罢了。当然也可以在使用weak修饰的变量前先判断它是否为nil,然后再做处理。
        酱油葱:@Sheepy 不是,是延时异步操作后weak引用的对象可能已经被释放了
        Sheepy:@be47022ef6ea 延时操作使用weak转strong?不太明白你的意思,能具体说说是哪种情况么。难道是指使用NSTimer时容易出现的循环引用问题?
      • 00fe1d42e006:虽然不太懂,看看还是涨知识:stuck_out_tongue_winking_eye:
        Sheepy:@迷糊小小姐 我自然是的……
        00fe1d42e006:@Sheepy 不是你是程序猿嘛😜
        Sheepy:@迷糊小小姐 :relaxed: 你是程序媛么

      本文标题:Swift中的内存管理

      本文链接:https://www.haomeiwen.com/subject/caqmhttx.html