深入解析 autoreleasepool
https://draveness.me/autoreleasepool/
http://blog.sunnyxx.com/2014/10/15/behind-autorelease/
Autorelease机制是iOS开发者管理对象内存的好伙伴,MRC中,调用[obj autorelease]
来延迟内存的释放是一件简单自然的事,ARC下,我们甚至可以完全不知道Autorelease就能管理好内存。而在这背后,objc和编译器都帮我们做了哪些事呢,它们是如何协作来正确管理内存的呢?刨根问底,一起来探究下黑幕背后的Autorelease机制。
面试题: Autorelease对象什么时候释放?
Autorelease Pool的作用域结束的时候会给当前池中的对象发送release消息.
在没有手加Autorelease Pool的情况下,Autorelease对象是在当前的runloop
迭代结束时释放的,而它能够释放的原因是系统在每个runloop迭代中都加入了自动释放池Push和Pop.
Autorelease原理
AutoreleasePoolPage
@autoreleasepool
到底是什么?使用 clang -rewrite-objc main.m
随后编译器将其改写成下面的样子, 删掉无用代码后, 找到了这2个关键函数:
void *context = objc_autoreleasePoolPush();
// {}中的代码
objc_autoreleasePoolPop(context);
而这两个函数都是对AutoreleasePoolPage
的简单封装,所以自动释放机制的核心就在于这个类。
AutoreleasePoolPage是一个C++实现的类
- AutoreleasePool并没有单独的结构,而是由若干个AutoreleasePoolPage以
双向链表
的形式组合而成(分别对应结构中的parent指针和child指针) - AutoreleasePool是按线程一一对应的(结构中的thread指针指向当前线程)
- AutoreleasePoolPage每个对象会开辟4096字节内存(也就是虚拟内存一页的大小),除了上面的实例变量所占空间,剩下的空间全部用来储存autorelease对象的地址
- 上面的
id *next
指针作为游标指向栈顶最新add进来的autorelease对象的下一个位置 - 一个AutoreleasePoolPage的空间被占满时,会新建一个AutoreleasePoolPage对象,连接链表,后来的autorelease对象在新的page加入
所以,若当前线程中只有一个AutoreleasePoolPage对象,并记录了很多autorelease对象地址时内存如下图:
图中的情况,这一页再加入一个autorelease对象就要满了(也就是next指针马上指向栈顶),这时就要执行上面说的操作,建立下一页page对象,与这一页链表连接完成后,新page的next
指针被初始化在栈底(begin的位置),然后继续向栈顶添加新对象。
所以,向一个对象发送- autorelease
消息,就是将这个对象加入到当前AutoreleasePoolPage的栈顶next指针指向的位置
释放时刻
每当进行一次objc_autoreleasePoolPush
调用时,runtime向当前的AutoreleasePoolPage中add进一个哨兵对象
,值为0(也就是个nil),那么这一个page就变成了下面的样子:
objc_autoreleasePoolPush
的返回值正是这个哨兵对象的地址,被objc_autoreleasePoolPop(哨兵对象)
作为入参,于是:
- 根据传入的哨兵对象地址找到哨兵对象所处的page
- 在当前page中,将晚于哨兵对象插入的所有autorelease对象都发送一次
- release
消息,并向回移动next
指针到正确位置 - 补充2:从最新加入的对象一直向前清理,可以向前跨越若干个page,直到哨兵所在的page
刚才的objc_autoreleasePoolPop执行后,最终变成了下面的样子:
纸上得来终觉浅, 绝知此事要躬行 , 上面的都是原理上的讲解, 没有深入到源码中, 还是要到源码中看看的.
通过上面的学习我们知道了有2个关键函数, 那么现在开始分析 objc_autoreleasePoolPush
和 objc_autoreleasePoolPop
的实现:
void *objc_autoreleasePoolPush(void) {
return AutoreleasePoolPage::push();
}
void objc_autoreleasePoolPop(void *ctxt) {
AutoreleasePoolPage::pop(ctxt);
}
上面的方法看上去是对 AutoreleasePoolPage
对应静态方法 push
和 pop
的封装。在放一遍这个图
自动释放池中的 AutoreleasePoolPage
是以双向链表的形式连接起来的, 所以实际是这样:
parent
和 child
就是用来构造双向链表的指针。
自动释放池中的栈
如果我们的一个 AutoreleasePoolPage
被初始化在内存的 0x100816000 ~ 0x100817000
中,它在内存中的结构如下:
其中有 56 bit 用于存储 AutoreleasePoolPage
的成员变量,剩下的 0x100816038 ~ 0x100817000
都是用来存储加入到自动释放池中的对象。
begin()
和 end()
这两个类的实例方法帮助我们快速获取 0x100816038 ~ 0x100817000
这一范围的边界地址。
next
指向了下一个为空的内存地址,如果 next
指向的地址加入一个 object
,它就会如下图所示移动到下一个为空的内存地址中:
关于 hiwat
和 depth
在文章中并不会进行介绍,因为它们并不影响整个自动释放池的实现,也不在关键方法的调用栈中。
POOL_SENTINEL(哨兵对象, 现在的版本叫POOL_BOUNDARY, 本文中POOL_SENTINEL和POOL_BOUNDARY等价)
到了这里,你可能想要知道 POOL_SENTINEL 到底是什么,还有它为什么在栈中。
首先回答第一个问题: POOL_SENTINEL 只是 nil
的别名。
#define POOL_SENTINEL nil
在每个自动释放池初始化调用 objc_autoreleasePoolPush
的时候,都会把一个 POOL_SENTINEL push 到自动释放池的栈顶,并且返回这个 POOL_SENTINEL 哨兵对象。
int main(int argc, const char * argv[]) {
{
void * atautoreleasepoolobj = objc_autoreleasePoolPush();
// do whatever you want
objc_autoreleasePoolPop(atautoreleasepoolobj);
}
return 0;
}
上面的 atautoreleasepoolobj
就是一个 POOL_SENTINEL。
而当方法 objc_autoreleasePoolPop
调用时,就会向自动释放池中的对象发送 release
消息,直到第一个 POOL_SENTINEL:
objc_autoreleasePoolPush 方法
了解了 POOL_SENTINEL
,我们来重新回顾一下 objc_autoreleasePoolPush
方法:
它调用 AutoreleasePoolPage
的类方法 push
,也非常简单:
在这里会进入一个比较关键的方法 autoreleaseFast
,并传入哨兵对象 POOL_SENTINEL
:
先解释下什么是hotpage, 可以理解为当前正在使用的 AutoreleasePoolPage
, 一般是最后一页page, 上述方法分三种情况选择不同的代码执行:
- 有
hotPage
并且当前page
不满- 调用
page->add(obj)
方法将对象添加至AutoreleasePoolPage
的栈中
- 调用
- 有
hotPage
并且当前page
已满- 调用
autoreleaseFullPage
初始化一个新的页 - 调用
page->add(obj)
方法将对象添加至AutoreleasePoolPage
的栈中
- 调用
- 无
hotPage
- 调用
autoreleaseNoPage
创建一个hotPage
- 调用
page->add(obj)
方法将对象添加至AutoreleasePoolPage
的栈中
- 调用
最后的都会调用 page->add(obj)
将对象添加到自动释放池中。
page->add 添加对象, 将对象添加到自动释放池页中:
这个方法其实就是一个压栈的操作,将对象加入 AutoreleasePoolPage
然后移动栈顶的指针。
autoreleaseFullPage(当前 hotPage 已满)
autoreleaseFullPage
会在当前的 hotPage
已满的时候调用:
它会从传入的 page
开始遍历整个双向链表,直到:
- 查找到一个未满的
AutoreleasePoolPage
- 使用构造器传入
parent
创建一个新的AutoreleasePoolPage
在查找到一个可以使用的 AutoreleasePoolPage
之后,会将该页面标记成 hotPage
,然后调动上面分析过的 page->add
方法添加对象。
autoreleaseNoPage(没有 hotPage)
如果当前内存中不存在 hotPage
,就会调用 autoreleaseNoPage
方法初始化一个 AutoreleasePoolPage
:
static __attribute__((noinline))
id *autoreleaseNoPage(id obj) {
// 省略很多安全检查代码
// We are pushing an object or a non-placeholder'd pool.
// Install the first page.
AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
setHotPage(page);
// Push a boundary on behalf of the previously-placeholder'd pool.
if (pushExtraBoundary) {
page->add(POOL_BOUNDARY);
}
// Push the requested object or pool.
return page->add(obj);
}
既然当前内存中不存在 AutoreleasePoolPage
,就要从头开始构建这个自动释放池的双向链表,也就是说,新的 AutoreleasePoolPage
是没有 parent
指针的。
初始化之后,将当前页标记为 hotPage
,然后会先向这个 page
中添加一个 POOL_SENTINEL
对象,来确保在 pop
调用的时候,不会出现异常。
最后,将 obj
添加到自动释放池中。
objc_autoreleasePoolPop 方法
同样,回顾一下上面提到的 objc_autoreleasePoolPop
方法:
看起来传入任何一个指针都是可以的,但是在整个工程并没有发现传入其他对象的例子。不过在这个方法中传入其它的指针也是可行的,会将自动释放池释放到相应的位置,只是我们一般不会传入非哨兵对象。。
我们一般都会在这个方法中传入一个哨兵对象 POOL_SENTINEL
,如下图一样释放对象:
也就是 AutoreleasePoolPage::pop
方法的调用 , 对原始方法有删减, 方便看原理:
static inline void pop(void *token) {
AutoreleasePoolPage *page = pageForPointer(token);
id *stop = (id *)token;
page->releaseUntil(stop);
if (page->child) {
if (page->lessThanHalfFull()) {
page->child->kill();
} else if (page->child->child) {
page->child->child->kill();
}
}
}
该静态方法总共做了三件事情:
- 使用
pageForPointer
获取当前token
所在的AutoreleasePoolPage
- 调用
releaseUntil
方法释放栈中的对象,直到stop
- 如果当前页容量小于一半了, 把子页kill掉; 超过一半了, 认为后期可能会用到子页, 就先保留了子页
pageForPointer 获取 AutoreleasePoolPage
pageForPointer
方法主要是通过内存地址的操作,获取当前指针所在页的首地址:
将指针与页面的大小,也就是 4096 取模,得到当前指针的偏移量,因为所有的 AutoreleasePoolPage
在内存中都是对齐的, 比如:
p = 0x100816048
p % SIZE = 0x48
result = 0x100816000
而最后调用的方法 fastCheck()
用来检查当前的 result
是不是一个 AutoreleasePoolPage
。
通过检查 magic_t
结构体中的某个成员是否为 0xA1A1A1A1
。
releaseUntil 释放对象
releaseUntil
方法的实现如下:
它的实现还是很容易的,用一个 while
循环持续释放 AutoreleasePoolPage
中的内容,直到 next
指向了 stop
。
使用 memset
将内存的内容设置成 SCRIBBLE
,然后使用 objc_release
释放对象。
最后还更新了一下hotpage.
kill() 方法
到这里,没有分析的方法就只剩下 kill
了,而它会将当前页面以及子页面全部删除:
autorelease 方法
我们已经对自动释放池生命周期有一个比较好的了解,最后需要了解的话题就是 autorelease
方法的实现,先来看一下方法的调用栈:
- [NSObject autorelease]
└── id objc_object::rootAutorelease()
└── id objc_object::rootAutorelease2()
└── static id AutoreleasePoolPage::autorelease(id obj)
└── static id AutoreleasePoolPage::autoreleaseFast(id obj)
├── id *add(id obj)
├── static id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page)
│ ├── AutoreleasePoolPage(AutoreleasePoolPage *newParent)
│ └── id *add(id obj)
└── static id *autoreleaseNoPage(id obj)
├── AutoreleasePoolPage(AutoreleasePoolPage *newParent)
└── id *add(id obj)
在 autorelease
方法的调用栈中,最终都会调用上面提到的 autoreleaseFast 方法,将当前对象加到 AutoreleasePoolPage
中。
这一小节中这些方法的实现都非常容易,只是进行了一些参数上的检查,最终还要调用 autoreleaseFast 方法:
// Base autorelease implementation, ignoring overrides.
inline id objc_object::rootAutorelease() {
if (isTaggedPointer()) return (id)this;
if (prepareOptimizedReturn(ReturnAtPlus1)) return (id)this;
return rootAutorelease2();
}
__attribute__((noinline,used)) id objc_object::rootAutorelease2() {
assert(!isTaggedPointer());
return AutoreleasePoolPage::autorelease((id)this);
}
static inline id autorelease(id obj) {
assert(obj);
assert(!obj->isTaggedPointer());
id *dest __unused = autoreleaseFast(obj);
assert(!dest || dest == EMPTY_POOL_PLACEHOLDER || *dest == obj);
return obj;
}
由于在上面已经分析过 autoreleaseFast
方法的实现,这里就不会多说了。
小结
整个自动释放池 AutoreleasePool
的实现以及 autorelease
方法都已经分析完了,我们再来回顾一下文章中的一些内容:
- 自动释放池是由
AutoreleasePoolPage
以双向链表+栈的方式实现的 - 当对象调用
autorelease
方法时,会将对象加入AutoreleasePoolPage
的栈中 - 调用
AutoreleasePoolPage::pop
方法会向栈中的对象发送release
消息
其他Autorelease相关知识点
嵌套的AutoreleasePool会有问题吗?
知道了上面的原理,嵌套的AutoreleasePool就非常简单了,pop的时候总会释放到上次push的位置为止,多层的pool就是多个哨兵对象而已,就像剥洋葱一样,每次一层,互不影响。
Autorelease返回值的快速释放机制
值得一提的是,ARC下,runtime有一套对autorelease返回值的优化策略。
比如一个工厂方法:
+ (instancetype)createSark {
return [self new];
}
// caller
Sark *sark = [Sark createSark];
秉着谁创建谁释放的原则,返回值需要是一个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的形式进行读写,比如在非arm架构下,使用pthread提供的方法实现:
void* pthread_getspecific(pthread_key_t);
int pthread_setspecific(pthread_key_t , const void *);
在返回值身上调用objc_autoreleaseReturnValue
方法时,runtime将这个返回值object储存在TLS中,然后直接返回这个object(不调用autorelease);同时,在外部接收这个返回值的objc_retainAutoreleasedReturnValue
里,发现TLS中正好存了这个对象,那么直接返回这个object(不调用retain)。
于是乎,调用方和被调方利用TLS做中转,很有默契的免去了对返回值的内存管理, 提升了性能.
使用容器的block版本的枚举器时,内部会自动添加一个AutoreleasePool:
[array enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
// 这里被一个局部@autoreleasepool包围着
}];
当然,在普通for循环和for in循环中没有,所以,还是新版的block版本枚举器更加方便。for循环中遍历产生大量autorelease变量时,就需要手加局部AutoreleasePool咯。
推荐阅读
-
深入hibernate的三种状态 博客分类: Hibernate
-
深入解析 autoreleasepool
-
java通过org.w3c.dom解析xml文件 博客分类: xml java解析xml文件dom4j解析xml文件
-
ReentrantLockd的非公平锁lock方法实现源码解析 博客分类: java多线程 java
-
Android事件机制深入探讨(一)
-
解析崩溃日志的堆栈信息
-
Android事件机制深入探讨(二)
-
多点弹性运动(动画编程深入) 博客分类: AS动画3D动画,AS,FL springmouseeventflashspriteevents
-
多点弹性运动(动画编程深入) 博客分类: AS动画3DflashFL displaymouseeventflashasevents
-
一次javascript深拷贝引发的深入思考