前言
内存管理里的篇章还有个自动释放池(AutoreleasePool)
,它是一种内存回收机制,放入autoreleasePool
中的对象会延迟释放。那么这个自动释放池是什么,又有怎样的特性?下面我们将对它进行深入分析
结构分析
-
在
main.m
有@autoreleasepool
,通过clang
查看如下:在
C++
源码中@autoreleasepool
是个__autoreleasepool
的构造函数,通过搜索AtAutoreleasePool
发现其代码如下:struct __AtAutoreleasePool { __AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();} ~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);} void * atautoreleasepoolobj; };
- 通过观察结构,发现在构造函数时会调用
objc_autoreleasePoolPush
,析构时调用objc_autoreleasePoolPop
,再结合OC
断点和汇编,最终确定代码在libobjc.A.dylib
,于是在objc4-818.2
源码中分析核心代码
- 通过观察结构,发现在构造函数时会调用
-
通过搜索
objc_autoreleasePoolPush
和objc_autoreleasePoolPop
发现,他们的源码比较类似都是调用AutoreleasePoolPage
中的函数,一个是调用push
,一个是调用pop
:void * objc_autoreleasePoolPush(void) { return AutoreleasePoolPage::push(); } void objc_autoreleasePoolPop(void *ctxt) { AutoreleasePoolPage::pop(ctxt); }
下面先分析下
AutoreleasePoolPage
AutoreleasePoolPage
-
- 找到
AutoreleasePoolPage
,在方法的开头可以看到注释:
- 在注释中可以得到:自动释放池存放的是指针,这些指针要么指向对象,要么指向释放池的边界
POOL_BOUNDARY
,池中还有一个哨兵对象
,自动释放池是一个双向链表
。
- 找到
-
AutoreleasePoolPage
有自己的构造函数和析构函数:
AutoreleasePoolPageData
-
AutoreleasePoolPage
继承AutoreleasePoolPageData
,它是一个结构体相关参数作用如下:
magic
:用来校验AutoreleasePoolPage
的结构是否完整next
:指向最新添加的autoreleased
对象下一个位置,就是这个对象初始化时指向的begin()
thread
:指向当前线程parent
:指向父结点,第一个节点的parent
值为nil
child
:指向子结点,最后一个结点的child
值为nil
depth
:代表深度,从0
开始往后递增1
。也就是每增一页就递增1
hiwat
:代表high water mark
,最大入栈数量标记
-
下面在代码中分析
AutoreloeasePool
的结构
代码分析
-
创建一个空的工程,将环境改成
MRC
然后在@autoreleasePool
中加入对象,并打印autoreleasePool
中的相关内容:extern void _objc_autoreleasePoolPrint(void); int main(int argc, char * argv[]) { @autoreleasepool { NSObject *obj = [[NSObject alloc] autorelease]; _objc_autoreleasePoolPrint(); } return 0; }
-
_objc_autoreleasePoolPrint
是在objc
源码中是一个打印自动释放池内容的函数。
-
@autoreleasepool
是自动释放池,autorelease
是将对象加入自动释放池
-
-
打印结果如下:
- 可以观察到自动释放池前面有
56字节
的一些相关初始化成员变量,然后有个8字节
的哨兵对象
,之后才是加入自动释放池的对象。
- 可以观察到自动释放池前面有
首页结构图
-
根据上面的分析,
autoreleasePool
第一页的结构如下图
自动释放池是怎么加入销毁对象的,一页能够加入多少销毁对象呢,以及对象的释放在什么时候?下面我们将从原理去分析这些特性
原理
push
-
首先来看看
push
代码static inline void *push() { id *dest; if (slowpath(DebugPoolAllocation)) { // Each autorelease pool starts on a new pool page. dest = autoreleaseNewPage(POOL_BOUNDARY); } else { dest = autoreleaseFast(POOL_BOUNDARY); } ASSERT(dest == EMPTY_POOL_PLACEHOLDER || *dest == POOL_BOUNDARY); return dest; }
不看
debug
的代码,于是可以定位到autoreleaseFast
代码 -
autoreleaseFast
代码如下:static inline id *autoreleaseFast(id obj) { AutoreleasePoolPage *page = hotPage(); // 获取hotpage if (page && !page->full()) { // 如果page存在且没有存满,则添加对象 return page->add(obj); } else if (page) { // 存满则新增 return autoreleaseFullPage(obj, page); } else { // page不存在,则创建page return autoreleaseNoPage(obj); } }
- 从
hotPage
取到page
后主要分为三种情况:-
page
存在且没有满,则添加
-
page
满了,则调用autoreleaseFullPage
方法分页后添加
-
page
不存在就调用autoreleaseNoPage
进行相关创建并添加操作
-
下面针对这三种情况进行详细分析
- 从
add
-
add
代码的核心是个内存平移的操作next
指针最开始指向begin
位置,当进来一个obj
,next
就会往下平移一个单位直到存满next
指针平移过程如下图:
判断存满full
-
首先来看看存满的条件
full
:- 当
next
指向end
位置,也就是page
的末尾时就代表存满了,可以看到size
的最大值是2^12
也就是4096
,由于里面的成员变量占56
字节,哨兵对象占8字节
,所以可以添加的对象占4032
字节,也就是可以添加504个
销毁对象。
- 当
-
代码验证:
-
当添加第
505
个时就会进行分页,打印如下- 第
505
个对象添加在了新的page
中,新的page
中处理初始化需要的成员变量外,并没有哨兵对象
,也就是新增的页面占用4040
个字节,也就是 可以存储505
个对象
- 第
-
疑问:为什么第二页没有哨兵对象?
- 答:因为哨兵对象有"监控"
page
边界的作用,第一页已经满了已经不需要add
操作,所以它可以去第二页工作,由于没有存满之前都在当前页工作,所以只需要一个哨兵对象就足够了
- 答:因为哨兵对象有"监控"
-
结论:
1. 第一页最多可以存储504
个对象,新增页最多存储505
个对象
2. 新增页没有哨兵对象
autoreleaseFullPage
-
当存满后会走
autoreleaseFullPage
方法- 此处主要是
do-while
循环中找到最后一张page
- 如果没有满,则将
page
设置成hot
,再调用add
添加obj
- 如果满了,就调用
AutoreleasePoolPage
创建新page
,然后再add
添加obj
。
- 如果没有满,则将
- 此处主要是
-
AutoreleasePoolPage
方法主要是进行page
初始化,并设置成员变量的值,如果有parent
,则设置parent
的child
为自己
autoreleaseNoPage
-
当没有取到
hotPage
,也就是第一个page
不存在时就会进行创建:- 创建的过程和上面存满的过程一样,但第一个界面创建后会先添加一个
哨兵对象
,然后再添加销毁对象
- 创建的过程和上面存满的过程一样,但第一个界面创建后会先添加一个
pop
-
pop
主要代码如下- 如果
token
指向哨兵对象,则说明第一页是空的,然后做一些相应处理 popPageDebug
方法最终会走到popPage
函数,再查看popPage
的实现
- 如果
popPage
代码如下
-
popPage
首先会调用releaseUntil
将对象进行release
:
- 这块代码主要是在
while循环
中进行内存平移取到对象,然后调用objc_release
方法进行释放 - 平移取对象的过程如下图:
-
- 对象释放后,然后当前页是空的,就会找到
parent
,然后将当前页kill
,并将parent
设置成hotPage
- 对象释放后,然后当前页是空的,就会找到
-
- 如果是
page
是第一页,则直接kill
,并将hotPage
设置为nil
- 如果是
-
- 如果当前
page
有child
:
- 如果当前页半满,则将
child
直接kill
- 如果
child
还有child
,则将child
的child
直接kill
- 如果当前
kill
-
page
销毁的核心是kill
函数,它的实现如下:
kill
,流程比较简单,就是获取当前页面的child
,从最后一张开始page
开始delete
,直到当前页才停止
整个
pop
过程是:先将销毁对象release
,然后从最后一张开始删除,直到第一张。第一张page
销毁对象release
后将hotPage
置为nil
多page结构图
相关题目
-
AutoreleasePool
能够嵌套使用吗?
- 使用代码测试如下
int main(int argc, char * argv[]) { @autoreleasepool { NSObject *obj1 = [[NSObject alloc] autorelease]; NSLog(@"obj1 : %@", obj1); @autoreleasepool { NSObject *obj2 = [[NSObject alloc] autorelease]; NSLog(@"obj2 : %@", obj2); _objc_autoreleasePoolPrint(); } _objc_autoreleasePoolPrint(); } return 0; }
- 打印结果:
-
从结果中可以看出,第一次打印里面有两个哨兵对象,也就是包含的自动释放池被压进了外面的自动释放池,所以打印会有两个哨兵对象,而出作用域后,里面的自动释放池释放了,所以下面的打印只有一个哨兵对象和
obj1
了。 -
答:自动释放池可以嵌套使用
-
ARC
中的对象怎么加入自动释放池的?
- 我们知道在
MRC
中,创建的对象需要加上autorelease
才可以入池,在ARC
中没有autorelease
,可以使用__autoreleasing
修饰,将对象加入自动释放池:
int main(int argc, char * argv[]) { @autoreleasepool { NSObject *obj1 = [NSObject alloc] ; NSLog(@"obj1 : %@", obj1); NSObject * __autoreleasing obj2 = [NSObject alloc] ; NSLog(@"obj2 : %@", obj2); _objc_autoreleasePoolPrint(); } return 0; }
打印结果如下:
ARC
入池主要有以下情形:如果不是alloc/new/copy/mutableCopy
创建,则自动将返回对象注册到Autoreleasepool