iOS 自动释放池autoreleasepool(一)

前言

在前面几篇文章,说了关于OC中的内存布局、内存管理方案、以及MRC情况下的retainCount、retain、release,但是MRC也已经是过去式了,这次来说说ARC。
从MRC到ARC的变化 就取决于@autoreleasepool。

@autoreleasepool 自动释放池:
管理内存的池,把不需要的对象放在自动释放池中,自动释放(延迟释放)这个池子内的对象。

@autoreleasepool的应用场景:

  1. 存在大量临时变量的时候
  2. 非UI操作,如:命令行
  3. 自己创建辅助线程

从哪开始

AutoreleasePool创建和释放

我们可以在工程中随便找个地方打个断点
po [NSRunLoop currentRunLoop]

image.png
  • App启动后,苹果在主线程 RunLoop 里注册了两个 Observer,其回调都是 _wrapRunLoopWithAutoreleasePoolHandler()。
  • 第一个 Observer 监视的事件是 Entry(即将进入Loop),其回调内会调用 _objc_autoreleasePoolPush() 创建自动释放池。其 order 是-2147483647,优先级最高,保证创建释放池发生在其他所有回调之前。
  • 第二个 Observer 监视了两个事件: BeforeWaiting(准备进入休眠) 时调用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 释放旧的池并创建新池;Exit(即将退出Loop) 时调用 _objc_autoreleasePoolPop() 来释放自动释放池。这个 Observer 的 order 是 2147483647,优先级最低,保证其释放池子发生在其他所有回调之后。
  • 在主线程执行的代码,通常是写在诸如事件回调、Timer回调内的。这些回调会被 RunLoop 创建好的 AutoreleasePool 环绕着,所以不会出现内存泄漏,开发者也不必显示创建 Pool 了。

也就是说AutoreleasePool创建是在一个RunLoop事件开始之前(push),AutoreleasePool释放是在一个RunLoop事件即将结束之前(pop)。
AutoreleasePool里的Autorelease对象的加入是在RunLoop事件中,AutoreleasePool里的Autorelease对象的释放是在AutoreleasePool释放时。
以上内容参考自这位老哥

查看@autoreleasepool{ }编译成C++代码

使用编译器clang编译main.m转化成main.cpp文件(在终端:clang -rewrite-objc main.m)

$ cd main.m所在文件夹

$ clang -rewrite-objc main.m -o main.cpp

image.png

会在原有路径下生成一个main.cpp,点开之后我们可以找到如下代码:

image.png

可知:main函数在c++中被编译成了上图中的模样,核心重点一看就知道是这个__AtAutoreleasePool,在当前代码直接搜索,很容易就能找到下面这些代码

image.png

注意:这里的~是C++的析构函数(destructor) 与构造函数相反,当对象脱离其作用域时(例如对象所在的函数已调用完毕),系统自动执行析构函数。

从上图我们可以找到两个东西,从名字也很浅显,一个进池子一个出池子
objc_autoreleasePoolPush
objc_autoreleasePoolPop
结合上面的注意点,我觉得可以先这么总结一下

@autoreleasepool,就是把在它作用域(就是"{}")中的代码,先push进去,然后等这些代码都干完活了,再把他们pop出去。

走进源码

我们先看一下这个 objc_autoreleasePoolPush

void *
objc_autoreleasePoolPush(void)
{
    return AutoreleasePoolPage::push();
}

这里出现了一个AutoreleasePoolPage,且语法是C++的语法。我们这里先看看这个AutoreleasePoolPage是何方神圣。

AutoreleasePoolPage

我们先看看变量声明

class AutoreleasePoolPage 
{
#   define EMPTY_POOL_PLACEHOLDER ((id*)1)
#   define POOL_BOUNDARY nil
    static pthread_key_t const key = AUTORELEASE_POOL_KEY;
    static uint8_t const SCRIBBLE = 0xA3;  // 0xA3A3A3A3 after releasing
    static size_t const SIZE = 
#if PROTECT_AUTORELEASEPOOL
        PAGE_MAX_SIZE;  // must be multiple of vm page size
#else
        PAGE_MAX_SIZE;  // 4096字节 size and alignment, power of 2
#endif
    static size_t const COUNT = SIZE / sizeof(id);

    magic_t const magic; // 4*4字节 用来校验 AutoreleasePoolPage 的结构是否完整;
    id *next; // 8字节 指向最新添加的 autoreleased 对象的下一个位置,初始化时指向 begin() ;
    pthread_t const thread; // 8字节 指向当前线程;
    AutoreleasePoolPage * const parent; // 8字节,指向父结点,第一个结点的 parent 值为 nil ;双向链表上一个节点
    AutoreleasePoolPage *child; //8字节, 指向子结点,最后一个结点的 child 值为 nil ;双向链表下一个节点
    uint32_t const depth; // 4字节,代表深度,从 0 开始,往后递增 1;
    uint32_t hiwat; // 4字节 , 代表 high water mark 。
    //这几个加起来是56字节  
    ...
}
image.png

