重拾 ObjC 自动释放池

Objc 自动释放池平时很少显式的使用,但其实它时刻在默默为我们工作。关于自动释放池源码分析的文章已经很多了,本文不会在源码层面剖析原理。

初衷

在 MRC 时代,需要使用retainrelease手动维护对象的引用计数,并要遵循「谁创建谁释放」的原则。

然而在某些场景下无法满足这个原则,比如说工厂方法:

+ (id)factory {
    return [self new];
}
复制代码

return处如果调用retain,就需要调用方负责 release,这显然是不科学的设计。所以这里不能retain, 但是不 retain,该对象超出作用域后就会被释放,调用方取到的会是 nil。该如何保证调用方在这个对象超出作用域后,还能取到呢?方法就是自动释放池,在返回前,将该对象被加入自动释放池,这样调用方就能顺利取到返回值了。

那如果对象真的需要被释放了,如何从自动释放池里移除?熟悉 RunLoop 的同学应该知道,在 RunLoop 唤醒和即将睡眠状态之间会被插入自动释放池,每次 RunLoop 迭代都会向本次迭代加入的对象发送一条release 消息。如果对象的引用计数变为 0,便会被释放。

实现和实践

自动释放池虽然被叫做”池“,其实它是一个栈结构。栈的实现方式有很多,自动释放池采用了双向链表,能够比较方便的实现 push 和 pop。

自动释放池之所有用栈实现,而不用其他数据结构,比如说散列表?是因为 autorelease 对象往往需要批量处理,比如说一次 RunLoop 迭代生成一大批的 autorelease 对象。

所以这里就出现一个问题,假如某一段代码在一次 RunLoop 周期内生成大量的 autorelease 对象,还没有等到迭代结束清理,就已经内存溢出了,该怎么办?

这是就需要手动的来触发 autorelease 对象的释放。@autoreleasepool{}登场,它能够控制 autorelease 对象释放的颗粒度。

{
	for (NSInteger i = 0; i < 10000; i++) {
		@autoreleasepool {
		    NSImage *img = [NSImage imageNamed:@"aimge"];
		}
	}
}
复制代码

上述的极端例子,如果for循环中不嵌套 autoreleasepool,在 Xcode 侧边栏的 Debug Session,能看到应用的占用的内存不断的增加。

在嵌套使用 autoreleasepool 的场景,并且需要由内而外逐层清理,所以使用栈最适合不过了。

钉子和锤子

以前面试几乎每次都会被问对象是什么时候释放的,一般的回答是引用计数为 0,但这只是站在对象的角度考虑的,那什么时候对象的引用计数会变为 0 呢?

因为了解过自动释放池,所以会说是在 autoreleasepool pop 的时候,如果没有手动的添加 autoreleasepool 便会在 RunLoop 迭代的时候引用计数被减为 0 时释放。

其实回答的有些片面,并不是所有对象都是在 pop 的时候引用计数才会为 0 的,普通局部对象其实在超出作用域时(大括号)引用计数为 0,就会被立即释放。

  1. 普通局部对象 局部对象在超出作用域并且引用计数为 0 时会立即释放。
override func viewDidLoad() {
	super.viewDidLoad()
	let o = NSObject()
}
// `o` has release
复制代码
  1. autorelease 对象
override func viewDidLoad() {
	super.viewDidLoad()
	let imgO = UIImage(named: “image”);
}
// imgO 还未释放,会等到 autoreleasepool pop 才释放。
// 可使用 weak 指针测试,在 viewWillApper 方法该对象仍然存在
// 或使用符号断点,检测 AutoreleasePoolPage::autorelease() 方法被调用
复制代码

猜你喜欢

转载自juejin.im/post/5c8dbb1ae51d455b9c2b6a41