浅谈 AutoreleasePool 的实现原理

面试题:

Autoreleasepool 里面的对象什么时候销毁。
这个问题经常被拿来做面试题,问很多人,很少能答对。很多答案都是“当前作用域大括号结束时释放”,显然没有正确理解 Autoreleasepool 的机制。

在没有手动加入 Autoreleasepool 的情况下,Autorelease 对象是在当前的 runloop 迭代结束时释放的,而它能够释放的原因是系统在每个 runloop 迭代中都加入了自动释放池 pushpop

对于,在手动加入 Autoreleasepool,会在大括号结束时释放。如果是嵌套的 Autoreleasepool 中,只有最里层的 Autoreleasepool 使对象的引用计数加1。反过来说就是最里层的 Autoreleasepool 会阻止外层的 Autoreleasepool 对对象的引用。

这样就可以解释为什么在方法里面,如果有 for 循环的话,应该对 for 循环加 Autoreleasepool了 ,因为这个 Autoreleasepool 阻止了 RunLoop 的一次迭代中加入的 Autoreleasepool 对对象的引用。这样在一次循环结束后,在循环中创建的变量就会被释放。

Autoreleasepool 销毁时,在调用堆栈中可以发现,系统调用了 -[NSAutoreleasePool release] 方法,这个方法最终通过调用 AutoreleasePoolPage::pop(void *) 函数来负责对 Autoreleasepool 中的 autorelease 对象执行 release 操作。

浅谈 AutoreleasePool 的实现原理_第1张图片
image

The Application Kit creates an autorelease pool on the main thread at the beginning of every cycle of the event loop, and drains it at the end, thereby releasing any autoreleased objects generated while processing an event

苹果官方文档:
在开始每一个事件循环之前系统会在主线程创建一个自动释放池, 并且在事件循环结束的时候把前面创建的释放池释放, 回收内存。

程序运行 -> 开启事件循环 -> 发生触摸事件 -> 创建自动释放池 -> 处理触摸事件 -> 事件对象加入自动释放池 -> 一次事件循环结束, 销毁自动释放池。

@autoreleasepool 使用时机

苹果官方文档

If you are writing a program that is not based on a UI framework, such as a command-line tool.
If you write a loop that creates many temporary objects.You may use an autorelease pool block inside the loop to dispose of those objects before the next iteration. Using an autorelease pool block in the loop helps to reduce the maximum memory footprint of the application.
If you spawn a secondary thread.You must create your own autorelease pool block as soon as the thread begins executing; otherwise, your application will leak objects.

即以下三种情况

  • 非UI程序
  • 循环中嵌套大量临时对象时
  • 自己创建了一个辅助线程时

autorelease 方法的实现

调用栈:

- [NSObject autorelease]
└── id objc_object::rootAutorelease()
    └── id objc_object::rootAutorelease2()
        └── static id AutoreleasePoolPage::autorelease(id obj)
            └── static id AutoreleasePoolPage::autoreleaseFast(id obj)
                ├── id *add(id obj)
                ├── static id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page)
                │   ├── AutoreleasePoolPage(AutoreleasePoolPage *newParent)
                │   └── id *add(id obj)
                └── static id *autoreleaseNoPage(id obj)
                    ├── AutoreleasePoolPage(AutoreleasePoolPage *newParent)
                    └── id *add(id obj)

autorelease 方法的调用栈中,最终都会调用上面提到的 autoreleaseFast 方法,将当前对象加到 AutoreleasePoolPage中。

AutoreleasePool 的实现原理

int main(int argc, char * argv[]) {
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

@autoreleasepool

使用 clang -rewrite-objc 命令将下面的 Objective-C 代码重写成 C++ 代码:

clang -rewrite-objc main.m

extern "C" __declspec(dllimport) void * objc_autoreleasePoolPush(void);
extern "C" __declspec(dllimport) void objc_autoreleasePoolPop(void *);
struct __AtAutoreleasePool {
  __AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
  ~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
  void * atautoreleasepoolobj;
};
#define __OFFSETOFIVAR__(TYPE, MEMBER) ((long long) &((TYPE *)0)->MEMBER)
int main(int argc, char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
    }
}

声明一个 __AtAutoreleasePool 类型的局部变量 __autoreleasepool 来实现 @autoreleasepool {}。当声明 __autoreleasepool 变量时,构造函数 __AtAutoreleasePool() 被调用,即执行:

atautoreleasepoolobj = objc_autoreleasePoolPush();

当出了当前作用域时,析构函数 ~__AtAutoreleasePool() 被调用,即执行:

objc_autoreleasePoolPop(atautoreleasepoolobj);

也就是说 @autoreleasepool {} 的实现代码可以进一步简化如下:

/* @autoreleasepool */ {
    void *atautoreleasepoolobj = objc_autoreleasePoolPush();
    // 用户代码,所有接收到 autorelease 消息的对象会被添加到这个 autoreleasepool 中
    objc_autoreleasePoolPop(atautoreleasepoolobj);
}

因此,单个 autoreleasepool 的运行过程可以简单地理解为 objc_autoreleasePoolPush()[obj release]objc_autoreleasePoolPop(void *) 三个过程。

AutoreleasePoolPage

image.png

