拾陆:AutoreleasePool实现原理

前言

通过前面runloop文章中,我们知道在 mainRunloop 存在两个关于 autoreleasePoolRunLoopObserver,分别监听了 runloop 的 ①. 进入(Entry)、②. BeforeWaiting(准备进入休眠)和Exit(即将退出Loop)

  1. 进入(Entry): 监听到进入后会调用 _objc_autoreleasePoolPush()函数创建自动释放池。
  2. 准备进入休眠(BeforeWaiting):调用_objc_autoreleasePoolPop()_objc_autoreleasePoolPush() 释放旧池并创建新池。
  3. 即将退出Loop(Exit):调用_objc_autoreleasePoolPop() 来释放_autoreleasePool

所以基本上我们不需要手动创建 AutoreleasePool。

NSAutoreleasePool

在 MRC 的时代,我们通过 NSAutoreleasePool 类或者@autoreleasepool创建自动释放池子,并调用对象的autorelease方法,将对象放入自动释放池子中。

// main.m MRC环境
#import <Foundation/Foundation.h>
#import "Person.h"

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *p = [[Person new] autorelease];
        NSLog(@"%@",p);
    }
    return 0;
}

通过xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m我们将代码编译生成 C++ 代码。

image

代码中的@autoreleasepool{}会转换成__AtAutoreleasePool的结构体对象。

struct __AtAutoreleasePool {
  __AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();} // 构造函数
  ~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}  // 析构函数
  void * atautoreleasepoolobj;
};

在上图中__AtAutoreleasePool __autoreleasepool;首先会调用结构体的构造函数objc_autoreleasePoolPush(),将对象放入自动释放池中,再当结构体的局部对象__autoreleasepool离开作用域后,调用析构函数objc_autoreleasePoolPop(atautoreleasepoolobj)对自动释放池内的对象进行一次release操作。

AutoreleasePool 结构

autoreleasepool通过__AtAutoreleasePool的构造函数、析构函数来创建和释放的了,那么autoreleasepool又是如何组织、存放自动释放池的对象的呢?

让我通过 obj4 的源码来一探究竟,首先我们可以在 NSObject.mm 找到 objc_autoreleasePoolPush的实现。它主要是调用了AutoreleasePoolPage类的类方法push,返回AutoreleasePoolPage对象。

objc_autoreleasePoolPop函数中也是对AutoreleasePoolPage对象进行操作。可以推断出autoreleasepool自定释放池对自动释放对象的管理正是通过AutoreleasePoolPage对象来实现的。

class AutoreleasePoolPage 
{

    static size_t const SIZE = 
#if PROTECT_AUTORELEASEPOOL
        PAGE_MAX_SIZE;  // must be multiple of vm page size
#else
        PAGE_MAX_SIZE;  // size and alignment, power of 2
#endif
    magic_t const magic;
    id *next;
    pthread_t const thread;
    AutoreleasePoolPage * const parent;
    AutoreleasePoolPage *child;
    uint32_t const depth;
    uint32_t hiwat;
    // ... other methods
}

AutoreleasePoolPage类中定义了Page的空间大小PAGE_MAX_SIZE = 4096 个字节,除去上面代码中存放内部的成员变量外,所有剩下的空间都是用来存放 autorelease 的对象地址(8字节)。

成员变量next指向下一个自动释放对象地址的指针存放在当前的page的位置,而且每个 page 通过 parentchild 通过双向链表的形式连接。 thread记录当前page所在的线程,AutoreleasePool是与线程一一对应的.

image

自动释放的对象从上图的红色 begin 箭头处开始存放,当一个 page 存放达到上限后,会在创建一个 page 并通过parentchild两个指针关联这两个page对象。以此类推每个page都存放了。

AutoreleasePool 工作流程

我们知道了 AutoreleasePool 两个主要的数据结构:__AtAutoreleasePoolAutoreleasePoolPage。现在来总结下 AutoreleasePool 工作的整个流程。

将最开始的代码@autoreleasepool{}替换成__AtAutoreleasePool的实现。


int main(int argc, const char * argv[]) {
//    @autoreleasepool {
    __AtAutoreleasePool atautoreleasepoolobj = objc_autoreleasePoolPush();
        Person *p = [[Person new] autorelease];
        NSLog(@"%@",p);
    objc_autoreleasePoolPop(atautoreleasepoolobj)
//  }
    return 0;
}

objc_autoreleasePoolPush

objc_autoreleasePoolPush()函数:

#define POOL_BOUNDARY = nil
    static inline void *push() 
    {
        id *dest;
        if (DebugPoolAllocation) {
            dest = autoreleaseNewPage(POOL_BOUNDARY);
        } else {
            dest = autoreleaseFast(POOL_BOUNDARY);
        }
        assert(dest == EMPTY_POOL_PLACEHOLDER || *dest == POOL_BOUNDARY);
        return dest;
    }

首先会将POOL_BOUNDARY(一个nil对象)入栈,并返回当前压入栈中的地址值,用作 pop 时的终点。调用autorelease方法的对象会调用到 AutoreleasePoolPageautoreleaseFast 函数,添加到 page 中,在objc_autoreleasePoolPop函数调用时会将POOL_BOUNDARY的地址值传入,用作自动释放池pop的停止边界。

