自动释放池 ~ AutoreleasePool

2,573 阅读4分钟

概述

Autorelease机制是为了延时释放对象,即在超出释放池生命周期后,向其管理的对象实例发送release消息

原理

在每个自动释放池创建的时候,会在当前的AutoreleasePoolPage中设置一个标记位,在此期间,当有对象调用autorelease时,会把对象添加AutoreleasePoolPage

若当前页添加满了,会初始化一个AutoreleasePoolPage,然后用双向链表连接起来,并把新初始化的这一页设置为hotPage,当自动释放池pop时,从最下面依次往上pop,调用每个对象的release方法,直到遇到标志位

MRC下使用自动释放池

MRC环境下,通过NSAutoreleasePool对象来管理对象,对于所有调用过autorelease方法的对象,在废弃NSAutoreleasePool对象时,都将调用release实例方法

// 第一步:生成并持有释放池NSAutoreleasePool对象
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];

// 第二步:调用对象的autorelease实例方法
id obj = [[NSObject alloc] init];
[obj autorelease];

// 第三步:废弃NSAutoreleasePool对象,即向pool管理的所有对象发送消息,相当于[obj release]
[pool drain];
              

ARC下使用自动释放池

ARC环境下不能使用NSAutoreleasePool类,也不能调用autorelease方法,而是通过@autoreleasepool块和__autoreleasing修饰符

如下图所示,

1、@autoreleasepool替代NSAutoreleasePool类对象的生成、持有以及废弃这一过程

2、通过__autoreleasing修饰符的变量,替代autorelease方法,将对象注册到了AutoreleasePool

// ARC优化:可省略__autoreleasing
@autoreleasepool {
    id obj = [[NSObject alloc] init];
    NSLog(@"%s %@", __func__, obj);
 }
              

底层原理

通过clang编译器将以上代码编译成C++源码

{
    __AtAutoreleasePool __autoreleasepool; // 对应以上代码中的@autoreleasepool
    id obj = ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc")), sel_registerName("init"));
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_15_7m893_bx0_x7vt0_x2yk610w0000gp_T_main_a49e01_mi_0, __func__, obj);
} // 大括号对应释放池的作用域
              

结构体__AtAutoreleasePool的具体实现

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

struct __AtAutoreleasePool {
    // 构造函数,返回边界对象atautoreleasepoolobj
    // 在程序执行到声明这个对象的位置时调用
    __AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
    // 析构函数,传入边界对象atautoreleasepoolobj
    // 在程序执行到离开这个对象的作用域时调用
    ~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
  void * atautoreleasepoolobj;
};

// 简化以上代码如下
int main {
    void *atautoreleasepoolobj = objc_autoreleasePoolPush();
    ...
    objc_autoreleasePoolPop(atautoreleasepoolobj);    
}
              

objc_autoreleasePoolPush与objc_autoreleasePoolPop

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

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

AutoreleasePoolPage

每个AutoreleasePoolPage对象占用4M内存,用于存放自身内部的成员变量,剩下的空间用来存放autorelease对象的地址

每个自动释放池都是由若干个AutoreleasePoolPage组成的双向链表结构,如下图

哨兵对象即边界对象(POOL_BOUNDARY)

#define POOL_BOUNDARY nil
  • 每当自动释放池初始化调用objc_autoreleasePoolPush方法时,总会通过AutoreleasePoolPagepush方法,将POOL_BOUNDARY放到当前page的栈顶,并且返回这个边界对象
  • 每当自动释放池释放调用objc_autoreleasePoolPop方法时,又会将边界对象以参数传入,这样自动释放池就会向释放池中对象发送release方法,直至找到第一个边界对象为止

Runloop与AutoreleasePool的关系

主线程的NSRunloop在监测到事件响应开启每一次事件循环之前,会自动创建一个autoreleasepool,并且会在事件循环结束的时候执行drain操作,释放其中的对象

AutoreleasePool在主线程上的释放时机

  • App 启动后,苹果在主线程 RunLoop 里注册了两个Observer,其回调都是_wrapRunLoopWithAutoreleasePoolHandler()
  • 第一个Observer监视的事件是Entry(即将进入Loop),其回调内会调用 _objc_autoreleasePoolPush()创建自动释放池。其order是 -2147483647,表示其优先级最高,保证创建释放池发生在其他所有回调之前
  • 第二个Observer监视了两个事件:

1、BeforeWaiting(准备进入休眠) 时调用_objc_autoreleasePoolPop()_objc_autoreleasePoolPush() 释放旧的池并创建新池

2、Exit(即将退出Loop)时调用 _objc_autoreleasePoolPop() 来释放自动释放池,其 order是 2147483647,优先级最低,保证其释放池子发生在其他所有回调之后

  • 在主线程执行的代码,通常是写在诸如事件回调、Timer回调内的。这些回调会被 RunLoop 创建好的AutoreleasePool环绕着,所以不会出现内存泄漏,开发者也不必显示创建 Pool

AutoreleasePool子线程上的释放时机

每一个线程都会维护自己的AutoreleasePool栈,所以子线程虽然默认没有开启Runloop,但是依然存在AutoreleasePool,在子线程退出的时候会去释放autorelease对象

从源码分析的角度来看,如果子线程中没有创建AutoReleasePool,而一旦产生了 autorelease对象,就会调用autoreleaseNoPage方法自动创建hotPage,并将对象加入到其栈中。所以,一般情况下,子线程即时我们不手动添加自动释放池,也不会产生内存泄漏

AutoreleasePool需要手动添加的情况

  1. 编写的不是基于UI框架的程序,例如命令行工具

  2. 通过循环方式创建大量临时对象

  3. 使用非Cocoa程序创建的子线程

    // 创建大量临时对象 // 以手动干预的方式及时释放不需要的对象,减少内存消耗 for (int i = 0; i < 10000000; i++) { @autoreleasepool { NSObject *obj = [[NSObject alloc] init]; NSLog(@"打印obj %@", obj); } }