Autorelease与AutoreleasePool

1,279 阅读9分钟

以前大概看过AutoreleasePool里面有AutoreleasePoolPage有热页, 冷页. 页有深度, 双向链表链接, 首位置有哨兵对象等等. 大概就这么多印象, 但是没有系统的梳理过. 最近在重新梳理一遍, 就当学习了, 希望可以表达的更清晰.

文档

AutoreleasePool官方文档 可以看到文档是在Advanced Memory Management Programming Guide类别下面.直接搜Autorelease其实是搜不到的.

文档很老, 现在不在更新了, 但是很多东西并不过时, 建议多多查看.

看文档如果看不懂的, 或者看起来很费力的伙计, 建议谷歌浏览器自带翻译. 机场推荐巴士, 挺好用的. 谷歌对应英文文档的翻译, 还原度还是相当高的, 结合理解绝大部分是够用的.

AutoreleasePool相关问题

  • Autorelease对象什么时候释放?
  • 一个线程可以多少个AutoreleasePool
  • main函数的@autoreleasepool做了什么?
  • 在子线程中创建的对象,如果没有启动runloop,也没有声明AutoreleasePool,怎么管理呢?
  • 已知已经有了AutoreleasePool,但是没有runloop,子线程中的AutoreleasePool什么时候会清空?

很多题目不会单单问AutoreleasePool做了什么, 干嘛的, 主要都是问和线程, RunLoop的关系而产生的一些问题. 下面我来进行AutoreleasePool的探究.

Autorelease

MRC的时代, retainrelease是一对亲人, 你创建我释放. 而为了更方便的使用, autorelease出现了, 它不需要用户自己去做release的动作, 只需要在创建的时候标注上, 后续会在将修饰的对象放入自动释放池之中, 会在一个合适的时机自动去调用release, 从而达到自动释放的效果.

AutoreleasePool

探索思路

正常情况下, 我们不太了解文档, 或者地址, 要怎么去探索呢?

第一步:

  • xcrun
  • clang, 其实xcrunclang可以算作为一种
  • 汇编 找到底层调用的真正的符号或者函数, 从而从创建到销毁进行探索.

第二步:

  • 拿到符号或者函数, 找到对应的库或者源码下载探索

第三步:

  • 如果没有开源, 可以去hopper系统动态库对应的可执行文件, 根据真实的符号, 查看汇编, 还原核心部分进行探索.
  • 比较适合轻量级的 大致就是这几种方案, 最好用的个人认为是汇编直接查找符号, 因为运行时的原因, 汇编看到的东西是真实的符号, clang看到的是静态编译的产物, 这瓜不保熟.

选择汇编探索

找到符号, 查看符号所在源码是否开源

新建一个程序, main函数入口的@autoreleasepool打一个断点. 然后汇编如下:

01.png 继续往内部跳转, 发现内部无法继续.

所以, 我们先拿到调用的符号objc_autoreleasePoolPush, 然后直接下符号断点, 如下:

02.png 看最上面, 我们看到了符号在libobjc中, 一直下载开源框架, 内部查看.

AutoreleasePoolPage结构

全局搜索符号objc_autoreleasePoolPush, 在NSObject.mm我们看到下图:

03.png 我们继续查看AutoreleasePoolPage是个什么东西, 如下:

04.png 可以看到是一个C++的类, 继承与一个叫AutoreleasePoolPageData的结构体, 之前有人疑惑类和结构体怎么继承? 在C++中, 类和结构体没有什么区别只是默认是public.

继续看下AutoreleasePoolPageData的结构:

05.png 很清晰的双向链表, 之前YYCache基本也是这样, 一般看到child, parent, sibling基本上数据结构都是固定的.

从上面得到的信息, 调用@autoreleasepool的时候:

  • 1.实际上调用了AutoreleasePoolPage::push();这么一个函数
  • 2.AutoreleasePoolPage继承AutoreleasePoolPageData结构体的
  • 3.AutoreleasePoolPageData类似于一个双向链表的节点, 每个节点就是一个AutoreleasePoolPage内部有childparent两个指针, 指向下一节点.

AutoreleasePoolPage::push()做了什么

