老青菜

iOS autoreleasepool 底层分析

2019-11-16

autoreleasepool,自动释放池,项目中我们一般这样使用:

- (void)test {
    @autoreleasepool {
        NSString *str1 = [NSString stringWithFormat:@"你好"];
    }

autoreleasepool

我们先用clang -rewrite-objc ./OCTest.m编译一下,看看能发现什么。编译后,打开OCTest.cpp。找到以下部分关键代码。

//OCTest.cpp
static void _I_OCTest_test(OCTest * self, SEL _cmd) {
    /* @autoreleasepool */ {
        //声明__AtAutoreleasePool 结构体变量
        __AtAutoreleasePool __autoreleasepool; 
        //省略...
    }
}

extern "C" __declspec(dllimport) void * objc_autoreleasePoolPush(void);
extern "C" __declspec(dllimport) void objc_autoreleasePoolPop(void *);

struct __AtAutoreleasePool {
  //pool构造函数,调用objc_autoreleasePoolPush
  __AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}

  //pool析构函数,调用objc_autoreleasePoolPop
  ~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
  void * atautoreleasepoolobj;
};

不难发现,@autoreleasepool{}这种写法,巧妙利用了{}作用域和结构体的构造函数、析构函数。

上面代码其实等价于以下代码:

{
    void *atautoreleasepoolobj = objc_autoreleasePoolPush();
    //...
    objc_autoreleasePoolPop(atautoreleasepoolobj);
}

那么objc_autoreleasePoolPushobjc_autoreleasePoolPop又做了什么呢?打开objc源码下的NSObject.mm文件,找到以下代码:

//NSObject.mm
void * objc_autoreleasePoolPush(void) {
    if (UseGC) return nil;
    return AutoreleasePoolPage::push();
}
void objc_autoreleasePoolPop(void *ctxt) {
    if (UseGC) return;
    AutoreleasePoolPage::pop(ctxt);
}

AutoreleasePoolPage

AutoreleasePoolPage,自动释放池页,是一个C++实现的类。objc中并没有AutoreleasePool,而是由多个AutoreleasePoolPage以双链表的形式串起来的结构。

class AutoreleasePoolPage  {
#define POOL_SENTINEL nil
    static pthread_key_t const key = AUTORELEASE_POOL_KEY;
    static uint8_t const SCRIBBLE = 0xA3;  // 0xA3A3A3A3 after releasing
    //4096字节,固定大小。
    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;
    //high water mark,最高水位标记
    uint32_t hiwat;

    static void * operator new(size_t size) {
        //申请4096字节固定大小的空间
        return malloc_zone_memalign(malloc_default_zone(), SIZE, SIZE);
    }
}

结构图如下:

接下来我们来看看pushpop的实现。

push

push操作,核心功能是在AutoreleasePoolPage的栈里压入了一个POOL_SENTINEL边界对象,把next指针指向下一个位置。并返回POOL_SENTINEL边界对象指针。我们来看源码:

/// autoreleasepool page入栈
static inline void *push() {
    id *dest;
    if (DebugPoolAllocation) {
        //debug模式每次新建一个page,POOL_SENTINEL=nil
        // Each autorelease pool starts on a new pool page.
        dest = autoreleaseNewPage(POOL_SENTINEL);
    } else {
        //调用autoreleaseFast,快速添加autorelease,这里的POOL_SENTINEL=nil
        dest = autoreleaseFast(POOL_SENTINEL);
    }
    assert(*dest == POOL_SENTINEL);
    return dest;
}
//快速添加 autorelease
static inline id *autoreleaseFast(id obj) {
    //hotpage可以理解为当前在使用的 AutoreleasePoolPage
    AutoreleasePoolPage *page = hotPage();
    if (page && !page->full()) {
        //hotpage不为空,且未满,添加到hotpage里
        return page->add(obj);
    } else if (page) {
        //hotpage不为空,但是满了,
        //以hotpage为parent,新建child page,设置为hotpage,添加到hotpage里。
        return autoreleaseFullPage(obj, page);
    } else {
        //没有hotpage,新建page,设置为hotpage,添加到hotpage里。
        return autoreleaseNoPage(obj);
    }
}
/// 获取hotPage
static inline AutoreleasePoolPage *hotPage()  {
    //通过key去tls线程缓存中查找,key在AutoreleasePoolPage中定义了。
    AutoreleasePoolPage *result = (AutoreleasePoolPage *)
        tls_get_direct(key);
    if (result) result->fastcheck();
    return result;
}
/// 添加obj到page里
id *add(id obj) {
    assert(!full());
    unprotect();
    id *ret = next;  // faster than `return next-1` because of aliasing
    *next++ = obj;
    protect();
    return ret;
}
/// 有hotPage,且已满的情况,标记obj autorelease 
id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page)
{
    // The hot page is full. 
    // Step to the next non-full page, adding a new page if necessary.
    // Then add the object to that page.
    //hotPage已满,转到下一个非完整页,如有必要,添加新页。
    //然后将obj添加到该页。
    do {
        if (page->child) page = page->child;
        else page = new AutoreleasePoolPage(page);
    } while (page->full());
    setHotPage(page);
    return page->add(obj);
}
/// 没有page的情况,标记obj autorelease 
id *autoreleaseNoPage(id obj) {
    // No pool in place.
    assert(!hotPage());
    // Install the first page.
    // 新建page,作为第一页,设置为hotPage。
    AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
    setHotPage(page);
    // Push an autorelease pool boundary if it wasn't already requested.
    // 添加POOL_SENTINEL边界到page
    if (obj != POOL_SENTINEL) {
        page->add(POOL_SENTINEL);
    }
    // Push the requested object.
    // 然后将obj添加到page
    return page->add(obj);
}

