拾陆: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 
#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 方法。

你可能感兴趣的:(拾陆:AutoreleasePool实现原理)