Autorelease与AutoreleasePool
以前大概看过AutoreleasePool
里面有AutoreleasePoolPage
有热页, 冷页. 页有深度, 双向链表链接, 首位置有哨兵对象等等. 大概就这么多印象, 但是没有系统的梳理过. 最近在重新梳理一遍, 就当学习了, 希望可以表达的更清晰.
文档
AutoreleasePool官方文档
可以看到文档是在Advanced Memory Management Programming Guide
类别下面.直接搜Autorelease
其实是搜不到的.
文档很老, 现在不在更新了, 但是很多东西并不过时, 建议多多查看.
看文档如果看不懂的, 或者看起来很费力的伙计, 建议谷歌浏览器自带翻译. 机场推荐巴士, 挺好用的. 谷歌对应英文文档的翻译, 还原度还是相当高的, 结合理解绝大部分是够用的.
AutoreleasePool相关问题
Autorelease
对象什么时候释放?- 一个线程可以多少个
AutoreleasePool
? - main函数的
@autoreleasepool
做了什么? - 在子线程中创建的对象,如果没有启动
runloop
,也没有声明AutoreleasePool
,怎么管理呢? - 已知已经有了
AutoreleasePool
,但是没有runloop,子线程中的AutoreleasePool
什么时候会清空?
很多题目不会单单问AutoreleasePool
做了什么, 干嘛的, 主要都是问和线程, RunLoop
的关系而产生的一些问题. 下面我来进行AutoreleasePool
的探究.
Autorelease
MRC的时代, retain
和release
是一对亲人, 你创建我释放. 而为了更方便的使用, autorelease
出现了, 它不需要用户自己去做release
的动作, 只需要在创建的时候标注上, 后续会在将修饰的对象放入自动释放池之中, 会在一个合适的时机自动去调用release
, 从而达到自动释放的效果.
AutoreleasePool
探索思路
正常情况下, 我们不太了解文档, 或者地址, 要怎么去探索呢?
第一步:
xcrun
clang
, 其实xcrun
和clang
可以算作为一种- 汇编 找到底层调用的真正的符号或者函数, 从而从创建到销毁进行探索.
第二步:
- 拿到符号或者函数, 找到对应的库或者源码下载探索
第三步:
- 如果没有开源, 可以去
hopper
系统动态库对应的可执行文件, 根据真实的符号, 查看汇编, 还原核心部分进行探索. - 比较适合轻量级的
大致就是这几种方案, 最好用的个人认为是汇编直接查找符号, 因为运行时的原因, 汇编看到的东西是真实的符号,
clang
看到的是静态编译的产物, 这瓜不保熟.
选择汇编探索
找到符号, 查看符号所在源码是否开源
新建一个程序, main
函数入口的@autoreleasepool
打一个断点. 然后汇编如下:
继续往内部跳转, 发现内部无法继续.
所以, 我们先拿到调用的符号objc_autoreleasePoolPush
, 然后直接下符号断点, 如下:
看最上面, 我们看到了符号在
libobjc
中, 一直下载开源框架, 内部查看.
AutoreleasePoolPage结构
全局搜索符号objc_autoreleasePoolPush
, 在NSObject.mm
我们看到下图:
我们继续查看
AutoreleasePoolPage
是个什么东西, 如下:
可以看到是一个C++的类, 继承与一个叫
AutoreleasePoolPageData
的结构体, 之前有人疑惑类和结构体怎么继承? 在C++中, 类和结构体没有什么区别只是默认是public
.
继续看下AutoreleasePoolPageData
的结构:
很清晰的双向链表, 之前
YYCache
基本也是这样, 一般看到child
, parent
, sibling
基本上数据结构都是固定的.
从上面得到的信息, 调用@autoreleasepool
的时候:
- 1.实际上调用了
AutoreleasePoolPage::push();
这么一个函数 - 2.
AutoreleasePoolPage
继承AutoreleasePoolPageData
结构体的 - 3.
AutoreleasePoolPageData
类似于一个双向链表的节点, 每个节点就是一个AutoreleasePoolPage
内部有child
和parent
两个指针, 指向下一节点.
AutoreleasePoolPage::push()做了什么
接下来我们, 继续看push()
方法内部做了什么.
DebugPoolAllocation是一个宏定义:
OPTION( DebugPoolAllocation, OBJC_DEBUG_POOL_ALLOCATION, "halt when autorelease pools are popped out of order, and allow heap debuggers to track autorelease pools")
意思就是根据, 环境变量OBJC_DEBUG_POOL_ALLOCATION设置的. 是否允许堆栈调试POOL, 追踪 ,默认是NO.
所以, 我们直接看上面第1173
行代码, 看看autoreleaseFast
做了什么:
- 先取出当前的
hotPage
, 如果不空不满, 那么将obj
加入当前page
- 如果
page
存在且不满, 那么走autoreleaseFullPage
- 如果
page
不存在且不满, 那么走autoreleaseNoPage
hotPage(), 取出hotPage
hotPage
就是一个热页(当做一个状态), 就是当前加载的页,在最前线正在使用的. 冷页就是距离当前页最远的.
这里出现了一个hotPage()
, 那么是什么呢? 又是如何取的:
可以看到是根据
tls_get_direct
函数和一个key
获取的, 在这里不多赘述了, 给大家看一下key
:
这里
tls
是操作系统给每个线程会分配一个很小的空间地址, 存储一些所需要的值, 这里的key
就是AUTORELEASE_POOL_KEY
, 每一个线程生成的一个线程key
对应的AUTORELEASE_POOL_KEY
的结构体作为key
去tls
空间寻找对应的指针.
从里面可以看到, SYNC_COUNT
和SYNC_DATA
对应的都有自己的用处, 有兴趣的可以自己研究一下.
下面看一段注释:
这个注释里, 也很清楚的说明了,
tls
存储了指向hot page
的指针.
autoreleaseFullPage
是一个C++的静态函数, 代码很明显从一个
do...while
循环中, 找到下一页不满的页面并且设置为hotPage
, 然后添加进当前页.
autoreleaseNoPage
留下核心的注释和代码如下:
static __attribute__((noinline))
id *autoreleaseNoPage(id obj) {
// "No page" could mean no pool has been pushed
// or an empty placeholder pool has been pushed and has no contents yet
ASSERT(!hotPage());
bool pushExtraBoundary = false;
// 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);
}
POOL_BOUNDARY
代表哨兵对象, 每一个page
的第一个元素都是空的哨兵对象.
- 如果没有
page
那么, 创建page. - 如果是空池子, 那么先添加一个哨兵对象进去
- 添加元素进当前页.
page->add(obj)
id *add(id obj) {
id *ret;
ret = next; // faster than `return next-1` because of aliasing
//返回next指针, 这样返回比next-1返回的快
*next++ = obj; //next的内存内容=obj, next++指向下一个进来的元素
protect();
return ret;
}
begin, end, full, empty
全都是根据
next
指针的地址进行判断的
begin()
的位置等于AutoreleasePoolPage
+当前自己的结构体大小, 就是起始位置+56.end()
的位置等于起始位置+SIZE
,SIZE
定义为1<<12
=4096
b, 所以一页page
的大小为4kb
full()
就是next
指针地址和end
比较是否相等empty()
就是next
指针地址和begin
比较是否相等
AutoreleasePoolPage::pop()做了什么
上面大致聊清楚了, push
做了什么, 接下来看pop
做了什么, C++的构造函数和析构函数:
AutoreleasePoolPage() {
//构造函数
}
~AutoreleasePoolPage() {
//析构函数, 销毁时候调用
}
直接看源码, 可以直接跳过看下面的流程:
static inline void
//省略大多代码留下核心
pop(void *token) {
AutoreleasePoolPage *page;
id *stop;
stop = (id *)token;
return popPage<false>(token, page, stop);
}
下面是popPage
popPage(void *token, AutoreleasePoolPage *page, id *stop)
{
//核心代码
page->releaseUntil(stop);
}
下面是releaseUntil
void releaseUntil(id *stop)
{
// Not recursive: we don't want to blow out the stack
// 不是递归的, 我们不想让堆栈爆炸
// if a thread accumulates a stupendous amount of garbage
// 如果一个线程堆积了 大量的垃圾
如果当前next对象, 不等于烧饼对象的地址 ,那么就进行循环.
while (this->next != stop) {
//取出当前页
AutoreleasePoolPage *page = hotPage();
//如果页面为空, 那么取出不为空的父页面, 并且设置为热页
while (page->empty()) {
page = page->parent;
setHotPage(page);
}
//省略
//取出next
AutoreleasePoolEntry* entry = (AutoreleasePoolEntry*) --page->next;
// create an obj with the zeroed out top byte and release that
//取出指针
id obj = (id)entry->ptr;
//取出obj的引用数量
int count = (int)entry->count; // grab these before memset
//如果obj不等于烧饼对象
if (obj != POOL_BOUNDARY) {
// release count+1 times since it is count of the additional
// autoreleases beyond the first one
//释放额外的引用计数
for (int i = 0; i < count + 1; i++) {
objc_release(obj);
}
}
}
pop
判断*stop
是不是哨兵对象,page
是不是冷页(AutoreleasePoolPage *),token
(void *)和stop
(id *)可以认为是一样的. 最后都赋值为上面判断的对象, 继续调用popPage
popPage
是调用了page->releaseUntil(stop);
releaseUntil
调用了objc_release
, 直到找到传入的烧饼对象为止.
所以, autorelease最后还是调用了objc_release, 只不过objc_release是由自动释放池管理的.
探索总结
- 重点的不是哪个知识点, 而是解决问题的方式和方法..
push
找到page
存入对象,页有满, 有热, 有冷. 页与页的链接是双向链表结构, 通过指针链接, 有深度.pop
释放页中的对象到传入的哨兵对象为止.
extern void _objc_autoreleasePoolPrint(void); 可以查看自动释放池的情况
验证
创建一个mac
工程, 然后跑一下:
从上图我们可以看出,
0x38
就是56
, Page
的结构体大小. 接下来就是哨兵对象. 我们计算一下, end
之前我们算的是4096, 一个对象指针是8, 所以大概创建512个局部变量对象我们可以看到新页的创建. 因为还有哨兵和结构体本身的内存, 512肯定够了. 看下图:
图15和, 图16对比就可以看出, 一个
@autoreleasepool
只会创建一个哨兵对象, 创建页的时候不会创建哨兵对象.
如图17所示: 创建了506个对象, 第一页的
page
就变成了cold
和full
.
多层嵌套的情况, 都是根据哨兵对象来进行释放的.
回答问题
Autorelease
对象什么时候释放?
在AutoreleasePop被调用的时候释放.
系统在每个runloop迭代中都加入了自动释放池Push和Pop, 所以在RunLoop完成一次循环的时候会释放
AutoreleasePool
和创建AutoreleasePool
.
- 一个线程可以多少个
AutoreleasePool
?
这里要归结到文档, 线程在取
AutoreleasePool
的时候是根据tls
和定义的autoreleasekey
进行获取的, 所以只会获取当前的hotPage
. 所以只要autoreleasePool
创建了, 就一定会有hotPage
, 所以不管多少autoreleasePool
最终都是一个.双向链表结构每创建一个自动释放池,就会在当前线程的 poolPage 的栈中先添加一个边界对象,然后把池中的对象添加进去,直至栈满,创建子 page,继续添加。
所以线程和
AutoreleasePool
是一一对应的. 重复调用只是添加哨兵对象.
这张图应该够清晰了, 在一条线程里创建了两个
@autoreleasepool
,
- main函数的
@autoreleasepool
做了什么?
main
函数里面的注释是// Setup code that might create autoreleased objects goes here.
可能释放的对象放在这里, 也就是说, 在这里只是加了一个池子.并没有什么特殊的用法. 而且在return
之前已经返回了.
- 在子线程中创建的对象,如果没有启动
runloop
,也没有声明AutoreleasePool
,怎么管理呢?
上图可知, 自己默认创建的有自动缓冲池.
- 已知已经有了
AutoreleasePool
,但是没有runloop
,子线程中的AutoreleasePool
什么时候会清空?
线程在销毁时, 会清理掉
AutoreleasePool
总结
写了什么东西不重要, 重要的是思路, 线程和runloop
以及AutoreleasePool
都不是单一的知识点, 都是相互协作的.
搞清楚下面的关系:
AutoreleasePool
和runloop
的关系AutoreleasePool
和线程
的关系AutoreleasePool
和Autorelease
的关系
转载自:https://juejin.cn/post/6991018639927279623