以前大概看过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
探索思路
正常情况下, 我们不太了解文档, 或者地址, 要怎么去探索呢?
第一步:
xcrunclang, 其实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=4096b, 所以一页page的大小为4kbfull()就是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 *)可以认为是一样的. 最后都赋值为上面判断的对象, 继续调用popPagepopPage是调用了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的关系