分析自动释放池aureleasePool的原理

一、简介

aureleasePool,顾名思义,自动释放池。它在iOS系统的内存管理中,有着极其重要的作用。放入其池中的对象,最终系统通过它进行释放,不用程序员手动去管理。在MRC中,当然还是需要手动调用一个autorelase方法将对象添加进自动释放池,在ARC中,这一步直接省略,只需要在 @autoreleasepool { ... }中创建对象,然后执行代码即可。最后,在对象需要释放的时候,自动释放池自动帮助完成这一行为。

 

二、概念

自动释放池到底是什么?其实,在对源码进行分析的过程中,发现它是由双向链表构成的,通过栈来管理对象。一句话总结就是,它是以栈为节点通过双向链表实现的一种存储结构。

 

三、示例

说了这么多,那么放入自动释放池的对象,到底是不是由它来管理内存的呢?现在来验证一下,看看到底是不是由它来释放的,示例代码如下:main.m

#import <Foundation/Foundation.h>

__weak NSObject *weak_obj1; 
__weak NSObject *weak_obj2;

int main(int argc, const char * argv[]) {
    
    NSObject *obj1 = [[NSObject alloc] init];
    weak_obj1 = obj1;
    NSLog(@"before---weak_obj1--%p",weak_obj1);
    
    @autoreleasepool {
        NSObject *obj2 = [[NSObject alloc] init];
        weak_obj2 = obj2;
        NSLog(@"before---weak_obj2--%p",weak_obj2);
    }
    
    NSLog(@"after---weak_obj1--%p",weak_obj1);
    NSLog(@"after---weak_obj2--%p",weak_obj2);
    
    return 0;
}

创建了两个weak指针分别指向对象obj1和ob2,obj1对象在自动释放池之外创建的,obj2对象则是在自动释放池之内创建的。按照上面简介的说明,猜想打印的结果一定是:

  • obj1 在 before 和 after 这里,指针的值一直存在,除非到程序执行结束,也即main函数执行完毕,它才被系统释放回收。
  • obj2 在 before 这里,指针的值是存在的,但是出了自动释放池的作用域外面后,在after 这里, 指针的值一定为空,表明它由自动释放池进行释放回收了。

打印结果,果然印证了这个猜想,如下所示:

2021-04-16 17:11:04.250446+0800 原理剖析[82323:4507357] before---weak_obj1--0x1031adbe0
2021-04-16 17:11:04.250835+0800 原理剖析[82323:4507357] before---weak_obj2--0x103304830
2021-04-16 17:11:04.250887+0800 原理剖析[82323:4507357] after---weak_obj1--0x1031adbe0
2021-04-16 17:11:04.250917+0800 原理剖析[82323:4507357] after---weak_obj2--0x0
Program ended with exit code: 0

 

四、结构

结果是喜人的,接着,我们就需要去一探究竟了。它的结构是什么的?如何实现的? 为了更好更直观的分析结构,我们需要做一些前期工作,就是把OC代码转成C++代码。xcode自带了clang编译器,可以借助它进行语法的转换。转换命令行如下:

/*
xcrun: xcode run运行。

-sdk:表示转成什么平台下的C++代码。例如 iphoneos,代表手机系统,可以是模拟器,也可以是真机。如果不写,会兼容所有的平台,macos、iphoneos、windowos。

clang:编译器

-arch:表示在选择的此平台下,采用什么样的架构。arm64是真机64bit的,i386是模拟器32bit的。如果不写,会兼容该平台下的所有架构。

-fobjc-arc:表示内存的管理方式,可以不用写,默认ARC。

-fobjc-runtime=ios-8.0.0:weak指针修饰的变量,需要低系统环境支持,此处指定iOS8.0。

-o:output 输出文件
*/


// weak 需要运行时环境的支持。当你选择了一个合适的最低版本后,才能编译有 weak 修饰变量的源码。

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 main.m -o mian_arm64.cpp 

xcrun clang -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 main.m -o mian.cpp  //【忽略:64 warnings generated.】


// 非weak 修饰变量的源码。

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

xcrun clang -rewrite-objc  main.m -o mian.cpp  //【忽略:64 warnings generated.】

