本文是经过翻阅博客、论坛学习以及代码调试的总结,如有疑惑或不准确的地方,欢迎评论沟通指正。
通读本文你将理解:
- 自动释放池底层结构
- 自动释放池何时释放(换言之autorelease何时执行release操作)
- ARC下什么样的初始化方法系统会为我们做一次autorelease操作
Objc源码下载地址https://opensource.apple.com/tarballs/objc4/
一、自动释放池底层结构
总结一句话就是:以栈为节点,以双向链表形式组合而成的一个数据结构。通俗一些讲自动释放池是以多个AutoreleasePoolPage为结点,通过链表的方式串连起来的结构,这一整串就是自动释放池。
- magic 用于对当前 AutoreleasePoolPage 完整性的校验
- thread 保存了当前页所在的线程
- id *next指针作为游标指向栈顶最新add进来的autorelease对象的下一个位置
-
每个AutoreleasePoolPage大小均为4096
二、自动释放池何时释放
//ARC 环境
- (void)viewDidLoad {
[super viewDidLoad];
@autoreleasepool {
YMObject *object = [[YMObject alloc] init];
};
}
实际的函数调用是这样的
需要注意的是,整个程序中push和pop的操作都是一一对应的,下面会说明
来分析一下 objc_autoreleasePoolPush和objc_autoreleasePoolPop这两个函数做了什么
void *
objc_autoreleasePoolPush(void)
{
return AutoreleasePoolPage::push();
}
void
objc_autoreleasePoolPop(void *ctxt)
{
AutoreleasePoolPage::pop(ctxt);
}
void *
_objc_autoreleasePoolPush(void)
{
return objc_autoreleasePoolPush();
}
void
_objc_autoreleasePoolPop(void *ctxt)
{
objc_autoreleasePoolPop(ctxt);
}
Objc4源码中可以看到实际上都是AutoreleasePoolPage类的push和pop函数。Push函数中会调用一个比较核心的方法: autoreleaseFast
//这个函数的作用就是,找到最顶层的一个AutoreleasePoolPage对象,如果没有那就创建一个;如果找到了,判断他是否已经装满了full(),因为一个AutoreleasePoolPage只有4096个字节大小,如果满了那就会调用autoreleaseNoPage()创建一个AutoreleasePoolPage对象并添加add;如果没满则直接执行add(obj)。
static inline id *autoreleaseFast(id obj)
{
AutoreleasePoolPage *page = hotPage(); //hotPage()函数会对应线程去取自动释放池,这里也可以看出释放池和线程是一一对应的关系
if (page && !page->full()) {
return page->add(obj);
} else if (page) {
return autoreleaseFullPage(obj, page);
} else {
return autoreleaseNoPage(obj);
}
}
需要注意的是上述add(obj)中的obj是一个POOL_BOUNDARY对象(可以称为哨兵对象),并不是我们的autorelease的对象,每次执行push操作时都会插入一个哨兵对象,并且把哨兵对象的地址作为返回值返回了,pop函数需要用到这个哨兵对象的地址,对应的每次pop都是寻找到上一个哨兵对象,对期间所有的autorelease对象执行一次release操作。
再来看下pop函数:其中会调用一个比较核心的函数releaseUntil(id *stop)
void releaseUntil(id *stop)
{
// Not recursive: we don't want to blow out the stack
// if a thread accumulates a stupendous amount of garbage
while (this->next != stop) { //一直遍历
// Restart from hotPage() every time, in case -release
// autoreleased more objects
AutoreleasePoolPage *page = hotPage();
// fixme I think this `while` can be `if`, but I can't prove it
while (page->empty()) { //如果当前page中的autorelease对象已释放完毕则会重新遍历父结点的page,知道找到传递来的哨兵对象为止
page = page->parent;
setHotPage(page);
}
page->unprotect();
id obj = *--page->next;
memset((void*)page->next, SCRIBBLE, sizeof(*page->next));
page->protect();
if (obj != POOL_BOUNDARY) {
objc_release(obj);
}
}
setHotPage(this);
#if DEBUG
// we expect any children to be completely empty
for (AutoreleasePoolPage *page = child; page; page = page->child) {
assert(page->empty());
}
#endif
}
上述就是我们自己通过@autoreleasePool{ }形式创建的释放池的创建和销毁,以及其内部的主要逻辑分析。
当然系统默认也是会在当前runloop结束本次循环即将进入休眠的时候也会执行一次pop操作来对释放池内的对象进行一次release操作,然后再重新执行一次push操作。
再补充一下:(直接从别处搬来的…)
App启动后,苹果在主线程 RunLoop 里注册了两个 Observer,其回调都是 _wrapRunLoopWithAutoreleasePoolHandler()。
- 第一个 Observer 监视的事件是 Entry(即将进入Loop),其回调内会调用 _objc_autoreleasePoolPush() 创建自动释放池。其 order 是-2147483647,优先级最高,保证创建释放池发生在其他所有回调之前。
- 第二个 Observer 监视了两个事件: BeforeWaiting(准备进入休眠) 时调用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 释放旧的池并创建新池;Exit(即将退出Loop) 时调用 _objc_autoreleasePoolPop() 来释放自动释放池。这个 Observer 的 order 是 2147483647,优先级最低,保证其释放池子发生在其他所有回调之后。
总结可以下来就是:app启动 调用了一次push来创建释放池,中间我们自己可能会通过@autoreleasePool{ }再执行n次push和pop操作,runloop即将休眠时又会调用一次pop和一次push,runloop在Exit时,又会调用一次pop操作,可以说是一个push就有一个pop与之对应的,从而保证对象能正常释放。
三、ARC下什么样的初始化方法系统会为我们做一次autorelease操作
ARC下以alloc/new/copy/mutableCopy开头的方法,其返回的对象归调用者所有。归调用者所有的意思是:调用上述四个方法的那段代码负责释放方法返回的对象。(摘自Effective Objective-C 2.0 P124)
非alloc/new/copy/mutableCopy开头的方法编译器都会自动帮我们调用autorelease方法。
//自定义类方法
+ (instancetype)createSark {
return [self new]; //这里是有autorelease的(谁创建谁释放)
}
// caller
Sark *sark = [Sark createSark]; //此处有retain操作,作用域结束会有release操作
值得一提的是,ARC下,runtime有一套对autorelease返回值的优化策略:
秉着谁创建谁释放的原则,返回值需要是一个autorelease对象才能配合调用方正确管理内存,于是乎编译器改写成了形如下面的代码:
+ (instancetype)createSark {
id tmp = [self new];
return objc_autoreleaseReturnValue(tmp); // 代替我们调用autorelease
}
// caller
id tmp = objc_retainAutoreleasedReturnValue([Sark createSark]) // 代替我们调用retain
Sark *sark = tmp;
objc_storeStrong(&sark, nil); // 相当于代替我们调用了release
一切看上去都很好,不过既然编译器知道了这么多信息,干嘛还要劳烦autorelease这个开销不小的机制呢?于是乎,runtime使用了一些黑魔法将这个问题解决了,继续往下看。
黑魔法之Thread Local Storage
Thread Local Storage(TLS)线程局部存储,目的很简单,将一块内存作为某个线程专有的存储,以key-value的形式进行读写.
在返回值身上调用objc_autoreleaseReturnValue方法时,runtime将这个返回值object储存在TLS中,然后直接返回这个object(不调用autorelease);同时,在外部接收这个返回值的objc_retainAutoreleasedReturnValue里,发现TLS中正好存了这个对象,那么直接返回这个object(不调用retain)。
于是乎,调用方和被调方利用TLS做中转,很有默契的免去了对返回值的内存管理。这样就省去了两个操作:autorelease和外部的一次retain操作,对于性能提高很多。
自动释放池用途
for (int i = 0; i < 100000000; i++)
{
@autoreleasepool
{
NSString* string = @"ab c";
NSArray* array = [string componentsSeparatedByString:string]; //放入自动释放池中,从而避免大量局部变量要等待runloop将要休眠才释放从而造成的内存峰值。
}
}
参考文章:
https://www.jianshu.com/p/8b011b844231
https://blog.sunnyxx.com/2014/10/15/behind-autorelease/