张东轩的博客

合抱之木,生于毫末;九层之台,起于累土;千里之行,始于足下。

0%

NSAutoreleasePool分析

定义

NSAutoreleasePool是用来做引用计数管理的,当一个对象收到autorelease消息的时候,这个对象就会被放到NSAutoreleasePool中。当NSAutoreleasePool被销毁的时候,NSAutoreleasePool向它包含的每一个对象发送release消息,也就是说调用autorelease并不会立马销毁对象,这样就延长了这个对象的生命周期。

用法

autorelease pool的用法如下:

1
2
3
@autoreleasepool {
// Code that creates autoreleased objects.
}

等同于

1
2
3
4
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];

// Code that creates autoreleased objects.
[pool drain];

Cocoa建议在autorelease pool中执行我们的代码,否则造成一个autorelease的对象无法释放导致内存泄露。AppKit和UIKit会在每次event runloop进行的开始创建autorelease pool,在runloop结束的时候会释放autorelease pool,所以我们一般不需要自己创建一个autorelease pool。

但是还是需要自己创建autorelease pool的场景有如下三种:

  • 不是基于UI Framework的程序,例如命令行程序。
  • 创建了其他线程,而且要创建子该线程启动之前,否则会造成内存泄露。
  • 在循环中创建了很多临时对象,特别是比较占内存的object,这种情况使用runloop会大大减少内存占用。典型的例子是读取大量图像的同时对图像进行压缩,图像文件读入到NSData对象,并从中生成UIImage对象,改变该UIImage的尺寸之后生成新的UIImage对象,这种情况下会成成大量的autorelease的对象,这个时候内存会暴涨。
1
2
3
4
5
6
7
8
9
for(...){
/*
*
* read image data
* compress image
* get new image
* 在循环中产生了大量的autorelease对象,会导致内存不足
*/
}

由于大量的autorelease对象没有得到释放,在for循环中内存会暴涨,特别是在内存受限的场景例如Share Extension(内存限制在100M),在这种情况下就很有必要在合适的地方创建自己的autoreleasepool。

1
2
3
4
5
6
7
8
9
10
11
for(...){

@autoreleasepool{
/*
*
* read image data
* compress image
* get new image
*/
}
}

这种方式会极大的减少内存的占用,因为每次循环都会释放autoreleasepool的block中产生的临时对象。

@autoreleasepool的本质

代码

1
2
3
4
5
6
int main(int argc, const char * argv[]) {

@autoreleasepool {
}
return 0;
}

用clang -rewrite-objc main.m进行转换,得到如下:

1
2
3
4
5
6
7
8
int main(int argc, const char * argv[]) {

/* @autoreleasepool */
{ __AtAutoreleasePool __autoreleasepool;

}
return 0;
}

其中__AtAutoreleasePool的定义如下:

1
2
3
4
5
struct __AtAutoreleasePool {
__AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
void * atautoreleasepoolobj;
};

__AtAutoreleasePool在创建时会执行objc_autoreleasePoolPush,在被销毁的时候会执行objc_autoreleasePoolPop(atautoreleasepoolobj)
很明显@autoreleasepool{}被转换成了

1
2
3
{ __AtAutoreleasePool __autoreleasepool; 

}

也就是说在该代码块开始执行的时候会创建结构体__autoreleasepool也就是执行:

1
void *atautoreleasepoolobj = objc_autoreleasePoolPush();

在改代码块结束执行结束的时候会销毁结构体__autoreleasepool,也就是执行

1
objc_autoreleasePoolPop(atautoreleasepoolobj);

所以@autoreleasepool{}也就相当于:

1
2
3
void *atautoreleasepoolobj = objc_autoreleasePoolPush();
/*..代码..*/
objc_autoreleasePoolPop(atautoreleasepoolobj)

在NSObject的源码中可以看到如下定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void * objc_autoreleasePoolPush(void)
{
return AutoreleasePoolPage::push();
}

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

class AutoreleasePoolPage
{
static inline void *push(){
//相当于生成或持有NSAutoreleasePool类对象
}

static inline void *push(){
//相当于废弃NSAutoreleasePool类对象
}
}

