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
父节点,指向前一个pagechild
子节点,指向下一个pagedepth
链表的深度,节点个数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 国际许可协议 进行许可,完整转载、部分转载、图片转载时均请注明原文链接。