由上图可以看到AutoreleasePoolPage的大小为4096字节,其中自身变量占用56个字节。
这里可以来验证一下。
这里先介绍一下这个函数,这个可以打印当前AutoreleasePool中Page的状态

void
_objc_autoreleasePoolPrint(void); 

已知AutoreleasePoolPage size = 4096,自身占用56,那么剩余空间为4096-56=4040。
一个NSObject的大小为8字节,就是说一个AutoreleasePoolPage的剩余空间还可以容纳4040/8=505个NSObject。
所以就来一波这种操作

void
_objc_autoreleasePoolPrint(void);
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        for (int i = 0 ; i<505; i++) {
            NSObject *obj = [[NSObject new]autorelease];
        }
        _objc_autoreleasePoolPrint();
     }
    return 0;
}

然后看一下打印信息


image.png

中间省略几百个NSObject...


image.png

可以看到上面两张图合起来的话,一共出现了2个PAGE。

  • 第一个PAGE标记了(full)(cold)第一个词很容易理解,就是满了,第二个词大意就是“凉了”可以理解成不活跃了。
  • 第二个PAGE标记了(hot)就是(cold)的反义词咯,那就理解成活跃的意思。
    总而言之,就是第一个PAGE满了且被标记成非活跃状态,第二个PAGE没满且是活跃状态。

那么问题来了,按照我们先前的计算,505个不是应该刚刚好装满一个PAGE吗?这第二页是什么鬼?明显就是多出了一个什么奇怪的东西。
我们观察一下这三个地址

objc[32613]: [0x102002000] ................ PAGE (full) (cold)
objc[32613]: [0x102002038] ################ POOL 0x102002038
objc[32613]: [0x102002040] 0x100f8d780 NSObject

前两个地址的区别就在于一个38,这里是16进制,
所以38在十进制中为 3*16+8 = 56,所以这一系列的#号为变量进入pool的起点
第三个地址就是从38变成了40,38要变成40,在16机制中就是需要38中的8再加上8,让3变成4。
所以就是这 0x102002038 里面的东西在作怪。那这个又是什么呢?
这里我先贴一下部分代码

    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());

        // 翻译:pushExtraBoundary 推入一个额外边界
        bool pushExtraBoundary = false;
        if (haveEmptyPoolPlaceholder()) {
            //当push一个新页或者第一页的时候,在push之前需要先push一个poolboundary(边界)
            pushExtraBoundary = true;
        }
        else if (obj != POOL_BOUNDARY  &&  DebugMissingPools) {
            // 异常情况,抛错
            _objc_inform("MISSING POOLS: (%p) Object %p of class %s "
                         "autoreleased with no pool in place - "
                         "just leaking - break on "
                         "objc_autoreleaseNoPool() to debug", 
                         pthread_self(), (void*)obj, object_getClassName(obj));
            objc_autoreleaseNoPool(obj);
            return nil;
        }
        else if (obj == POOL_BOUNDARY  &&  !DebugPoolAllocation) {
            // We are pushing a pool with no pool in place,
            // and alloc-per-pool debugging was not requested.
            // Install and return the empty pool placeholder.
            return setEmptyPoolPlaceholder();
        }
        // 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);
    }

这里解读一下这段代码
当我们push一个对象进来时,没有page或者page满了,需要到新的一页的时候,他就会将pushExtraBoundary设置为true,在底下会判断pushExtraBoundary,如果为true,就会先push一个POOL_BOUNDARY进入page中。

所以这个POOL_BOUNDARY就是我们上文中的“奇怪的东西”,从字面翻译我们可以理解为这是一个“边界”或者叫“边界符”

总结:

  1. autoreleasepool由许许多多的AutoreleasePoolPage组成
  2. 当一个AutoreleasePoolPage装满之后,就会创建新的AutoreleasePoolPage,两个Page之间用parent/child互相关联,从而证明双向链表的说法
  3. 在AutoreleasePoolPage自身变量的56个字节之后,当push对象进page时,会先push一个边界符进去POOL_BOUNDARY。这个边界符也占8个字节

这里给一张图,可以更好的理解,加深印象


image.png

这篇文章先到这里,下一篇autoreleasepool将会解析push跟pop

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,163评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,301评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,089评论 0 352
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,093评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,110评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,079评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,005评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,840评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,278评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,497评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,667评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,394评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,980评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,628评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,796评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,649评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,548评论 2 352