之前用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.pngobject1和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
和unowned
该用哪个么,看情况了,weak
修饰的属性或变量是一个optional类型,也就是说是可以为nil的。而unowned
则是修饰一个nonoptional,是不能为nil的,一旦这个属性或变量指向的对象被释放了(这是有可能发生的,因为unowned引用也是不算在引用计数中的,如果除了unowned引用外没有其他引用指向那个对象,那它将被释放),而你还想使用该对象的话,将会触发runtime error,程序也就crash了。所以个人来说,我是更推荐使用weak
的。
网友评论
self.refreshControl = refreshCtrl
let refreshCtrl = SimpleRefreshCtrl { [weak self] in
self?.getPageData()
}这两种写法没太看懂,你这是把传值改成block了么,不可以直接用传值的做法做么?
刚开始学swift,不知道这里可不可以加个weak解决问题。