AutoreleasePool(自动释放池)是OC中的一种内存自动回收机制,它可以延迟加入AutoreleasePool中的变量release的时机。在正常情况下,创建的变量会在超出其作用域的时候release,但是如果将变量加入AutoreleasePool,那么release将延迟执行。
需要了解AutoreleasePool的工作原理,我们需要知道它的底层到底做了什么事情,那我们就先从汇编代码入手,新建一个命令行工程,创建一个新的对象继承自NSObject:
1 | #import <Foundation/Foundation.h> |
我们利用命令将OC代码重写为c++代码:
1 | xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m |
我们可以大约得到3万2千行的c++代码的cpp文件,但是不要紧,因为最终的核心代码在该cpp的最底部:
1 | int main(int argc, const char * argv[]) { |
中间的代码层是object对象的创建过程,发送objc_msgSend消息创建对象。那其实最核心的代码就在下面这这两句上了
__AtAutoreleasePool __autoreleasepool;
__AtAutoreleasePool
我们在cpp文件中搜索__AtAutoreleasePool会找到如下代码,__AtAutoreleasePool具体定义如下:
1 | extern "C" __declspec(dllimport) void * objc_autoreleasePoolPush(void); |
上面两个调用,分别是构造函数和析构函数,根据构造函数和析构函数的特点:自动局部变量的构造函数是在程序执行到声明这个对象的位置时调用的,而对应的析构函数是在程序执行到离开这个对象的作用域时调用。苹果实际上是通过声明一个__AtAutoreleasePool类型的局部变量__autoreleasepool实现了@autoreleasepool{},那么实际上单个自动释放池的执行过程就是:
1 | objc_autoreleasePoolPush() —> [object autorelease] —> objc_autoreleasePoolPop(void *) |
想了解objc_autoreleasePoolPush和objc_autoreleasePoolPop具体都做了些什么,其实很简单,我们只要到runtime->NSObject.mm的源码中就能窥探它的真是面目了,这里我们分析的runtime源码是objc-750的版本。
在源码中我们可以发现这样一段代码:
1 | void *objc_autoreleasePoolPush(void) { |
objc_autoreleasePoolPush和objc_autoreleasePoolPop分别是由AutoreleasePoolPage调用了push方法入栈和pop方法出栈,其本质实际上是AutoreleasePoolPage对应的静态方法push和pop的封装。那么问题就显而易见了,如果要知道这个push和pop方法到底做了什么,我们还得从源码里获取到AutoreleasePoolPage相关的内容以及其实现原理。
AutoreleasePoolPage定义
在runtime源码中对AutoreleasePoolPage的定义是这样的:
1 | class AutoreleasePoolPage { |
去除那些静态成员变量,AutoreleasePoolPage的成员变量的解释如下:
1 | class AutoreleasePoolPage { |
这里需要注意的是AutoreleasePoolPage有一个成员变量是PAGE_MAX_SIZE,这个表示一个AutoreleasePoolPage最大内存大小,这个宏其实在上面可以找得到,也就是说一个AutoreleasePoolPage的最大内存大小是PAGE_MAX_SIZE(也就是4096):
1 | #define I386_PGBYTES 4096 /* bytes per 80386 page */ |
AutoreleasePoolPage工作原理
每个AutoreleasePoolPage对象的内存大小事4096字节,除去AutoreleasePoolPage的成员变量所占用的空间,剩下的空间用来存放Autorelease对象的地址,知道了AutoreleasePoolPage的定义,现在我们回到objc_autoreleasePoolPush这个方法,我们发现了,实际上这个方法是调用了AutoreleasePoolPage的push方法:
1 | static inline void *push() { |
细心的你肯定会发现,在调用push方法的时候autoreleaseFast会将一个POOL_BOUNDARY的对象放在临界点上。POOL_BOUNDARY这个对象属于比较关键的对象,关系到AutoreleasePoolPage的释放过程。
1 | static inline id *autoreleaseFast(id obj) { |
上述方法分三种情况选择不同的代码执行:
1、有 hotPage 并且当前 page 不满,调用 page->add(obj) 方法将对象添加至 AutoreleasePoolPage 的栈中
2、有 hotPage 并且当前 page 已满,调用 autoreleaseFullPage 初始化一个新的页,调用 page->add(obj) 方法将对象添加至 AutoreleasePoolPage 的栈中
3、无 hotPage,调用 autoreleaseNoPage 创建一个 hotPage,调用 page->add(obj) 方法将对象添加至 AutoreleasePoolPage 的栈中
最后的都会调用 page->add(obj) 将对象添加到自动释放池中。而hotPage 可以理解为当前正在使用的 AutoreleasePoolPage。
接下来我们看一看objc_autoreleasePoolPop方法调用pop的实现:
1 | static inline void pop(void *token) { |
顺着源码一步一步找就会发现,autorelease函数和push函数一样,关键代码都是调用autoreleaseFast函数向自动释放池的链表栈中添加一个对象,不过push函数的入栈的是一个边界对象,而autorelease函数入栈的是需要加入autoreleasepool的对象。自动释放池释放是传入 push 返回的边界对象(POOL_BOUNDARY),autoreleasepool在调用autorelease时逐渐kill存在在autoreleasepool中的对象的地址,直到找到POOL_BOUNDARY对象所在的地址才会停止。
那么这就衍生了一个问题,如果AutoreleasePoolPage在添加需要释放的对象的地址超过了4096的空间或者是说有多个AutoreleasePoolPage的时候它是如何存入需要释放对象的地址,又是如何一层一层的释放的呢?
AutoreleasePoolPage双向链表
其实AutoreleasePoolPage并没有单独的结构,而是由若干个AutoreleasePoolPage以双向链表的形式组合而成的栈结构在AutoreleasePoolPage的成员变量内部,我们可以清晰的看到有两个成员变量:
1 | AutoreleasePoolPage * const parent; //指向上一个AutoreleasePoolPage |
parent指针和child指针,parent指向的上一个AutoreleasePoolPage的内存空间地址而child则指向下一个AutoreleasePoolPage的内存地址,当一个AutoreleasePoolPage的空间被占满时,会新建一个AutoreleasePoolPage对象,连接链表,后来的autorelease对象在新的page加入。这样无论在添加autorelease对象地址和释放autorelease对象地址的时候都能很准确的找到对应的AutoreleasePoolPage的地址
具体查看AutoreleasePoolPage的工作原理,可以用_objc_autoreleasePoolPrint这个私有函数来查看
Runloop和AutoreleasePool的关系
我们新建一个空的工程,在viewDidLoad打印[NSRunLoop mainRunLoop]的详细信息,我们会在observers发现两个关于AutoreleasePool的Handler操作_wrapRunLoopWithAutoreleasePoolHandler:
1 | observers = ( |
我们查看它的activities,分别是在0x1和0xa0,那这两个分别有代表是什么呢?在runloop 的源码里我们可以找到runloop的相关枚举:
1 | typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) { |
根据位运算可以的出上述结果:0x1 = 1 等价于kCFRunLoopEntry,0xa0 = 64 + 128 等价于 kCFRunLoopBeforeWaiting | kCFRunLoopExit,意味着runloop会在kCFRunLoopEntry时进行一次push操作,在kCFRunLoopBeforeWaiting进行一次pop操作,然后在进行一次push操作,最后会在kCFRunLoopExit时进行一次pop操作。
也就是说runloop会在即将进行休眠和退出runloop是将AutoreleasePool进行释放。