本质
@autoreleasepool
,即自动释放池,是自动内存管理的核心。官方文档给出的解释如下:
Autorelease pool implementation
A thread's autorelease pool is a stack of pointers.
Each pointer is either an object to release, or POOL_BOUNDARY which is
an autorelease pool boundary.
A pool token is a pointer to the POOL_BOUNDARY for that pool. When
the pool is popped, every object hotter than the sentinel is released.
The stack is divided into a doubly-linked list of pages. Pages are added
and deleted as necessary.
Thread-local storage points to the hot page, where newly autoreleased
objects are stored.
解释如下:
autoreleasepool
的实现
- 线程的自动释放池是指针的栈,即自动释放池是栈结构。
- 每个指针要么是要释放的对象,要么是自动释放池边界
POOL_BOUNDARY
。 -
POOL_BOUNDARY
对自动释放池来说就是一个token
指针。当释放池开始进行pop
操作时,对象都在POOL_BOUNDARY
之前释放。 - 释放池是一个双向链表页,页面并根据需要来添加或者删除。
-
tls
指针会指向最新页面的最新存储的对象。
从代码可以看出iOS
程序入口就是在自动释放池内的。
int main(int argc, char * argv[]) {
NSString * appDelegateClassName;
@autoreleasepool {
appDelegateClassName = NSStringFromClass([AppDelegate class]);
}
return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}
使用clang
指令将mian.m
文件编译成mian.cpp
,可以看出
@autoreleasepool {
}
变成了如下代码:
{ __AtAutoreleasePool __autoreleasepool;
}
__AtAutoreleasePool
则是一个结构体:
struct __AtAutoreleasePool {
__AtAutoreleasePool() {
atautoreleasepoolobj = objc_autoreleasePoolPush();
}
~__AtAutoreleasePool() {
objc_autoreleasePoolPop(atautoreleasepoolobj);
}
void * atautoreleasepoolobj;
};
说明@autoreleasepool
其实是调用了objc_autoreleasePoolPush
和objc_autoreleasePoolPop
这2个方法,其中objc_autoreleasePoolPush
是构造函数,而objc_autoreleasePoolPop
是析构函数。当然,也可以通过查看汇编来验证这个结论。
在@autoreleasepool
出设置一个断点运行程序,进入汇编也可以看到,程序的入口处是objc_autoreleasePoolPush
,出口处是objc_autoreleasePoolPop
:
原理
结构
可以看出@autoreleasepool
是属于objc
的,那我们直接去源码里一探究竟。
void *
objc_autoreleasePoolPush(void)
{
return AutoreleasePoolPage::push();
}
void
objc_autoreleasePoolPop(void *ctxt)
{
AutoreleasePoolPage::pop(ctxt);
}
class AutoreleasePoolPage : private AutoreleasePoolPageData
{
}
AutoreleasePoolPage
继承自AutoreleasePoolPageData
,而AutoreleasePoolPageData
的结构如下:
class AutoreleasePoolPage;
struct AutoreleasePoolPageData {
magic_t const magic; // 16
__unsafe_unretained id *next; // 8
pthread_t const thread; // 8
AutoreleasePoolPage * const parent; // 8
AutoreleasePoolPage *child; // 8
uint32_t const depth; // 4
uint32_t hiwat; // 4
AutoreleasePoolPageData(__unsafe_unretained id* _next, pthread_t _thread, AutoreleasePoolPage* _parent, uint32_t _depth, uint32_t _hiwat)
: magic(), next(_next), thread(_thread),
parent(_parent), child(nil),
depth(_depth), hiwat(_hiwat)
{
}
};
其中的参数意思如下:
-
magic_t const magic
:用来校验自动释放池结构的完整性 -
__unsafe_unretained id *next
:指向最新添加到自动释放池的对象的下一个位置(对象的末尾位置),初始化的时候指向begin
位置 -
pthread_t const thread
:指向当前线程,说明autoreleasepool
是与线程紧密相关的,每一个autoreleasepool
只对应一个线程,而一个线程可以对应多个autoreleasepool
-
AutoreleasePoolPage * const parent
:指向父page
,第一页的parent
为nil -
AutoreleasePoolPage *child
:指向子page
,最后一页的parent
为nil -
uint32_t const depth
:表示深度,即第几页,从0开始 -
uint32_t hiwat
: 暂时没明白这个属性的含义
可以得出,自动释放池里面存放着一个双向链表,链表的每一个元素就是一页AutoreleasePoolPage
,我们需要自动释放的对象都存放在AutoreleasePoolPage
中。那么一页AutoreleasePoolPage
能存放多少对象呢?
@autoreleasepool {
for (int i = 0; i < 5; i++) {
TPerson *person = [[TPerson alloc] autorelease];
NSLog(@"==%@==", person);
}
_objc_autoreleasePoolPrint();
}
运行程序,控制台输出:
可以看出释放了6个对象,我们明明只创建了5个对象,为什么释放了6个对象,其实有一个是我们前面提到的POOL_BOUNDARY
。再把i
改为1000,运行程序,可以得出,一页能存放505个对象,只是第一页有一个特殊的对象,POOL_BOUNDARY
。这个我们也可以从源码中得到验证:
#define I386_PGBYTES 4096
#define PAGE_SIZE I386_PGBYTES
PAGE_SIZE
是4096,4096减去AutoreleasePoolPage
其他变量的内存大小,即(4096-56)/8 = 505。
push
熟悉了autoreleasepool
的结构,再来看看它是怎么进行初始化构造的。先解释几个名词:
-
hotPage
:新入池子的对象都会放到hotPage
。 -
page->full()
:当前页已经满了
static inline void *push()
{
id *dest;
if (slowpath(DebugPoolAllocation)) {
// 是DEBUG环境
dest = autoreleaseNewPage(POOL_BOUNDARY);
} else {
// 调用@autoreleasepool的时候传入的参数是POOL_BOUNDARY
dest = autoreleaseFast(POOL_BOUNDARY);
}
return dest;
}
static inline id *autoreleaseFast(id obj)
{
// 获取hotPage
AutoreleasePoolPage *page = hotPage();
if (page && !page->full()) {
// 入栈
return page->add(obj);
} else if (page) {
// page满了
return autoreleaseFullPage(obj, page);
} else {
// 没有获取到page
return autoreleaseNoPage(obj);
}
}
id *add(id obj)
{
unprotect();
id *ret = next;
// 把obj放入next指针地址内,然后next指针地址++
*next++ = obj;
protect();
return ret;
}
static __attribute__((noinline))
id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page)
{
// hotPage满了,遍历hotPage的子面,
// 如果有没满的页就把该页设置为hotPage
// 找不到没满的子页,就新创建一页
do {
if (page->child) page = page->child;
else page = new AutoreleasePoolPage(page);
} while (page->full());
setHotPage(page);
// 添加对象
return page->add(obj);
}
static __attribute__((noinline))
id *autoreleaseNoPage(id obj)
{
// 创建一个新页,设置为hotPage
AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
setHotPage(page);
// 添加`POOL_BOUNDARY`对象
if (pushExtraBoundary) {
page->add(POOL_BOUNDARY);
}
// 对象入页
return page->add(obj);
}
初始化自动释放池的步骤如下:
- 获取
hotPage
- 如果存在
hotPage
-
hotPage
没有满,把对象放入next
指针地址,然后next
指针地址++ -
hotPage
满了,遍历hotPage
的子页- 如果有没满的页,就把该页设置为
hotPage
,存入对象 - 找不到没满的子页,就新创建一页,设置为
hotPage
,存入对象
- 如果有没满的页,就把该页设置为
-
- 如果不存在
hotPage
,新创建一页,设置为hotPage
,如果是个空池子则放入边界对象POOL_BOUNDARY
,存入对象。
需要注意的是,初始化自动释放池,存入的其实是边界对象POOL_BOUNDARY
。
pop
自动释放池的析构函数即为AutoreleasePoolPage::pop(ctxt)
,此时传入的参数即为POOL_BOUNDARY
,调用该方法会把整个释放池里面的对象全部释放。
static inline void
pop(void *token)
{
AutoreleasePoolPage *page;
id *stop;
if (token == (void*)EMPTY_POOL_PLACEHOLDER) {
// Popping the top-level placeholder pool.
// 出栈的对象是不是自动释放池的占位对象
page = hotPage();
if (!page) {
// 自动释放池是空的,删除占位对象
return setHotPage(nil);
}
// Pool was used. Pop its contents normally.
// Pool pages remain allocated for re-use as usual.
page = coldPage();
token = page->begin();
} else {
// 获取到需要释放的对象存储的页
page = pageForPointer(token);
}
stop = (id *)token;
if (*stop != POOL_BOUNDARY) {
// 不是边界对象
// 要出栈的对象在第一页的开始节点
if (stop == page->begin() && !page->parent) {
} else {
return badPop(token);
}
}
return popPage(token, page, stop);
}
static void
popPage(void *token, AutoreleasePoolPage *page, id *stop)
{
page->releaseUntil(stop);
......
if (page->child) {
// 如果当前页存储已经超过一半,给其保留一个空的子页
if (page->lessThanHalfFull()) {
page->child->kill();
}
else if (page->child->child) {
page->child->child->kill();
}
}
}
void releaseUntil(id *stop)
{
// 循环遍历 清除自动释放池里的所有对象 并释放
while (this->next != stop) {
AutoreleasePoolPage *page = hotPage();
while (page->empty()) {
// 如果当前页是空的,就把其父页设置为hotPage
page = page->parent;
setHotPage(page);
}
// 修改next指针的指向
page->unprotect();
id obj = *--page->next;
memset((void*)page->next, SCRIBBLE, sizeof(*page->next));
page->protect();
if (obj != POOL_BOUNDARY) {
// 非边界对象的释放需要调用objc_release
objc_release(obj);
}
}
setHotPage(this);
}
自动释放池析构的步骤如下:
-
- 获取边界对象
POOL_BOUNDARY
所在的page
- 获取边界对象
-
- 判断
page
是不是第一页的第一个节点,如果不是就抛出异常
- 判断
-
- 遍历释放池里的所有对象,修改
next
指针的指向,并且释放非POOL_BOUNDARY
的对象
- 遍历释放池里的所有对象,修改
-
- 删除当前页的子页
autoreleasepool
的嵌套
下面我们来看一个例子
@autoreleasepool {
NSObject *obj1 = [[NSObject alloc] autorelease];
NSLog(@"obj1 = %@",obj1);
NSLog(@"-----------00000-----------");
@autoreleasepool {
NSObject *obj2 = [[NSObject alloc] autorelease];
NSLog(@"obj2 = %@",obj2);
NSObject *obj3 = [[NSObject alloc] autorelease];
NSLog(@"obj3 = %@",obj3);
_objc_autoreleasePoolPrint();
NSLog(@"-----------11111-----------");
@autoreleasepool {
NSObject *obj4 = [[NSObject alloc] autorelease];
NSLog(@"obj4 = %@",obj4);
_objc_autoreleasePoolPrint();
NSLog(@"---------22222-------------");
}
}
NSLog(@"---------33333-------------");
_objc_autoreleasePoolPrint();
}
sleep(5);
return 0;
运行程序,控制台输出:
可以看出,在第一次打印的地方,page
里面有5个对象,obj1
、obj2
、obj3
还有2个POOL
也就是边界对象;第二个打印的地方有7个对象,obj1
、obj2
、obj3
、obj4
还有3个边界对象;而在第三次打印的时候只有2个对象了,obj1
和1个边界对象,这是因为前面的对象被释放了。我们前面说了一个page
里面可以存放505个对象,所以当出现嵌套的时候,一个page
里面可能存在多个释放池。而嵌套的时候,每个释放池的边界对象都是单独存在的。
如果释放池是串行的,则互不影响。
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSObject *obj1 = [[NSObject alloc] autorelease];
NSLog(@"obj1 = %@",obj1);
_objc_autoreleasePoolPrint();
}
NSLog(@"---------22222-------------");
@autoreleasepool {
NSObject *obj2 = [[NSObject alloc] autorelease];
NSLog(@"obj2 = %@",obj2);
NSObject *obj3 = [[NSObject alloc] autorelease];
NSLog(@"obj3 = %@",obj3);
_objc_autoreleasePoolPrint();
}
NSLog(@"---------33333-------------");
@autoreleasepool {
NSObject *obj4 = [[NSObject alloc] autorelease];
NSLog(@"obj4 = %@",obj4);
_objc_autoreleasePoolPrint();
}
sleep(5);
return 0;
}
autoreleasepool
的使用
通常我们是不必自己创建自动释放池,甚至不必查看用于创建自动释放池的代码。但是在以下3种情况,我们可能会使用到自动释放池:
- 编写不基于
UI
框架的程序,例如命令行工具。 - 编写一个创建许多临时对象的循环语句。可以在循环内使用自动释放池在下一次迭代之前处理这些对象,有助于减少应用程序的最大内存占用。
- 使用辅助线程的时候,一旦线程开始执行,就必须创建自己的自动释放池块;否则可能出现内存泄露。
for (int i = 0; i < 100000; i++) {
@autoreleasepool {
TPerson *person = [[TPerson alloc] init];
NSLog(@"==name==%@==", person.name);
}
}
autorelease
看完了@autoreleasepool
,我们再来看看autorelease
。
- (id)autorelease {
return _objc_rootAutorelease(self);
}
NEVER_INLINE id
_objc_rootAutorelease(id obj)
{
return obj->rootAutorelease();
}
inline id
objc_object::rootAutorelease()
{
if (isTaggedPointer()) return (id)this;
if (prepareOptimizedReturn(ReturnAtPlus1)) return (id)this;
return rootAutorelease2();
}
id
objc_object::rootAutorelease2()
{
ASSERT(!isTaggedPointer());
return AutoreleasePoolPage::autorelease((id)this);
}
static inline id autorelease(id obj)
{
id *dest __unused = autoreleaseFast(obj);
return obj;
}
根据代码,可以看出,对象执行autorelease
方法后自身引用计数器不会改变,而且会返回对象本身。而且autoreleasepool
和autorelease
的区别就是传入的参数不同,autoreleasepool
初始化的时候传入的是POOL_BOUNDARY
,而autorelease
传入的是对象本身。autorelease
实际上只是把对release
的调用延迟了,因为对象调用了autorelease
,系统就会把该对象放入了当前的autoreleasepool
中,当autoreleasepool
被释放时,其中的对象才会被调用release
。
总结
autoreleasepool
是一种自动内存管理机制,一般情况下,创建的变量会在超出其作用域的时候就会release
,但是如果将变量加入到autoreleasePool
中,那么release
操作将延迟到在释放池销毁的时候执行。其结构是一个双向链表,链表的元素是AutoreleasePoolPage
。ARC
下,自动释放池的压栈操作是系统自行处理的。autoreleasepool
的初始化过程如下:
-
autoreleasepool
的释放过程如下:
-
向对象执行
autorelease
操作会将对象放入autoreleasepool
,此时对象的释放时间就被延迟了,会和autoreleasepool
一起释放。autorelease
的流程:autorelease
->_objc_rootAutorelease
->rootAutorelease
->rootAutorelease2
->AutoreleasePoolPage::autorelease
->autoreleaseFast
之后就和autoreleasepool
初始化的流程类似,只是传入的对象不同,autoreleasepool
传入的是POOL_BOUNDARY
,而autorelease
传入的是当前对象。
-
autoreleasepool
可以嵌套使用,此时边界对象POOL_BOUNDARY
都会存在,互不影响。
参考文献:
- 官方源代码
- 官方文档