一文吃透autorelease

全文速览

  • 引子
  • activities与order的含义
  • _wrapRunLoopWithAutoreleasePoolHandler反汇编分析
  • autorelease在runloop中的调用时机
  • NSPushAutoreleasePool与NSPopAutoreleasePool的调用情况
  • AutoreleasePoolPage详解
  • hotPage与codePage
  • objc_autoreleasePoolPush
  • objc_autoreleasePoolPop
  • @autoreleasepool
引子

先对autorelease的调用情况有个基本认知,新建iOS工程,直接在viewDidLoad中打上断点,如图:

运行程序后在控制台执行po [NSRunLoop mainRunLoop]

可见在iOS中,系统给mainRunLoop添加了6个observer,其中2个observer对应的callback都是_wrapRunLoopWithAutoreleasePoolHandler函数,并且两者函数地址相同,所以这两个callback是同一函数。


activities与order的含义

这两个observer的activities分别为0x10xa0,order分别为-21474836472147483647

从文档中的这句话可知,order越小优先级越高,所以activities为0x1回调的优先级大于activities为0xa0的回调

activities是一组用来标识runloop状态的标识符,可通过CFRunLoopActivity得到:

/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry = (1UL << 0),
    kCFRunLoopBeforeTimers = (1UL << 1),
    kCFRunLoopBeforeSources = (1UL << 2),
    kCFRunLoopBeforeWaiting = (1UL << 5),
    kCFRunLoopAfterWaiting = (1UL << 6),
    kCFRunLoopExit = (1UL << 7),
    kCFRunLoopAllActivities = 0x0FFFFFFFU
};

typedef unsigned long CFOptionFlags;

对照上文,0x1就是kCFRunLoopEntry,而0xa0则是kCFRunLoopBeforeWaiting | kCFRunLoopExit两者组合。
所以_wrapRunLoopWithAutoreleasePoolHandler会检测3种runloop状态,当runloop状态为kCFRunLoopEntry 时会最先回调,而kCFRunLoopBeforeWaiting 与kCFRunLoopExit 状态则最后回调。

对callback函数地址下断点,发现他在UIKitCore中。如果通过image lookup来查找这个函数还可以看到,除UIKitCore外,BaseBoard与MapKit中也有同名函数。

_wrapRunLoopWithAutoreleasePoolHandler反汇编分析

打上符号断点后,重新运行程序:

可以看到,这里会对activities的值进行检测,检测值分别为0x800x200x1,对照CFRunLoopActivity ,分别为kCFRunLoopExit 、kCFRunLoopBeforeWaiting 与kCFRunLoopEntry ,这与上文相互验证。

  1. 如果activities为0x80,跳转到0x7fff47848ced处,后续会调用NSPopAutoreleasePool,后面还有个与0x20的判等比较,由于不等所以走后续return流程

  2. 如果activities为0x20,同样跳转到0x7fff47848ced处,后续调用NSPopAutoreleasePool,由于0x20判等比较,此时相等,跳转到0x7fff47848cd1处,接着执行NSPushAutoreleasePool。由于开头将rsp指向rbp,后面执行popq %rbp会跳出当前函数

  3. 如果activities不是0x1,会跳转到0x7fff47848d2e处,后续结束当前函数。因为当前函数的activities只可能是0x80、0x20、0x1这三个数,前面判等0x80与0x20后仍然没有跳转,那么当前activities一定是0x1,此时同样调用NSPushAutoreleasePool后,通过popq %rbp跳出当前函数

autorelease在runloop中的调用时机

通过刚才的反汇编分析,可以得到以下结论:

  1. 当前runloop状态为kCFRunLoopExit(退出runloop) 时,会调用NSPopAutoreleasePool,由于优先级最低,此处可确保在其他回调完成后释放缓存池

  2. 当前runloop状态为kCFRunLoopBeforeWaiting(runloop即将休眠)时,会先调用NSPopAutoreleasePool,再调用NSPushAutoreleasePool。对应着释放旧池并创建新池,由于优先级最低,这一操作也在其他回调之后

  3. 当前runloop状态为kCFRunLoopEntry(进入runloop) 时,会调用NSPushAutoreleasePool,由于此时优先级最高,可以确保创建缓存池在其他回调之前

NSPushAutoreleasePool与NSPopAutoreleasePool的调用情况

可见,NSPushAutoreleasePool 会调用objc_autoreleasePoolPush,而NSPopAutoreleasePool 会调用objc_autoreleasePoolPop。

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

