在MRC中,调用[obj autorelease]
来延迟内存的释放;在ARC下,对象调用autorelease
方法,就会被自动添加到最近的自动释放池,只有当自动释放池被销毁的时候,才会执行release
方法,进行释放。真实结果到底是什么,等看完源码后我们就会知道了。
AutoreleasePool
main.m
中有一段代码:
1 | int main(int argc, char * argv[]) { |
转换成C++代码:
1 | int main(int argc, char * argv[]) { |
@autoreleasepool
变成__AtAutoreleasePool __autoreleasepool
。__AtAutoreleasePool
结构体定义如下:
1 | struct __AtAutoreleasePool { |
它提供了两个方法:objc_autoreleasePoolPush
和objc_autoreleasePoolPop
。这两个方法的定义在NSObject.mm
文件中,分别是:
1 | void *objc_autoreleasePoolPush(void) { |
所以,autoreleasepool的自动释放的核心就是AutoreleasePoolPage类。
AutoreleasePoolPage
AutoreleasePoolPage的结构
在NSObject.mm
文件中定义了AutoreleasePoolPage
,这里我们只显示这个类比较重要的属性:
1 | class AutoreleasePoolPage { |
通过源码我们可以知道:
- AutoreleasePool并没有特定的内存结构,它是通过以
AutoreleasePoolPage
为节点的双向链表。 - 每一个
AutoreleasePoolPage
节点是一个堆栈结构,且大小为4096个字节。 - 一个
AutoreleasePoolPage
节点对应着一个线程,属于一一对应关系。
AutoreleasePool结构如图所示:
接着我们看一下AutoreleasePoolPage
的构造函数以及一些操作方法:
1 | // 构造函数 |
所以一个空的AutoreleasePoolPage
的结构如下:
AutoreleasePoolPage::push()
push代码如下:
1 | static inline void *push() { |
push执行的时候首先会进行判断,如果是需要每个pool都生成一个新page,即DebugPoolAllocation
为YES
,则执行autoreleaseNewPage
方法,否则执行autoreleaseFast
方法。
autoreleaseNewPage
1 | static __attribute__((noinline)) id *autoreleaseNewPage(id obj) { |
autoreleaseNewPage
分为两种情况:
- 当前存在page执行
autoreleaseFullPage
方法; - 当前不存在page
autoreleaseNoPage
方法。
autoreleaseFast
1 | static inline id *autoreleaseFast(id obj) { |
autoreleaseFast
分为三种情况:
- 存在page且未满,通过
add()
方法进行添加; - 当前page已满执行
autoreleaseFullPage
方法; - 当前不存在page执行
autoreleaseNoPage
方法。
hotPage
前面讲到的page其实就是hotPage
,通过AutoreleasePoolPage *page = hotPage();
获取。
1 | static inline AutoreleasePoolPage *hotPage() { |
通过上面的代码我们知道当前页是存在TLS(线程私有数据)
里面的。所以说第一次调用push的时候,没有page自然连hotPage也没有。
autoreleaseFullPage
1 | static __attribute__((noinline)) |
autoreleaseFullPage
会从传入的page
开始遍历整个双向链表,如果page
满了,就看它的child
节点,直到查找到一个未满的AutoreleasePoolPage
。接着使用AutoreleasePoolPage
构造函数传入parent
创建一个新的AutoreleasePoolPage
的节点(此时跳出了while循环)。
在查找到一个可以使用的AutoreleasePoolPage
之后,会将该页面标记成hotPage
,然后调动add()
方法添加对象。
autoreleaseNoPage
1 | static __attribute__((noinline)) |
从上面的代码我们可以知道,当前内存中不存在AutoreleasePoolPage
,就要从头开始构建这个自动释放池的双向链表,也就是说,新的AutoreleasePoolPage
是没有parent
指针的。初始化之后,将当前页标记为hotPage
,然后会先向这个page
中添加一个POOL_BOUNDARY
的标记,来确保在pop
调用的时候,不会出现异常。最后,将obj
添加到自动释放池中。
所以push的流程是:
AutoreleasePoolPage::pop(ctxt)
1 | static inline void pop(void *token) { |
这里我们主要分析下第三种情况。
releaseUntil
1 | void releaseUntil(id *stop) { |
从next指针开始,一个一个向前调用objc_release
,直到碰到push时压入的pool为止。
所以autoreleasePool的运行过程应该是:
1 | pool1 = push() |
每次pop,实际上都会把最近一次push之后添加进去的对象全部release掉。
autorelease方法
接着看一下当对象调用autorelase
方法发生了什么。
1 | - (id)autorelease { |
从上面的源码我们看到,对象调用autorelase
方法,最后会变成AutoreleasePoolPage
的autorelease
函数。AutoreleasePoolPage
的autorelease
的本质就是调用autoreleaseFast(obj)
函数。只不过push
操作插入的是一个POOL_BOUNDARY
,而autorelease
操作插入的是一个具体的autoreleased
对象即AutoreleasePoolPage
入栈操作。
当然这么说并不严谨,因为我们需要考虑是否是Tagged Pointer
和是否进行优化的情况(prepareOptimizedReturn
这个后面也会提到),如果不满足这两个条件才会进入缓存池。
AutoreleasePool、Runloop、线程之间的关系
苹果的文档中提到:
Each NSThread object, including the application’s main thread, has an NSRunLoop object automatically created for it as needed.
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.
Each thread (including the main thread) maintains its own stack of NSAutoreleasePool objects.
我们可以知道:
每一个线程,包括主线程,都会拥有一个专属的runloop,并且会在有需要的时候自动创建。
主线程在runloop开始之前会自动创建一个autoreleasePool,并在结束时pop。
每一个线程都会维护自己的autoreleasePool堆栈,也就是说每一个autoreleasePool对应一个线程。
进入AutoreleasePool的时机
那什么样的对象会进入autoreleasePool呢?
测试
测试1.1
1 | NSMutableArray *arr = [NSMutableArray new]; |
结果
1 | 2019-01-22 15:50:45.129263+0800 AutoreleasePool[31529:22121744] 1 |
测试1.2
1 | @autoreleasepool{ |
结果
1 | 2019-01-22 15:53:28.818873+0800 AutoreleasePool[31568:22134125] 1 |
测试1.3
1 | { |
结果
1 | 2019-01-22 15:55:21.271452+0800 AutoreleasePool[31596:22141965] 1 |
测试2.1
1 | NSMutableArray *arr = [NSMutableArray array]; |
结果
1 | 2019-01-22 15:57:02.360860+0800 AutoreleasePool[31615:22149043] 2 |
测试2.2
1 | @autoreleasepool { |
结果
1 | 2019-01-22 15:58:29.932693+0800 AutoreleasePool[31634:22153810] 2 |
测试2.3
1 | { |
结果
1 | 2019-01-22 16:01:11.925690+0800 AutoreleasePool[31670:22164284] 2 |
从上面的代码我们可以知道,使用new
、alloc
这样的方法创建的对象实例是不会进入autoreleasePool的,但是使用简便方法创建的对象如[NSMutableArray array]
是会进入自动缓存池的。
但是在测试2.3上,我们可以看到,只有一个array进入了自动缓存池,另外一个没有进入。看一下它的方法调用栈:
在《Objective-C高级编程》第66-67页提到了最优化程序运行。通过objc_retainAutoreleasedReturnValue
和objc_retainAutoreleaseReturnValue
函数的协作,可以不将对象注册到autoreleasePool中而直接传递,这一过程达到最优化。
objc_retainAutoreleasedReturnValue
objc_retainAutoreleasedReturnValue
的实现如下:
1 | // Accept a value returned through a +0 autoreleasing convention for use at +1. |
通过上面的代码我们可以知道objc_retainAutoreleasedReturnValue
会尝试接收一个被优化的结果,如何是ReturnAtPlus1
即YES
,返回对象本身,否则执行objc_retain(obj
。
这个被优化的结果是在线程私有数据TLS中的,我们可以理解为一个优化位。当优化位返回YES的时候,直接返回对象本身,否则执行retain。
objc_retainAutoreleaseReturnValue
objc_retainAutoreleaseReturnValue
的实现如下:
1 | // Prepare a value at +0 for return through a +0 autoreleasing convention. |
这里主要涉及到callerAcceptsOptimizedReturn
,这个函数意思不是很理解,但是里面涉及到了objc_retainAutoreleasedReturnValue
,猜测可能是程序检测在返回值之后是否紧接着调用了objc_retainAutoreleasedReturnValue
,如果是,就知道了外部是ARC环境走优化路线,反之就走没被优化的逻辑。
所以个人认为,使用new
、alloc
这样的方法创建的对象实例是不会进入autoreleasePool的,但是使用简便方法创建的对象,程序会进行优化后,再决定是否进入自动缓存池。
另外关于main函数中的@autoreleasepool
的作用是什么?简单的说就是让那些进入自动缓存池的对象有个地方被释放。
总结
AutoreleasePool的本质
AutoreleasePool并没有特定的内存结构,它是通过以AutoreleasePoolPage为节点的双向链表,每一个AutoreleasePoolPage节点是一个堆栈结构,且每个AutoreleasePoolPage节点对应着一个线程,属于一一对应关系。
AutoreleasePool、Runloop、线程之间的关系
每一个线程,包括主线程,都会拥有一个专属的runloop,并且会在有需要的时候自动创建。
主线程在runloop开始之前会自动创建一个autoreleasePool,并在结束时pop。
每一个线程都会维护自己的autoreleasePool堆栈,也就是说每一个autoreleasePool对应一个线程。
AutoreleasePool是工作原理
在APP中,整个主线程是运行在一个自动释放池中的。
使用
@autoreleasepool
标记,调用push()方法。没有hotpage,调用
autoreleaseNoPage()
,设置EMPTY_POOL_PLACEHOLDER
。因为设置了
EMPTY_POOL_PLACEHOLDER
,所以会设置本页为hotpage
,添加边界标记POOL_BOUNDARY
,最后添加obj。继续有对象调用
autorelease
,此时已经有了page,调用page->add(obj)
。如果page满了,调用
autoreleaseFullPage()
创建新page,重复第6点。到达autoreleasePool边界,调用pop方法,通常情况下会释放掉
POOL_BOUNDARY
之后的所有对象
什么样的对象对进入AutoreleasePool
使用Tagged Pointer技术的对象不会进入AutoreleasePool;
使用
new
、alloc
这样的方法创建的对象实例是不会进入autoreleasePool的;使用简便方法创建的对象,程序会进行优化后,再决定是否进入自动缓存池。
main函数中的自动释放池的作用
这个池块给出了一个pop点来显式的告诉我们这里有一个释放点,如果你的main在初始化的过程中有别的内容可以放在这里。
什么时候需要手动创建AutoreleasePool
一段代码里面(比如for循环)大量使用便利构造器创建对象,可能需要手动添加自动释放池;
在子线程中可能会使用便利构造器等方法来创建对象(类方法),那么这些对象的释放只能放在自动释放池中,此时可能需要在子线程中添加自动释放池。
子线程在使用autorelease对象的能否确保自动释放
必须能!底层函数autoreleaseNoPage
会在没有autoreleasepool的情况下懒加载一个出来。即使push但是没有pop也没关系,当线程exit的时候会释放资源,会执行AutoreleasePoolPage::tls_dealloc
,在这里面会清空autoreleasepool。
AutoreleasePool释放对象的时机
在没有手动建立AutoreleasePool的情况下,主线程是通过RunLoop的监听来决定释放autorelease对象的时机的,而子线程则是在退出线程的时候来释放autorelease对象;
在手动建立AutoreleasePool的情况下,autorelease对象的生命周期仅在block块内。