从图中可以看出

  • AutoreleasePoolPage 是由双向链表来实现的,parentchild 就是用来构造双向链表的指针。
  • magic 用来校验 AutoreleasePoolPage 的结构是否完整;
  • AutoreleasePool 是按线程一一对应的,结构中的 thread 指针指向当前线程。
  • AutoreleasePoolPage 会为每个对象会开辟 4096 字节内存。
  • id *next 指向了下一个为空的内存地址(初始化为栈底),如果有添加进来的 autorelease 对象,移动到下一个为空的内存地址中。

如果 AutoreleasePoolPage 里面的 autorelease 对象满了,也就是 id *next 指针指向了栈顶,会新建一个 AutoreleasePoolPage 对象,连接链表,后来添加的 autorelease 对象在新的 AutoreleasePoolPage 加入,id *next 指针指向新的 AutoreleasePoolPage 为空的内存地址,即栈底。所以,向一个对象发送 release 消息,就是将这个对象加入到当前 AutoreleasePoolPageid *next 指针指向的位置。

POOL_SENTINEL(哨兵对象)

浅谈 AutoreleasePool 的实现原理_第2张图片
image.png

POOL_SENTINEL 只是 nil 的别名。

在每个自动释放池初始化调用 objc_autoreleasePoolPush 的时候,都会把一个 POOL_SENTINEL push 到自动释放池的栈顶,并且返回这个 POOL_SENTINEL 哨兵对象。

而当方法 objc_autoreleasePoolPop 调用时,就会向自动释放池中的对象发送 release 消息,直到第一个 POOL_SENTINEL

详细参考POOL_SENTINEL(哨兵对象)

objc_autoreleasePoolPush

objc_autoreleasePoolPush() 函数本质上就是调用的 AutoreleasePoolPagepush 函数。

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

根据源码得出,每次执行 objc_autoreleasePoolPush 其实就是创建了一个新的 autoreleasepool,然后会把一个 POOL_SENTINEL push 到自动释放池的栈顶,并且返回这个 POOL_SENTINEL 哨兵对象。

static inline void *push() {
    id *dest = autoreleaseFast(POOL_SENTINEL);
    assert(*dest == POOL_SENTINEL);
    return dest;
}

push 函数通过调用 autoreleaseFast 函数并传入哨兵对象 POOL_SENTINEL 来执行具体的插入操作。

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

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

id *autoreleaseNoPage(id obj) {
    // No pool in place.
    assert(!hotPage());
    
    if (obj != POOL_SENTINEL  &&  DebugMissingPools) {
        // We are pushing an object with no pool in place,
        // and no-pool debugging was requested by environment.
        _objc_inform("MISSING POOLS: Object %p of class %s "
                     "autoreleased with no pool in place - "
                     "just leaking - break on "
                     "objc_autoreleaseNoPool() to debug",
                     (void*)obj, object_getClassName(obj));
        objc_autoreleaseNoPool(obj);
        return nil;
    }
    
    // Install the first page.
    AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
    setHotPage(page);
    
    // Push an autorelease pool boundary if it wasn't already requested.
    if (obj != POOL_SENTINEL) {
        page->add(POOL_SENTINEL);
    }
    
    // Push the requested object.
    return page->add(obj);
}

autoreleaseFast 函数在执行一个具体的插入操作时,分别对三种情况进行了不同的处理:

  • 当前 hotPage 存在且没有满时,调用 page->add(obj) 方法将对象添加至 AutoreleasePoolPage 的栈中。
  • 当前 hotPage 存在且已满时,调用 autoreleaseFullPage 初始化一个新的 page,调用 page->add(obj) 方法将对象添加至 AutoreleasePoolPage 的栈中。
  • 当前 hotPage 不存在时,调用 autoreleaseNoPage 创建一个 hotPage,调用 page->add(obj) 方法将对象添加至 AutoreleasePoolPage 的栈中。

objc_autoreleasePoolPop

objc_autoreleasePoolPop(void *)函数本质上也是调用的AutoreleasePoolPage的pop函数。

void objc_autoreleasePoolPop(void *ctxt) {
    if (UseGC) return;
    // fixme rdar://9167170
    if (!ctxt) return;
    AutoreleasePoolPage::pop(ctxt);
}

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

    page->releaseUntil(stop);

    if (page->child) {
        if (page->lessThanHalfFull()) {
            page->child->kill();
        } else if (page->child->child) {
            page->child->child->kill();
        }
    }
}

pop 函数的入参就是 push 函数的返回值,也就是POOL_SENTINEL 的内存地址。根据这个内存地址找到所在的 AutoreleasePoolPage 然后使用 objc_release 释放 POOL_SENTINEL 指针之前的对象。

总结:
每调用一次 push 操作就会创建一个新的 autoreleasepool,然后往 AutoreleasePoolPage 中插入一个 POOL_SENTINEL,并且返回插入的 POOL_SENTINEL 的内存地址.
在执行 pop 操作的时候传入 POOL_SENTINEL,根据传入的哨兵对象地址找到哨兵对象所处的 page
在当前AutoreleasePoolPage中,然后使用 objc_release 释放 POOL_SENTINEL 指针之前的对象,并把 id next 指针到正确位置。

参考

自动释放池的前世今生 ---- 深入解析 Autoreleasepool

你可能感兴趣的:(浅谈 AutoreleasePool 的实现原理)