void objc_autoreleasePoolPop(void *ctxt)
{
    AutoreleasePoolPage::pop(ctxt);
}

两者都通过AutoreleasePoolPage类调用了对应的push和pop函数

AutoreleasePoolPage详解

本文采用当前最新objc4-756.2源码,在NSObject.mm中可以看到AutoreleasePoolPage定义。去除内部函数后,AutoreleasePoolPage可简化为如下所示:

class AutoreleasePoolPage 
{
    // EMPTY_POOL_PLACEHOLDER is stored in TLS when exactly one pool is 
    // pushed and it has never contained any objects. This saves memory 
    // when the top level (i.e. libdispatch) pushes and pops pools but 
    // never uses them.
#   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;  // size and alignment, power of 2
#endif
    static size_t const COUNT = SIZE / sizeof(id);

    magic_t const magic;
    id *next;
    pthread_t const thread;
    AutoreleasePoolPage * const parent;
    AutoreleasePoolPage *child;
    uint32_t const depth;
    uint32_t hiwat;
}

显然,AutoreleasePoolPage是个双向链表

  • EMPTY_POOL_PLACEHOLDER
    从注释可知,EMPTY_POOL_PLACEHOLDER 用于标识一个空的自动释放池,一些系统方法中的block,如NSArray的实例方法:
- (void)enumerateObjectsUsingBlock:(void (NS_NOESCAPE ^)(ObjectType obj, NSUInteger idx, BOOL *stop))block;

对应文档中有这么句话:

This method executes synchronously. Values allocated within the block are deallocated after the block is executed.

显然,block内部添加了自动释放池,但如果block内部并无其他autorelease对象,且block的参数并未被使用,这个自动释放池会被标识为EMPTY_POOL_PLACEHOLDER

  • POOL_BOUNDARY
    自动释放池的边界标识,又称哨兵对象。push时会向池中添加哨兵对象,pop时会对哨兵对象之前的对象发送release消息

  • key

static pthread_key_t const key = AUTORELEASE_POOL_KEY;
#define AUTORELEASE_POOL_KEY  ((tls_key_t)__PTK_FRAMEWORK_OBJC_KEY3)
#define __PTK_FRAMEWORK_OBJC_KEY3    43

key是一个静态常量43,用来设置/取出当前page

  • SCRIBBLE
    在releaseUntil函数中有这么句代码memset((void*)page->next, SCRIBBLE, sizeof(*page->next)),可见SCRIBBLE只是个填充字节

  • SIZE

    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

#define PROTECT_AUTORELEASEPOOL 0
#define PAGE_MAX_SIZE           PAGE_SIZE
#define PAGE_SIZE               I386_PGBYTES
#define I386_PGBYTES            4096            /* bytes per 80386 page */

每个AutoreleasePoolPage对象大小为4096

  • COUNT
    AutoreleasePoolPage可存储的对象个数,因为SIZE等于4096个字节,所以COUNT等于512个,由于AutoreleasePoolPage本身成员变量占用56个字节(下文会说为什么占56个字节),所以一个page实际能存储的autorelease对象为505个

  • magic

struct magic_t {
...
    uint32_t m[4];
...
}

magic_t const magic;

magic是结构体magic_t的实例,用于校验AutoreleasePoolPage的完整性,共占16个字节

  • next
    next用于指向当前page中下一个要存储autorelease的位置,这是个栈结构,源码中用*next++ = obj来存储autorelease对象,用id obj = *--page->next来取出autorelease对象进行release,next是个指针,共占8字节

  • thread

pthread_t const thread;
typedef __darwin_pthread_t pthread_t;
typedef struct _opaque_pthread_t *__darwin_pthread_t;

thread为当前page所属线程,是个指针,共占8字节

  • parent与child
    parent与child分别指向当前page的父节点与子节点,两者都是指针,都占8字节

  • depth与hiwat
    depth为链表深度,hiwat用于记录链表中存储autorelease对象的最大个数,两者各站4字节

  • page在未添加autorelease对象时占用的字节数
    16 magic + 8 next + 8 thread + 8 parent + 8 child + 4 depth + 4 hiwat ,即56字节

hotPage与codePage
static inline AutoreleasePoolPage *hotPage()
{
    AutoreleasePoolPage *result = (AutoreleasePoolPage *)
        tls_get_direct(key);
    if ((id *)result == EMPTY_POOL_PLACEHOLDER) return nil;
    if (result) result->fastcheck();
    return result;
}