可以看到,不同的配置选择,生成c++代码文件大小是不一样的,如下所示:

分析自动释放池aureleasePool的原理

选择main_arm64.cpp文件点击查看,代码如下,可以发现,在自动释放池那里,其实是生成了一个代码块,内部创建了一个自动释放池对象_autorelasepool,它的类型是__AtAutoreleasePool。

#pragma clang assume_nonnull end

__attribute__((objc_ownership(weak))) NSObject *weak_obj1;
__attribute__((objc_ownership(weak))) NSObject *weak_obj2;

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

    NSObject *obj1 = ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc")), sel_registerName("init"));
    weak_obj1 = obj1;
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_62_ptthn0jn271412kr3p6_0d3h0000gp_T_main_d51538_mi_0,weak_obj1);

    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        NSObject *obj2 = ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc")), sel_registerName("init"));
        weak_obj2 = obj2;
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_62_ptthn0jn271412kr3p6_0d3h0000gp_T_main_d51538_mi_1,weak_obj2);
    }

    NSLog((NSString *)&__NSConstantStringImpl__var_folders_62_ptthn0jn271412kr3p6_0d3h0000gp_T_main_d51538_mi_2,weak_obj1);
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_62_ptthn0jn271412kr3p6_0d3h0000gp_T_main_d51538_mi_3,weak_obj2);

    return 0;
}
static struct IMAGE_INFO { unsigned version; unsigned flag; } _OBJC_IMAGE_INFO = { 0, 2 };

全局搜索这个__AtAutoreleasePool,发现它就是一个struct结构体,内部有一个构造函数、一个析构函数。创建时调用objc_autoreleasePoolPush函数,销毁时调用objc_autoreleasePoolPop函数。

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

了解了这个结构后,我们来稍微改造一下当前的这个main_arm64.cpp函数,更加清晰直观,如下所示:

#pragma clang assume_nonnull end

__attribute__((objc_ownership(weak))) NSObject *weak_obj1;
__attribute__((objc_ownership(weak))) NSObject *weak_obj2;

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

    NSObject *obj1 = ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc")), sel_registerName("init"));
    weak_obj1 = obj1;
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_62_ptthn0jn271412kr3p6_0d3h0000gp_T_main_d51538_mi_0,weak_obj1);

    /* @autoreleasepool */  // { __AtAutoreleasePool __autoreleasepool;
        
        void * atautoreleasepoolobj = objc_autoreleasePoolPush(); 
        
        NSObject *obj2 = ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc")), sel_registerName("init"));
        weak_obj2 = obj2;
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_62_ptthn0jn271412kr3p6_0d3h0000gp_T_main_d51538_mi_1,weak_obj2);
        
        objc_autoreleasePoolPop(atautoreleasepoolobj)
    //}

    NSLog((NSString *)&__NSConstantStringImpl__var_folders_62_ptthn0jn271412kr3p6_0d3h0000gp_T_main_d51538_mi_2,weak_obj1);
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_62_ptthn0jn271412kr3p6_0d3h0000gp_T_main_d51538_mi_3,weak_obj2);

    return 0;
}
static struct IMAGE_INFO { unsigned version; unsigned flag; } _OBJC_IMAGE_INFO = { 0, 2 };

 

五、源码

现在需要去runtime源码中,看看 objc_autoreleasePoolPush函数 和 objc_autoreleasePoolPop函数的实现了,核心代码如下:

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

//pop出栈
NEVER_INLINE void objc_autoreleasePoolPop(void *ctxt)
{
    AutoreleasePoolPage::pop(ctxt);
}

从上面的方法实现,可以得出结论,自动释放池的实现其实是依赖类AutoreleasePoolPage的。AutoreleasePoolPage继承自结构体AutoreleasePoolPageData,也即

class AutoreleasePoolPage : private AutoreleasePoolPageData { 

  static size_t const SIZE = PAGE_MIN_SIZE; //每一个page最大容量是4096字节
  .........

}

所以,接着AutoreleasePoolPageData的最终内部构成,可以看到,有父亲parent page,也有孩子child page,还有深度depth,说明它是个双向链表。里面还有一个next指针,可自增自减,进行存放位置的读取,是一个指针栈,如下所示:

struct AutoreleasePoolPageData
{
    magic_t const magic; //用来校验AutoreleasePoolPage的结构是否完整

    __unsafe_unretained id *next; //指向下一个即将产生的autoreleased对象的存放位置(当next == begin()时,表示AutoreleasePoolPage为空;当next == end()时,表示AutoreleasePoolPage已满。

    pthread_t const thread; //指向当前线程,一个AutoreleasePoolPage只会对应一个线程,但一个线程可以对应多个AutoreleasePoolPage;

    AutoreleasePoolPage * const parent;//指向父结点,第一个结点的 parent 值为 nil;

    AutoreleasePoolPage *child;//指向子结点,最后一个结点的 child 值为 nil

    uint32_t const depth; //代表深度,第一个page的depth为0,往后每递增一个page,depth会加1;

    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)
    {
    }
};    

对AutoreleasePoolPage有了基本认识后,现在深入分析一下它的push函数。继续,分析源代码如下。如果是debug模式下,每个自动释放池从一个新的池页开始;否则,执行快速释放池逻辑。其中,都传入了一个参数POOL_BOUNDARY,翻译为池边界,在这里则表示为哨兵对象,用来作为page存储对象的起始边界条件。

//哨兵入栈
static inline void *push() { id *dest; if (slowpath(DebugPoolAllocation)) { // Each autorelease pool starts on a new pool page. dest = autoreleaseNewPage(POOL_BOUNDARY); } else { dest = autoreleaseFast(POOL_BOUNDARY); } ASSERT(dest == EMPTY_POOL_PLACEHOLDER || *dest == POOL_BOUNDARY); return dest; }

单个autoreleasepool就只有一个哨兵对象,多个嵌套autoreleasepool,就有多个哨兵对象。借用另一位博主的图例如下,  abc为图一,单个释放池。d为图二,嵌套释放池。

分析自动释放池aureleasePool的原理分析自动释放池aureleasePool的原理

           (a)                                         (b)                                         (c)                                           (d)

在自动释放池创建的对象,系统会自动给每一个对象发送autorelease消息,这样它们就可以被添加到自动释放池。存入自动释放池的对象地址在哨兵对象后面依次递增,最终释放对象的时候,系统会给每一个对象再次发送release消息进行释放,直到遇到哨兵对象后停止,表明对象全部释放完全。

// 对象进自动释放池
static inline id autorelease(id obj)
{
     ASSERT(obj);
     ASSERT(!obj->isTaggedPointer());
     id *dest __unused = autoreleaseFast(obj);
     ASSERT(!dest  ||  dest == EMPTY_POOL_PLACEHOLDER  ||  *dest == obj);
     return obj;
}

autoreleaseNewPage函数 和 autoreleaseFast函数,它们的实现存在较多相似地方,我们直接去看对应的实现,代码分析如下。

  • 首先获取当前的page栈节点,如果存在且未填满,则直接将对对象添加到此page栈中;
  • 如果存在但是已经填满,则去寻找它的子page栈,当子page栈不存在的时候,就新建一个子page栈并设置为当前page栈,接着将对对象添加到此page栈中;
  • 如果不存在,则懒加载新建一个子page栈并设置为当前page栈,这里会通过haveEmptyPoolPlaceholder函数判断一下是否存在嵌套的自动释放池,然后决定添加新的哨兵对象,最后接着将对象添加到此page栈中。
// 快速处理
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); } }
// 满页处理 static __attribute__((noinline)) 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. ASSERT(page == hotPage()); ASSERT(page->full() || DebugPoolAllocation); do { if (page->child) page = page->child; else page = new AutoreleasePoolPage(page); } while (page->full()); setHotPage(page); return page->add(obj); }
// 懒加载处理 static __attribute__((noinline)) 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()); bool pushExtraBoundary = false; if (haveEmptyPoolPlaceholder()) { // We are pushing a second pool over the empty placeholder pool // or pushing the first object into the empty placeholder pool. // Before doing that, push a pool boundary on behalf of the pool // that is currently represented by the empty placeholder. pushExtraBoundary = true; } else if (obj != POOL_BOUNDARY && DebugMissingPools) { // We are pushing an object with no pool in place, // and no-pool debugging was requested by environment. _objc_inform("MISSING POOLS: (%p) Object %p of class %s " "autoreleased with no pool in place - " "just leaking - break on " "objc_autoreleaseNoPool() to debug", objc_thread_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(); } // We are pushing an object or a non-placeholder'd pool. // 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); } // 新创建栈页 static __attribute__((noinline)) id *autoreleaseNewPage(id obj) { AutoreleasePoolPage *page = hotPage(); if (page) return autoreleaseFullPage(obj, page); else return autoreleaseNoPage(obj); }