当然系统会考虑到很多情况,比如当前的page情况比如是否存在、page是否满了。大概的过程如下:

  • 如果存在page、且未满会直接通过next指针将对象地址存放在next中,存在page。
  • 如果page 存在且满了,会创建一个新的page再压入POOL_BOUNDARY,再添加autorelease对象,然后通过parentchild将上下两个page关联
  • 如果page都不存在那么会创建一个新的page、压入POOL_BOUNDARY后再添加autorelease对象。

objc_autoreleasePoolPop

objc_autoreleasePoolPop(atautoreleasepoolobj)自动释放池的开始释放。其中atautoreleasepoolobj就是我们push时返回的边界地址(POOL_BOUNDARY)。objc_autoreleasePoolPop对调用AutoreleasePoolPagepop方法,如下。

// token == atautoreleasepoolobj  == 每次 push 时的`POOL_BOUNDARY `
  static inline void pop(void *token) 
    {
        AutoreleasePoolPage *page;
        id *stop;

        if (token == (void*)EMPTY_POOL_PLACEHOLDER) {
            // Popping the top-level placeholder pool.
            if (hotPage()) {
                // Pool was used. Pop its contents normally.
                // Pool pages remain allocated for re-use as usual.
                pop(coldPage()->begin());
            } else {
                // Pool was never used. Clear the placeholder.
                setHotPage(nil);
            }
            return;
        }

        page = pageForPointer(token);
        stop = (id *)token;
        if (*stop != POOL_BOUNDARY) {
            if (stop == page->begin()  &&  !page->parent) {
                // Start of coldest page may correctly not be POOL_BOUNDARY:
                // 1. top-level pool is popped, leaving the cold page in place
                // 2. an object is autoreleased with no pool
            } else {
                // Error. For bincompat purposes this is not 
                // fatal in executables built with old SDKs.
                return badPop(token);
            }
        }

        if (PrintPoolHiwat) printHiwat();

        page->releaseUntil(stop);

        // memory: delete empty children
        if (DebugPoolAllocation  &&  page->empty()) {
            // special case: delete everything during page-per-pool debugging
            AutoreleasePoolPage *parent = page->parent;
            page->kill();
            setHotPage(parent);
        } else if (DebugMissingPools  &&  page->empty()  &&  !page->parent) {
            // special case: delete everything for pop(top) 
            // when debugging missing autorelease pools
            page->kill();
            setHotPage(nil);
        } 
        else if (page->child) {
            // hysteresis: keep one empty child if page is more than half full
            if (page->lessThanHalfFull()) {
                page->child->kill();
            }
            else if (page->child->child) {
                page->child->child->kill();
            }
        }
    }

主要是三个操作

  • 使用 pageForPointer 获取当前 token 所在的 page,
  • 调用 releaseUntil 方法释放栈中的对象,直到 stop 边界(即push时返回的POOL_BOUNDARY地址)。
  • 调用 child 的 kill 方法,删除空的child page 对象。

延伸1: AutoReleasePool的循环嵌套

有时候我们会碰到AutoReleasePool嵌套的情况,那么自动释放的对象又是如何组织和管理的呢?

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSObject *obj1 = [[NSObject alloc] init];
        
    @autoreleasepool {
        NSObject *obj2 = [[NSObject alloc] init];
        NSObject *obj3 = [[NSObject alloc] init];
        
          @autoreleasepool {
              NSObject *obj4 = [[NSObject alloc] init];
            }
      }
    }
    return 0;
}

按照我们之前对 autoreleasepool的理解后,我们可以将代码转换成:

int main(int argc, const char * argv[]) {
     atautoreleasepoolobj1 = objc_autoreleasePoolPush();
        NSObject *obj1 = [[NSObject alloc] init];
         atautoreleasepoolobj2 = objc_autoreleasePoolPush();
        NSObject *obj2 = [[NSObject alloc] init];
        NSObject *obj3 = [[NSObject alloc] init];
           atautoreleasepoolobj3 = objc_autoreleasePoolPush();
              NSObject *obj4 = [[NSObject alloc] init];
            objc_autoreleasePoolPop(atautoreleasepoolobj3)
      objc_autoreleasePoolPop(atautoreleasepoolobj2)
    objc_autoreleasePoolPop(atautoreleasepoolobj1)
    return 0;
}

每层的嵌套,都会调用一次objc_autoreleasePoolPush函数,在objc_autoreleasePoolPush时会有一次入栈操作(POOL_BOUNDARY入栈)。作为这一层autoreleasepoolpop的停止边界,大概的结构如下图。

image

延伸2: Runloop 与AutoReleasePool的关系

注意:在上文提到过在mainRunloop中存在RunLoopObserver,分别监听了 runloop 的 ①. 进入(Entry)、②. BeforeWaiting(准备进入休眠)和Exit(即将退出Loop),会自动调用_objc_autoreleasePoolPush()_objc_autoreleasePoolPop() 所以一般情况下我们可以不用自己手动的去创建autoreleasePool

延伸3: autorelease对象在什么时候释放

autorelease对象的释放在不同场景下释放的时机不一样,

  • autorelease对象直接是由@autorelasepool{}包裹,那么autorelease对象会在@autorelasepool{}的大括号之后释放。
  • 没有显式使用@autorelasepool{},会根据某次Runloop循环中调用_objc_autoreleasePoolPop()释放(可能是在runloop休眠之前或者退出runloop时)。

tip: ARC模式下,方法内的局部变量会在方法的大括号之后就会被释放,ARC模式下可能直接插入的 release 方法。

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

推荐阅读更多精彩内容