AutoreleasePoolPage的结构

  • AutoreleasePool是由若干AutoreleasePoolPage以双向链表的形式组合而成的结构(parent、child)。
  • thread 指向当前页所在线程。
  • AutoreleasePoolPage每个对象会开辟4096字节内存(也就是虚拟内存一页的大小),除了上面的实例变量所占空间,剩下的空间全部用来储存autorelease对象的地址。
  • AutoreleasePoolPage用栈结构来存储block中的临时对象地址,next指向了下一个将要加入到栈顶的内存地址。当栈满之后会开辟新的page来继续添加。

代码分析

哨兵(POOL_BOUNDARY)

每当AutoreleasePoolPage::push()调用的时候向stack中添加一个哨兵对象(POOL_BOUNDARY),并将该哨兵对象返回。

AutoreleasePoolPage::push

1
2
3
4
5
6
7
8
9
10
11
static inline void *push() {
id *dest;
if (DebugPoolAllocation) {
// Each autorelease pool starts on a new pool page.
dest = autoreleaseNewPage(POOL_BOUNDARY);
} else {
dest = autoreleaseFast(POOL_BOUNDARY);
}
assert(dest == EMPTY_POOL_PLACEHOLDER || *dest == POOL_BOUNDARY);
return dest;
}

当AutoreleasePoolPage::pop(ctxt);调用的时候会将比ctxt(哨兵对象,也就是__AtAutoreleasePool中保存的atautoreleasepoolobj)后面加入的对象释放(When the pool is popped, every object hotter than the sentinel is released)。

AutoreleasePoolPage::autoreleaseFast

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static inline id *autoreleaseFast(id obj)
{
AutoreleasePoolPage *page = hotPage();
if (page && !page->full()) {
//page存在且没满,将哨兵加入栈
return page->add(obj);
} else if (page) {
//page 满了,创建新的page,并将哨兵加入栈
return autoreleaseFullPage(obj, page);
} else {
//没有page,创建page,并将哨兵加入栈
return autoreleaseNoPage(obj);
}
}

NSObject::autorelease

然后我们知道在ARC下,我们生成的对象会自动调用autorelease。autorelease在NSObject.mm中的定义为

1
2
3
4
5
6
7
8
9
- (id)autorelease {
return ((id)self)->rootAutorelease();
}

id objc_object::rootAutorelease2()
{
assert(!isTaggedPointer());
return AutoreleasePoolPage::autorelease((id)this);
}

AutoreleasePoolPage中autorelease的定义如下:

1
2
3
4
5
6
7
8
static inline id autorelease(id obj)
{
assert(obj);
assert(!obj->isTaggedPointer());
id *dest __unused = autoreleaseFast(obj);
assert(!dest || dest == EMPTY_POOL_PLACEHOLDER || *dest == obj);
return obj;
}

可以知道@autoreleasepool{}中创建的对象都会被加入到AutoreleasePoolPage的栈中,AutoreleasePoolPage的栈中只有两种对象一种是POOL_BOUNDARY,一种是在@autoreleasepool{}创建的临时对象。

AutoreleasePoolPage::pop

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static inline void pop(void *token) 
{
AutoreleasePoolPage *page;
id *stop;

page = pageForPointer(token);
stop = (id *)token;

page->releaseUntil(stop);

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

由上文可以当autoreleasepool的block结束时会先调用

1
objc_autoreleasePoolPop(atautoreleasepoolobj)

这里的atautoreleasepoolobj对象就是哨兵对象POOL_BOUNDARY,当然这里也可以理解为任何对象。

1、pop会根据传入的对象拿到其所在的page。
2、然后调用releaseUntil释放token以及比其晚入栈的对象。
3、然后会将空的page给销毁掉。

TLS(Thread Local Storage)

比较有趣的是page是用tls来进行存储的,hotPage使用tls_get_direct来获取当前页,tls中将一块内存作为某个线程专有的存储,以key-value的形式进行读写的,这里和Java中的ThreadLocal是一样的道理。

1
2
3
4
5
6
7
static inline AutoreleasePoolPage *hotPage() 
{
AutoreleasePoolPage *result = (AutoreleasePoolPage *)
tls_get_direct(key);
if (result) result->fastcheck();
return result;
}

参考:
https://developer.apple.com/documentation/foundation/nsautoreleasepool#//apple_ref/occ/cl/NSAutoreleasePool
http://www.jianshu.com/p/32265cbb2a26
https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/MemoryMgmt/Articles/mmAutoreleasePools.html
http://blog.sunnyxx.com/2014/10/15/behind-autorelease/