Swift中的内存管理

0.114字数 1533阅读 9084

之前用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的。

推荐阅读更多精彩内容