总结一下,push操作内部区分了三种情况:

  1. hotPage,且未满,则调用page->add,直接添加objhotPage
  2. hotPage,且已满,则调用autoreleaseFullPage,以hotpageparent,新建child page,设置为hotpage,并把obj添加到hotpage里。
  3. 没有hotPage,则调用autoreleaseNoPage,新建page,设置为hotpage,并把POOL_SENTINEL边界和obj对象添加到hotpage里。

虽然区分了三种情况,其实核心还是拿到HotPage,在HotPage上压入POOL_SENTINEL边界对象,也就是标记初始位置,为后续pop做准备。如下如,push就对应左边第1步。

pop

pop操作,核心是根据push返回的POOL_SENTINEL边界指针,找到所属page,然后释放page栈里的对象,直到边界位置。我们来看源码:

static inline void pop(void *token)  {
    AutoreleasePoolPage *page;
    id *stop;
    //获取 token 所在的 page
    page = pageForPointer(token);
    stop = (id *)token;
    //释放page里的对象,直到 stop 为止。
    page->releaseUntil(stop);
    //省略部分代码...
    //调用page的child->kill()
    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();
        }
    }
}
/// 释放page里的对象,直到 stop 为止
void releaseUntil(id *stop) {
    //遍历page的next,直到 next = stop 为止。
    while (this->next != stop) {
        AutoreleasePoolPage *page = hotPage();
        while (page->empty()) {
            page = page->parent;
            setHotPage(page);
        }
        page->unprotect();
        id obj = *--page->next;
        //清除 page->next 内存
        memset((void*)page->next, SCRIBBLE, sizeof(*page->next));
        page->protect();
        //obj release
        if (obj != POOL_SENTINEL) {
            objc_release(obj);
        }
    }
    setHotPage(this);
}
/// 删除 page ,会删除 page 和所有 child page。
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);
}

总结一下,pop操作流程如下:

  1. 根据 push 操作返回的边界指针获取所在的 page,并调用 releaseUntil 释放 page 里的对象,清除内存。
  2. 调用 page->childkill 方法,删除 page 和所有子 page

autorelease

autoreleaseautoreleasepool又有什么关系呢?还是来看源码。

//NSObject.mm
- (id)autorelease {
    return ((id)self)->rootAutorelease();
}

//objc-object.h
// Base autorelease implementation, ignoring overrides.
inline id objc_object::rootAutorelease() {
    if (isTaggedPointer()) return (id)this;
    if (prepareOptimizedReturn(ReturnAtPlus1)) return (id)this;
    return rootAutorelease2();
}

//NSObject.mm
id objc_object::rootAutorelease2() {
    //调用 AutoreleasePoolPage 的 autorelease
    return AutoreleasePoolPage::autorelease((id)this);
}

//class AutoreleasePoolPage
static inline id autorelease(id obj) {
    id *dest __unused = autoreleaseFast(obj);
    assert(!dest  ||  *dest == obj);
    return obj;
}

通过一层层的调用,autorelease最终还是调用了autoreleaseFast,把obj压到hot page的栈里。

那么autorelease对象什么时候释放?也就是系统隐示加入的_objc_autoreleasePoolPop什么时候调用呢?

查阅了很多资料,都是这样的说法(runloop源码里没找到相关逻辑代码):
1.runloop准备进入休眠BeforeWaiting,调用_objc_autoreleasePoolPop()_objc_autoreleasePoolPush()释放旧的池并创建新池。
2.runloop退出的时候Exit,调用_objc_autoreleasePoolPop()释放自动释放池。

测试一下

AutoreleasePoolPage底层提供了打印当前pool的方法。

// NSObject.mm
void  _objc_autoreleasePoolPrint(void) {
    if (UseGC) return;
    AutoreleasePoolPage::printAll();
}

我们做以下测试,嵌套三层autoreleasepool,看下pool结构。

// 导入 _objc_autoreleasePoolPrint
extern void _objc_autoreleasePoolPrint(void);

@autoreleasepool {
    NSString *str1 = [NSString stringWithFormat:@"你"];
    @autoreleasepool {
        NSString *str2 = [NSString stringWithFormat:@"好"];
        @autoreleasepool {
            NSString *str3 = [NSString stringWithFormat:@"啊"];
            // 嵌套3层,打印pool
            _objc_autoreleasePoolPrint();
        }
        // 销毁第3层,打印pool
        _objc_autoreleasePoolPrint();
    }
}

我们分别打印了嵌套3层pool销毁第3层pool的情况。日志如下图:

不难发现,嵌套pool只是压入了一个边界对象(POOL_SENTINEL);销毁pool,也就是把边界对象(POOL_SENTINEL)之前的对象出栈而已。

总结

AutoreleasePool核心结构是多个AutoreleasePoolPage双链表构成的。核心流程如下:

  1. 初始化 page ,调用pushPOOL_SENTINEL边界对象压入到page栈里,标记初始位置。

  2. 标记对象,调用[obj autorelease],把对象指针压入到page栈里。

  3. 销毁 page ,调用pop释放page栈里的对象,直到上次push的边界位置为止。

  4. 可以多层嵌套,多层的AutoreleasePool就是多个POOL_SENTINEL边界对象而已,每次pop都会到上次push的边界位置,不会影响其他其他AutoreleasePool

参考链接

opensource objc4

Tags: objc
使用支付宝打赏
使用微信打赏

若你觉得我的文章对你有帮助,欢迎点击上方按钮对我打赏

扫描二维码,分享此文章