static inline void setHotPage(AutoreleasePoolPage *page)
{
    if (page) page->fastcheck();
    tls_set_direct(key, (void *)page);
}

static inline AutoreleasePoolPage *coldPage()
{
    AutoreleasePoolPage *result = hotPage();
    if (result) {
        while (result->parent) {
            result = result->parent;
            result->fastcheck();
        }
    }
    return result;
}

hotPage会从TLS中取出当前page并返回,而codePage则是找到这个双向链表的头部然后返回

objc_autoreleasePoolPush
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;
}

static inline id *autoreleaseFast(id obj)
{
    AutoreleasePoolPage *page = hotPage();
    if (page && !page->full()) {
        return page->add(obj);
    } else if (page) {
        return autoreleaseFullPage(obj, page);
    } else {
        return autoreleaseNoPage(obj);
    }
}

#   define POOL_BOUNDARY nil
  1. 当前页未满,直接添加obj
  2. 当前页已满,通过autoreleaseFast 找到page的子节点,直到子节点的page未满,将obj添加到未满的page上
  3. 没有page,通过autoreleaseNoPage创建page添加obj

由于push中调用的autoreleaseFast的参数为POOL_BOUNDARY,而这个宏是nil,所以objc_autoreleasePoolPush其实是向page中新增一个nil,以此来作为边界区分,这也就是所谓的哨兵对象,而push函数会返回这个哨兵对象的地址。

objc_autoreleasePoolPop
static inline void pop(void *token)
{
    AutoreleasePoolPage *page;
    id *stop;

    if (token == (void*)EMPTY_POOL_PLACEHOLDER) {
        if (hotPage()) {
            pop(coldPage()->begin());
        } else {
            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);

    if (DebugPoolAllocation  &&  page->empty()) {
        AutoreleasePoolPage *parent = page->parent;
        page->kill();
        setHotPage(parent);
    } else if (DebugMissingPools  &&  page->empty()  &&  !page->parent) {
        page->kill();
        setHotPage(nil);
    }
    else if (page->child) {
        if (page->lessThanHalfFull()) {
            page->child->kill();
        }
        else if (page->child->child) {
            page->child->child->kill();
        }
    }
}

先对通过token判断是否为空page,如果不是则通过token找到所在page,取出toekn存储的对象,如果取出的对象不是哨兵对象,判断这个page是否为表头,若是则表示当前对象已被释放,否则通过badPop函数抛异常。

通过page调用releaseUntil函数进行release,这是pop函数的精华所在:

void releaseUntil(id *stop)
{
    while (this->next != stop) {
        AutoreleasePoolPage *page = hotPage();

        while (page->empty()) {
            page = page->parent;
            setHotPage(page);
        }

        page->unprotect();
        id obj = *--page->next;
        memset((void*)page->next, SCRIBBLE, sizeof(*page->next));
        page->protect();

        if (obj != POOL_BOUNDARY) {
            objc_release(obj);
        }
    }

    setHotPage(this);

#if DEBUG
    // we expect any children to be completely empty
    for (AutoreleasePoolPage *page = child; page; page = page->child) {
        assert(page->empty());
    }
#endif
}

通过id obj = *--page->next取出autorelease对象,若不是哨兵对象,则调用objc_release函数进行release操作

最后来看一下kill函数:

void kill()
{
    AutoreleasePoolPage *page = this;
    while (page->child) page = page->child;

    AutoreleasePoolPage *deathptr;
    do {
        deathptr = page;
        page = page->parent;
        if (page) {
            page->unprotect();
            page->child = nil;
            page->protect();
        }
        delete deathptr;
    } while (deathptr != this);
}

他其实是kill调用page及之后的所有子page

所以pop后半部分kill的逻辑就是:

  1. 如果可debug且当前page为空,则kill掉当前page及之后的所有子page,并设置父page为当前page
  2. 如果是表头,则kill表头及之后所有的子page,并设置当前page为nil
  3. 如果当前page有子page,并且当且page使用率未过半,则kill当前page子page及之后所有的子page;如果当前page有子page,并且当前page使用率过半,则判断子page的子page,如果存在,则kill掉page->child->child及之后的所有子page
@autoreleasepool

简单测试一下:

int main(int argc, const char * argv[]) {
    @autoreleasepool {

    }
    return 0;
}

通过一下命令生成cpp文件:

sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer/

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m

可见还是调用objc_autoreleasePoolPush 与objc_autoreleasePoolPop这两个函数


Have fun!

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

推荐阅读更多精彩内容