AutoReleasePool

在很久之后再次开始看基础,这一次,看一下 AutoReleasePool,AutoReleasePool 里面涵盖了很多知识点,是一个验证自己知识体系的好帮手。

一 简介

AutoReleasePool是自动释放池,打造了一个类似于C语言中,局部变量出了作用域变量就自动释放的效果。在MRC时代就已经有了,在ARC下,开发者直接使用不再那么多,但是系统内部使用非常多。
在 ARC 下,不能直接使用 NSAutoReleasePool 对象了,采用如下方式使用

@autoreleasepool {
}

下面就总结下 AutoReleasePool 特性及实现原理。

二 特性

首先要了解 autorelease,在MRC时代,使用 retain/release 管理对象引用计数。autorelease 和 release 类似,表示引用计数-1,但是时机是在当前 autorelease drain 之后。在 ARC 中,不再显示调用 drain,对象 autorelease 实际为出了 @autoreleasepool 花括号作用域时。
AutoReleasePool 是线程隔离的,在不同线程中,互不影响。如果线程开启了 runloop,那么在 runloop 的 Entry(即将进入Loop), BeforeWaiting(准备进入休眠) ,Exit(即将退出Loop) 时,都会将该线程的所有 AutoReleasePool 清空或者重新创建 AutoReleasePool 并加入待释放对象。
以主线程为例,APP 的 main 函数,就有一个大的 AutoReleasePool。

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
    }
    return 0;
}

三 实现

首先,珠玉在前,引用别人整理好的 blog :https://zhuanlan.zhihu.com/p/150569825
关键点:

  1. 源码编译:用 https://github.com/LGCooci/objc4_debug 中已经修改好的可编译源码
  2. AutoReleasePool 的 push 和 pop 操作
  3. 以 AutoReleasePoolPage 为节点的双向链表存储结构
  4. AutoReleasePoolPage 结构
  5. 一个 @autoreleasepool 和 AutoReleasePoolPage 的对应关系
  6. 实现结构中的优化点

clang 重写成 cpp 代码

struct __AtAutoreleasePool {
  __AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
  ~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
  void * atautoreleasepoolobj;
};

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; }
    return 0;
}

引出了关键俩函数 objc_autoreleasePoolPushobjc_autoreleasePoolPop
push 过程中,核心函数是 autoreleaseFast

 static inline id *autoreleaseFast(id obj)
    {
        AutoreleasePoolPage *page = hotPage(); // 双向链表中的最后一个 Page
        if (page && !page->full()) {// 如果当前 Page 存在且未满
            return page->add(obj);      // 将 autorelease 对象入栈,即添加到当前 Page 中;
        } else if (page) { // 如果当前 Page 存在但已满
            return autoreleaseFullPage(obj, page); // 创建一个新的 Page,并将 autorelease 对象添加进去
        } else {// 如果当前 Page 不存在,即还没创建过 Page
            return autoreleaseNoPage(obj);      // 创建第一个 Page,并将 autorelease 对象添加进去
        }
    }

结构

正常的变量,都是存在于堆上,大小固定。而所有的 AutoReleasePool 对象,都是共用同一块区间。他的结构是一个双向链表,每一个节点是一个 AutoReleasePoolPage,有 parent & child 指针,可以向上向下寻找其他 page 对象,每个 Page 大小 4K 字节。

Page 的数据区是一个栈,用来存储加入到 pool 里的对象引用,并在合适的时机遍历调用 release。每个 pool 都会用边界(哨兵) POOL_BOUNDARY (nil)隔开,所以当 pop 操作时,就是将到上一个 POOL_BOUNDARY 间的对象都出栈,并 release 。

当前使用的 Page 称之为 hotpage,第一个 Page 被称为 coldpage 。当增加一个 autorelease 调用,该对象的引用,都会被压入 hotpage 栈顶,当栈满了,就顺着 child 去下一 Page(没有就新建)寻找可存储的位置,并更新hotpage 位置。

hotpage 指针也是被存储在当前线程的 thread_local variables 中,是每次使用AutoReleasePool 可以通过 key 直接找到的入口。