不难发现,将对象入栈的最终调用的都是add函数,代码比较简单,就是栈指针移动,将对象保存到指定栈中位置,如下所示:

id *add(id obj)
{
     ASSERT(!full());
     unprotect();
     id *ret = next;  // faster than `return next-1` because of aliasing
      *next++ = obj;
      protect();
      return ret;
}

同样地,将对象出栈进行释放,调用的是releaseUntil函数。从后往前遍历父page栈,直到位置为stop时才结束遍历。遍历的过程中,每从栈中取出一个obj对象,next指针递减,同时,只要该obj对象不是哨兵对象,就发送objc_release消息,进行释放。

void releaseUntil(id *stop) 
{
      // Not recursive: we don't want to blow out the stack 
      // if a thread accumulates a stupendous amount of garbage        
      while (this->next != stop) {
          // Restart from hotPage() every time, in case -release 
          // autoreleased more objects
          AutoreleasePoolPage *page = hotPage();

          // fixme I think this `while` can be `if`, but I can't prove it
          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);
          }
} }

objc_release调用到runtime中,如下所示,最终结束流程。

__attribute__((aligned(16), flatten, noinline))
void 
objc_release(id obj)
{
    if (!obj) return;
    if (obj->isTaggedPointer()) return;
    return obj->release();
}
inline void
objc_object::release()
{
    ASSERT(!isTaggedPointer());

    if (fastpath(!ISA()->hasCustomRR())) {
        rootRelease();
        return;
    }

    ((void(*)(objc_object *, SEL))objc_msgSend)(this, @selector(release));
}

还有最后一点,push是将哨兵入栈,那哨兵对象出栈怎么实现的呢。其实,对应的就是pop函数。先调用releaseUntil函数将所有的非哨兵对象出栈,然后再将哨兵对象出栈。最终所有page均指向nil。代码如下:

//pop出栈
static inline void
pop(void *token)
{
    AutoreleasePoolPage *page;
    id *stop;
    if (token == (void*)EMPTY_POOL_PLACEHOLDER) {
        // Popping the top-level placeholder pool.
        page = hotPage();
        if (!page) {
            // Pool was never used. Clear the placeholder.
            return setHotPage(nil);
        }
        // Pool was used. Pop its contents normally.
        // Pool pages remain allocated for re-use as usual.
        page = coldPage();
        token = page->begin();
    } else {
        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 (slowpath(PrintPoolHiwat || DebugPoolAllocation || DebugMissingPools)) {
        return popPageDebug(token, page, stop);
    }

    return popPage<false>(token, page, stop);
}

//先处理非哨兵对象,再处理哨兵对象
template<bool allowDebug>
static void
popPage(void *token, AutoreleasePoolPage *page, id *stop)
{
    if (allowDebug && PrintPoolHiwat) printHiwat();

    page->releaseUntil(stop);

    // memory: delete empty children
    if (allowDebug && DebugPoolAllocation  &&  page->empty()) {
        // special case: delete everything during page-per-pool debugging
        AutoreleasePoolPage *parent = page->parent;
        page->kill();
        setHotPage(parent);
    } else if (allowDebug && 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();
        }
    }
}


//栈清空
void kill() 
{
    // Not recursive: we don't want to blow out the stack 
    // if a thread accumulates a stupendous amount of garbage
    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);
}

 完结。

 

 

 

 

 

上一篇:iOS Block循环引用的理解


下一篇:回顾2020 iOS面试