1、@autoreleasepool{}
在新建 iOS 项目的时候,会自动生成 main.m 文件:
1 |
|
通过如下命令:
1 | xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main.cpp |
将其转成 C/C++ 代码,main 函数实现如下:
1 | int main(int argc, char * argv[]) { |
为了看起来方便,先去掉无关逻辑:
1 | int main(int argc, char * argv[]) { |
可以看到,这里将 @autoreleasepool {}
转换成:
1 | { |
继续在 main.cpp 中查找 __AtAutoreleasePool
的定义,可以找到:
1 | struct __AtAutoreleasePool { |
可以确定 __AtAutoreleasePool
是一个结构体,这个结构体会在初始化时调用 objc_autoreleasePoolPush()
方法,在析构时调用 objc_autoreleasePoolPop
方法。
也就是说,main 函数实际实现可以理解为:
1 | int main(int argc, const char * argv[]) { |
在 OC 开源的源码中可以找到 objc_autoreleasePoolPush
和 objc_autoreleasePoolPop
实现如下(为了阅读方便,后续源码会有部分删简):
1 | void *objc_autoreleasePoolPush(void) { |
这两个方法是 AutoreleasePoolPage
对应静态方法 push
和 pop
的封装,其具体作用后文分析。
2、AutoreleasePoolPage
AutoreleasePool
并没有单独的结构,每一个 AutoreleasePool
都是由若干个 AutoreleasePoolPage
以双向链表的形式组成,并且每一个 AutoreleasePoolPage
的大小都是 4096 字节,AutoreleasePoolPage
定义如下:
1 | class AutoreleasePoolPage : private AutoreleasePoolPageData |
magic
检查校验完整性的变量next
指向栈顶最新add
进来的autorelease
对象的下一个位置thread
page
当前所在的线程,AutoreleasePool
是按线程一一对应的(结构中的thread
指针指向当前线程)parent
父节点,指向前一个page
child
子节点,指向下一个page
depth
链表的深度,节点个数hiwat
high water mark 数据容纳的一个上限
AutoreleasePoolPage
内存结构如下图:
每一个 AutoreleasePoolPage
的大小都是 4096 bit,其中有 56 bit 用于存储 AutoreleasePoolPage
的成员变量,剩下的用来存储加入到自动释放池中的对象。
begin()
和 end()
这两个类的实例方法用于快速获取 4096 bit - 56 bit = 4040 bit 这一内存范围的边界地址。
next
指向了下一个为空的内存地址,如果 next
指向的地址加入一个对象,它就会如下图所示移动到下一个为空的内存地址中:
其中 POOL_BOUNDARY
是一个边界对象 nil
(老版本变量名是 POOL_SENTINEL
哨兵对象)用来区别每个 AutoreleasePoolPage
边界,起到一个标识作用:
1 |
在每个自动释放池初始化调用 objc_autoreleasePoolPush
的时候,都会把一个 POOL_BOUNDARY
push
到自动释放池的栈顶,并且返回这个 POOL_BOUNDARY
哨兵对象。
1 | int main(int argc, const char * argv[]) { |
而调用 objc_autoreleasePoolPop
时,就会向自动释放池中的对象发送 release
消息,并在 AutoreleasePoolPage
中移除对应位置对象,直到第一个 POOL_BOUNDARY
。
3、push & pop
根据前面对 main.cpp 分析可知,@autoreleasepool{}
实际上就是在 {}
中的代码前后分别调用 objc_autoreleasePoolPush()
和 objc_autoreleasePoolPop(atautoreleasepoolobj);
objc_autoreleasePoolPush()
实现如下:
1 | void *objc_autoreleasePoolPush(void) { |
其内部调用的 AutoreleasePoolPage
中的 push
函数:
1 | static inline void *push() |
在这里会调用 autoreleaseFast
函数,并传入边界对象 POOL_BOUNDARY
:
1 | static inline id *autoreleaseFast(id obj) |
a、将对象添加到自动释放池页中:
1 | id *add(id obj) |
add
函数其实就是一个压栈的操作,将对象加入 AutoreleasePoolPage
然后移动栈顶的指针。
b、有 hotPage 并且当前 page 已满时:
1 | static id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page) |
该函数会从传入的 page
开始遍历整个双向链表
如果找到未满的 AutoreleasePoolPage
,则调用 add
函数添加对象。
如果没有找到未满的 AutoreleasePoolPage
,则创建新的 AutoreleasePoolPage
,并调用 add
函数添加对象。
最后将找到的或者新建的 AutoreleasePoolPage
标记为 hotPage
.
c、无 hotPage:
1 | id *autoreleaseNoPage(id obj) |
新建一个 AutoreleasePoolPage
,然后再加入 obj
,创建 Autorelease Pool
的时候,obj
的值是 POOL_BOUNDARY
。
我们用一张图来表示 Autorelease Pool
刚创建时候的结构:
总结上面 push
过程:
首先获取 hotPage
,即当前正在使用的 AutoreleasePoolPage
,然后针对 hotPage
的情况做不同处理:
- 存在
hotPage
&page
不满- 调用
page->add(obj)
方法将对象添加至AutoreleasePoolPage
的栈中。
- 调用
- 存在
hotPage
&page
已满- 调用
autoreleaseFullPage
函数,从传入的page
开始往后遍历双向链表。 - 如果找到未满的
AutoreleasePoolPage
,则将该page
标记为hotPage
。 - 如果没有找到未满的
AutoreleasePoolPage
,则创建新的AutoreleasePoolPage
,并将该page
标记为hotPage
。 - 调用
page->add(obj)
方法将对象添加至AutoreleasePoolPage
的栈中。
- 调用
- 无
hotPage
- 调用
autoreleaseNoPage
函数创建一个hotPage
- 调用
page->add(obj)
方法将对象添加至AutoreleasePoolPage
的栈中。
- 调用
前边提到,销毁 Autorelease Pool
会调用 objc_autoreleasePoolPop
方法:
1 | void objc_autoreleasePoolPop(void *ctxt) |
pop
函数实现如下:
1 | static inline void pop(void *token) |
根据前面的了解可知,这里 token
即创建 Autorelease Pool
时返回的 POOL_BOUNDARY
,这个会作为 pageForPointer
的输入参数。 pageForPointer
函数的实现如下:
1 | static AutoreleasePoolPage *pageForPointer(const void *p) |
这里是为了计算出创建 Autorelease Pool
时 AutoreleasePoolPage
的内存起始地址。所以,pageForPointer
函数返回的是当前 Autorelease Pool
创建时候的 AutoreleasePoolPage
,获取到 AutoreleasePoolPage
之后,调用 releaseUntil
函数:
1 | void releaseUntil(id *stop) |
这里主要是从当前的 hotPage
开始,依次对 AutoreleasePoolPage
里的对象执行 objc_release
操作,直到遇到 POOL_BOUNDARY
对象。
总结上面 pop
流程:
首先,找到传入的 POOL_BOUNDARY
所在的 page
,从 hotPage
开始(从自动释放池的中的最后一个入栈的 autorelease
对象开始),一直往前释放加入自动释放池的 autorelease
对象,可以向前跨越若干个 page
,直到遇到这个 POOL_BOUNDARY
,理的方式是向这些对象发送一次 release
消息,使其引用计数减一;
另外,清空 page
对象还会遵循一些原则:
- 如果清理后当前的
page
中存放的对象少于一半,则子page
全部删除; - 如果清理后当前的
page
存放的多于一半(意味着马上将要满),则保留一个子page
,节省创建新page
的开销;
接下来用图对 push
和 pop
过程进行一个演示:
首先,假设一个 AutoreleasePoolPage
中只能存储 4 个 Autorelease
对象,第一次创建 Autorelease Pool
时,有 5 个 Autorelease
对象需要放到缓存池中,这时候,第 5 个 Autorelease
对象只能存到一个新的 autoreleasePoolPage
中:
在上个 Autorelase Pool
还未销毁时,这时又新建了一个 Autorelase Pool
,需要存储 2 个 Autorelease
对象,则往 AutorelasePoolPage
的 next
位置加入 POOL_BOUNDARY
。并添加 obj6
和 obj7
:
释放时,根据 POOL_BOUNDARY
找到所在 page
,在对应 page
中,将晚于 POOL_BOUNDARY
插入的所有 autorelease
对象都发送一次 release
消息,并向回移动 next
指针到正确位置:
4、RunLoop 与 @autoreleasepool:
默认情况下,Autorelease
对象的释放时机是由 RunLoop 控制的,会在当前 RunLoop 每次循环期间时释放。
iOS 在主线程的 RunLoop 中注册了两个 Observer。
第 1 个 Observer
监听了 kCFRunLoopEntry 事件,会调用objc_autoreleasePoolPush()
;第 2 个 Observer
监听了kCFRunLoopBeforeWaiting
事件,会调用objc_autoreleasePoolPop()
、objc_autoreleasePoolPush()
;
监听了kCFRunLoopBeforeExit
事件,会调用objc_autoreleasePoolPop()
。
所以,释放时机在 RunLoop 的如下三个事件中:
kCFRunLoopEntry
在即将进入 RunLoop 时,会自动创建一个__AtAutoreleasePool
结构体对象,并调用objc_autoreleasePoolPush()
函数。kCFRunLoopBeforeWaiting
在 RunLoop 即将休眠时,会自动销毁一个__AtAutoreleasePool
对象,调用objc_autoreleasePoolPop()
。然后创建一个新的__AtAutoreleasePool
对象,并调用objc_autoreleasePoolPush()
。kCFRunLoopBeforeExit
在即将退出 RunLoop 时,会自动销毁最后一个创建的__AtAutoreleasePool
对象,并调用objc_autoreleasePoolPop()
。
- 本文章采用 知识共享署名 4.0 国际许可协议 进行许可,完整转载、部分转载、图片转载时均请注明原文链接。