黑魔法之Thread Local Storage
Thread Local Storage(TLS)线程局部存储,目的很简单,将一块内存作为某个线程专有的存储,以key-value的形式进行读写.
在返回值身上调用objc_autoreleaseReturnValue方法时,runtime将这个返回值object储存在TLS中,然后直接返回这个object(不调用autorelease);同时,在外部接收这个返回值的objc_retainAutoreleasedReturnValue里,发现TLS中正好存了这个对象,那么直接返回这个object(不调用retain)。
于是乎,调用方和被调方利用TLS做中转,很有默契的免去了对返回值的内存管理。这样就省去了两个操作:autorelease和外部的一次retain操作,对于性能提高很多。

双链表

image

每个Page

struct AutoreleasePoolPageData
{
    magic_t const magic; // 校验数字
    __unsafe_unretained id *next; // 栈顶指针
    pthread_t const thread; // 所在线程
    AutoreleasePoolPage * const parent;
    AutoreleasePoolPage *child;
    uint32_t const depth; // 深度
    uint32_t hiwat; // 高水位

    AutoreleasePoolPageData(__unsafe_unretained id* _next, pthread_t _thread, AutoreleasePoolPage* _parent, uint32_t _depth, uint32_t _hiwat)
        : magic(), next(_next), thread(_thread),
          parent(_parent), child(nil),
          depth(_depth), hiwat(_hiwat)
    {
    }
};

详情

image

优化

(当仅推入一个池且从未包含任何对象时,EMPTY_POOL_PLACEHOLDER就会存储在TLS中。当顶层(即libdispatch)推送并弹出池但从不使用它们时,这样可以节省内存。)在应用加载的时候 loadimage 时有很多的空 autoreleasepool 在加载(具体不清楚是什么)。这个场景是,所有的 autoReleasePage 都没有对象,就用这个来占位,而不是一个真正的 Page 对象。

在一个 Pool 中对象调用完 release 后,会将所有的空 page 清除掉。但如果页面已满一半,则保留一个空孩子。

线程/runloop相关

和 runloop 的关系,尚不明确。仅知道如果线程如果没 runloop ,在使用 pool 的时候也会创建。如果有 runloop,就会自动创建。

何时进行的线程绑定?
在可以看到的调用处(有一些代码没有开源), map_images_nolock 时,会调用 init 静态方法,里面产生了一些和线程的关联,在线程销毁时,也会将 pool 销毁。目前知道,在主线程当中,注册了两个监听器,监听 Entry(即将进入Loop), BeforeWaiting(准备进入休眠) ,Exit(即将退出Loop) 时,都对 pool 进行了相应的 pop push 操作。

    static void init()
    {
        int r __unused = pthread_key_init_np(AutoreleasePoolPage::key, 
                                             AutoreleasePoolPage::tls_dealloc);
        ASSERT(r == 0);
    }

    static void tls_dealloc(void *p) 
    {
        if (p == (void*)EMPTY_POOL_PLACEHOLDER) {
            // No objects or pool pages to clean up here.
            return;
        }

        // reinstate TLS value while we work
        setHotPage((AutoreleasePoolPage *)p);

        if (AutoreleasePoolPage *page = coldPage()) {
            if (!page->empty()) objc_autoreleasePoolPop(page->begin());  // pop all of the pools
            if (slowpath(DebugMissingPools || DebugPoolAllocation)) {
                // pop() killed the pages already
            } else {
                page->kill();  // free all of the pages
            }
        }
        
        // clear TLS value so TLS destruction doesn't loop
        setHotPage(nil);
    }

Q:关于不同的对象,为何不能触发 autorelease,这个可能和 taggedPoint 有关,待了解。

四 参考

加入pool的对象条件
很全面的介绍blog
结构图的引用
带有内存地址的单个Page结构
有 __builtin_return_address、TLS 的介绍, sunnyxx的blog,图片缺失
magic校验内容

你可能感兴趣的:(AutoReleasePool)