接下来我们, 继续看push()方法内部做了什么.

06.png

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做了什么:

07.png

  • 先取出当前的hotPage, 如果不空不满, 那么将obj加入当前page
  • 如果page存在且不满, 那么走autoreleaseFullPage
  • 如果page不存在且不满, 那么走autoreleaseNoPage
hotPage(), 取出hotPage

hotPage就是一个热页(当做一个状态), 就是当前加载的页,在最前线正在使用的. 冷页就是距离当前页最远的.

这里出现了一个hotPage(), 那么是什么呢? 又是如何取的:

09.png 可以看到是根据tls_get_direct函数和一个key获取的, 在这里不多赘述了, 给大家看一下key:

10.png

11.png 这里tls是操作系统给每个线程会分配一个很小的空间地址, 存储一些所需要的值, 这里的key就是AUTORELEASE_POOL_KEY, 每一个线程生成的一个线程key对应的AUTORELEASE_POOL_KEY的结构体作为keytls空间寻找对应的指针.

从里面可以看到, SYNC_COUNTSYNC_DATA对应的都有自己的用处, 有兴趣的可以自己研究一下.

下面看一段注释:

12.png 这个注释里, 也很清楚的说明了, tls存储了指向hot page的指针.

autoreleaseFullPage

13.png 是一个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

14.png 全都是根据next指针的地址进行判断的

  • begin()的位置等于AutoreleasePoolPage+当前自己的结构体大小, 就是起始位置+56.
  • end()的位置等于起始位置+SIZE, SIZE定义为1<<12=4096b, 所以一页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工程, 然后跑一下:

15.png 从上图我们可以看出, 0x38就是56, Page的结构体大小. 接下来就是哨兵对象. 我们计算一下, end之前我们算的是4096, 一个对象指针是8, 所以大概创建512个局部变量对象我们可以看到新页的创建. 因为还有哨兵和结构体本身的内存, 512肯定够了. 看下图:

16.png 图15和, 图16对比就可以看出, 一个@autoreleasepool只会创建一个哨兵对象, 创建页的时候不会创建哨兵对象.

17.png 如图17所示: 创建了506个对象, 第一页的page就变成了coldfull.

多层嵌套的情况, 都是根据哨兵对象来进行释放的.

回答问题

  • Autorelease对象什么时候释放?

在AutoreleasePop被调用的时候释放.

系统在每个runloop迭代中都加入了自动释放池Push和Pop, 所以在RunLoop完成一次循环的时候会释放AutoreleasePool和创建AutoreleasePool.

  • 一个线程可以多少个AutoreleasePool

这里要归结到文档, 线程在取AutoreleasePool的时候是根据tls和定义的autoreleasekey进行获取的, 所以只会获取当前的hotPage. 所以只要autoreleasePool创建了, 就一定会有hotPage, 所以不管多少autoreleasePool最终都是一个.

双向链表结构每创建一个自动释放池,就会在当前线程的 poolPage 的栈中先添加一个边界对象,然后把池中的对象添加进去,直至栈满,创建子 page,继续添加。

所以线程和AutoreleasePool是一一对应的. 重复调用只是添加哨兵对象.

18.png

18.png 这张图应该够清晰了, 在一条线程里创建了两个@autoreleasepool,

  • main函数的@autoreleasepool做了什么?

main函数里面的注释是// Setup code that might create autoreleased objects goes here.可能释放的对象放在这里, 也就是说, 在这里只是加了一个池子.并没有什么特殊的用法. 而且在return之前已经返回了.

  • 在子线程中创建的对象,如果没有启动runloop,也没有声明AutoreleasePool,怎么管理呢?

19.png 上图可知, 自己默认创建的有自动缓冲池.

  • 已知已经有了AutoreleasePool,但是没有runloop,子线程中的AutoreleasePool什么时候会清空?

线程在销毁时, 会清理掉AutoreleasePool

总结

写了什么东西不重要, 重要的是思路, 线程和runloop以及AutoreleasePool都不是单一的知识点, 都是相互协作的.

搞清楚下面的关系:

  • AutoreleasePoolrunloop的关系
  • AutoreleasePool线程的关系
  • AutoreleasePoolAutorelease的关系