Autoreleasepool
Autoreleasepool
: 自动释放池,
在ARC中,我们通常通过如下形式使用autoreleasepool
:
@autoreleasepool {
// do your code
}
实际上,编译器会将上面的代码转换为:
objc_autoreleasePoolPush();
// do your code
objc_autoreleasePoolPop();
相当于把autorelease的{}内执行的代码前后加上这两句
在iOS中,除了需要手动retain,release(现在已经交给了ARC自动生成)外,我们还可以将对象扔到自动释放池中,由自动释放池来自动管理这些对象。我们可以这样使用autoreleasepool
:
int main(int argc, char * argv[]) {
@autoreleasepool {
NSString *str = [NSString stringWithFormat:@"%d", 123];
}
}
clang
后可以得到:
int main(int argc, char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
NSString *str = ((NSString * _Nonnull (*)(id, SEL, NSString * _Nonnull, ...))(void *)objc_msgSend)((id)objc_getClass("NSString"), sel_registerName("stringWithFormat:"), (NSString *)&__NSConstantStringImpl__var_folders_8k_3pbszhls2czcmz0w335cvc0w0000gn_T_main_1a8fc0_mi_1, 123);
}
}
会发现,@autoreleasepool
被改写为了__AtAutoreleasePool __autoreleasepool
这样一个对象。在clang的代码里__AtAutoreleasePool
的定义为:
struct __AtAutoreleasePool {
__AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
void * atautoreleasepoolobj;
};
因此,关于@autoreleasepool
的代码可以被改写为开篇说的那样:
objc_autoreleasePoolPush();
// Do your code
objc_autoreleasePoolPop(atautoreleasepoolobj);
置于@autoreleasepool的{}
中的代码实际上是被一个push
和pop
操作所包裹。当push时,会找到一个hotpage
,并在page中第一个对象压栈之前
先压栈一个本次autorelease的界限标识POOL_BOUNDARY
地址。在{}中的所有的autorelease对象
都会放到这个page
中。当pop
时,会把本次存到page中的对象出栈,做release操作,直到把本次存储的界限标识POOL_BOUNDARY出栈时结束
。这就是一次autoreleasepool
的实现原理。
objc_autoreleasePoolPush()
我们先看下runtime
中objc_autoreleasePoolPush()
的内部逻辑:
void *
objc_autoreleasePoolPush(void)
{
return AutoreleasePoolPage::push();
}
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;
}
static inline id *autoreleaseFast(id obj)
{
AutoreleasePoolPage *page = hotPage();
if (page && !page->full()) {
return page->add(obj);
} else if (page) {
return autoreleaseFullPage(obj, page);
} else {
return autoreleaseNoPage(obj);
}
}
可以看到,当objc_autoreleasePoolPush()时,最终会调用到
autoreleaseFast, 在
autoreleaseFast中,会首先取出当前线程的
hotPage,根据当前
hotPage`的三种状态:
-
hot page
存在且未满
,调用page->add(obj)
-
hot page
存在但已满
, 调用autoreleaseFullPage(obj, page)
创建新的page,并设置成hot page
。 -
hot page 不存在
,调用autoreleaseNoPage(obj)
创建新的page,并设置成hot page
。
AutoreleasePoolPage
AutoreleasePoolPage
在runtime
中的定义如下:
class AutoreleasePoolPage
{
magic_t const magic; // 魔数,用于自身的完整性校验 16字节
id *next; // 指向autorelePool page中的下一个可用位置 8字节
pthread_t const thread; // 和autorelePool page中相关的线程 8字节
AutoreleasePoolPage * const parent; // autoreleasPool page双向链表的前向指针 8字节
AutoreleasePoolPage *child; // autoreleasPool page双向链表的后向指针 8字节
uint32_t const depth; // 当前autoreleasPool page在双向链表中的位置(深度) 4字节
uint32_t hiwat; // high water mark. 最高水位,可用近似理解为autoreleasPool page双向链表中的元素个数 4字节
// SIZE-sizeof(*this) bytes of contents follow
}
每个AutoreleasePoolPage
的大小为一个SIZE
,即内存管理中一个页的大小。这在Mac中是4KB
,而在iOS中,这里没有相关代码,估计差不多。
由源码可用看出,在AutoreleasePoolPage
类中共有7个成员属性,大小为56Bytes
,按照一个Page是4KB(4096字节)
计算,显然还有4040字节没有用。剩下的空间用来存放autorelease对象
的地址。
所有的AutoreleasePoolPage
对象通过双向链表
的形式连接在一起。
这个结合起来的双向链表就是我们所一直说的
AutoreleasePool
在图中也可以看出,单个的
AutoreleasePoolPage
是以栈的形式存储的。当加入到
autoreleasepool中的元素太多
时,一个AutoreleasePoolPage
就不够用的了。这时候我们需要新创建一个AutoreleasePoolPage
,多个AutoreleasePoolPage
之间通过双向链表
的形式串起来。成员parent
和child
就是用来构造双向链表的。
图中的一个begin()
就是对应objc_autoreleasePoolPush()
找到hotpage
的开始page->add(obj)
操作,依次入栈obj。
hotPage
hotPage
是autoreleasePage链表
中正在使用的autoreleasePage节点
。实质上是指向autoreleasepage的指针
,并存储于线程的TSD
(线程私有数据:Thread-specific Data)中:
static inline AutoreleasePoolPage *hotPage()
{
AutoreleasePoolPage *result = (AutoreleasePoolPage *)
tls_get_direct(key);
if ((id *)result == EMPTY_POOL_PLACEHOLDER) return nil;
if (result) result->fastcheck();
return result;
}
static inline void *tls_get_direct(tls_key_t k)
{
ASSERT(is_valid_direct_key(k));
if (_pthread_has_direct_tsd()) {
return _pthread_getspecific_direct(k);
} else {
return pthread_getspecific(k);
}
}
从这段代码可以看出,
autoreleasepool是和线程绑定的
,一个线程会对应一个autoreleasepool
,这个绑定和对应主要是runloop中的autoreleasepool,因为在runloop的run的过程中底层逻辑会去创建和销毁一个autoreleasepool。而我们也可以自己在需要的时候手动去创建自己的autoreleasepool
。而autoreleasepool
虽然叫做自动释放池,其实质上是多个autoreleasepoolpage组成的双向链表
。
在介绍runloop的时候,我们也曾提到过,runloop和线程也是一一对应的,并且在当前线程的runloop指针,也会存储到线程的TSD中。这是runtime对于TSD的一个应用。
我们应该想到的runloop和autoreleasepool的关系。
从其他地方看到,比如在gunstep
的NSThread
和NSRunLoop
伪代码逻辑里我们可以看到:
// 子线程的runloop去run起来的最终会调到这里
- (BOOL) runMode: (NSString*)mode beforeDate: (NSDate*)date
{
NSAutoreleasePool *arp = [NSAutoreleasePool new];
NSString *savedMode = _currentMode;
GSRunLoopCtxt *context;
NSDate *d;
NSAssert(mode != nil, NSInvalidArgumentException);
/* Process any pending notifications.
*/
GSPrivateNotifyASAP(mode);
/* And process any performers scheduled in the loop (eg something from
* another thread.
*/
_currentMode = mode;
context = NSMapGet(_contextMap, mode);
[self _checkPerformers: context];
_currentMode = savedMode;
/* Find out how long we can wait before first limit date.
* If there are no input sources or timers, return immediately.
*/
d = [self limitDateForMode: mode];
if (nil == d)
{
[arp drain];
return NO;
}
/* Use the earlier of the two dates we have (nil date is like distant past).
*/
if (nil == date)
{
[self acceptInputForMode: mode beforeDate: nil];
}
else
{
/* Retain the date in case the firing of a timer (or some other event)
* releases it.
*/
d = [[d earlierDate: date] copy];
[self acceptInputForMode: mode beforeDate: d];
RELEASE(d);
}
[arp drain];
return YES;
}
这里也能看到
autoreleasepool
是和线程绑定
的现象,(因为runloop和线程绑定的)。可以看到线程中的runloop
在每次run循环执行任务
的时候会创建一个autoreleasepool对象
,在线程里有autoreleas
的对象需要add(obj)
的时候就去按照上面提到的hotpage的逻辑
,向autoreleasepage
里压栈autoreleas
的对象,在一次run循环结束的时候,也销毁了当前创建的autoreleasepool对象。
iOS在
主线程的Runloop
中的Observer里对主线程的Autoreleasepool对象
做了操作。
1.如果Observer
监听了kCFRunLoopEntry
事件:
会在这个监听里调用objc_autoreleasePoolPush()
2.如果Observer
监听的是kCFRunLoopBeforeWaiting
事件,会调用先objc_autoreleasePoolPop()
然后再调用objc_autoreleasePoolPush()
3.如果Observe
r监听的是kCFRunLoopBeforeExit
事件:
只调用objc_autoreleasePoolPop()
objc_autoreleasePoolPop()
当需要pop autorelease pool
时,则会调用objc_autoreleasePoolPop()
:
static inline void pop(void *token)
{
AutoreleasePoolPage *page;
id *stop;
if (token == (void*)EMPTY_POOL_PLACEHOLDER) {
// Popping the top-level placeholder pool.
if (hotPage()) {
// Pool was used. Pop its contents normally.
// Pool pages remain allocated for re-use as usual.
pop(coldPage()->begin());
} else {
// Pool was never used. Clear the placeholder.
setHotPage(nil);
}
return;
}
page = pageForPointer(token);
stop = (id *)token;
if (*stop != POOL_BOUNDARY) {
if (stop == page->begin() && !page->parent) {
// Start of coldest page may correctly not be POOL_BOUNDARY:
// 1. top-level pool is popped, leaving the cold page in place
// 2. an object is autoreleased with no pool
} else {
// 这是为了兼容旧的SDK,看来在新的SDK里面,token 可能的取值只有两个:POOL_BOUNDARY, page->begin() && !page->parent
// Error. For bincompat purposes this is not
// fatal in executables built with old SDKs.
return badPop(token);
}
}
// 对page中的object做objc_release操作,一直到stop
page->releaseUntil(stop);
// memory: delete empty children 删除多余的child,节约内存
if (page->child) {
// hysteresis: keep one empty child if page is more than half full
if (page->lessThanHalfFull()) {
page->child->kill();
}
else if (page->child->child) {
page->child->child->kill();
}
}
}
在objc_autoreleasePoolPop()
中,会根据传入的token,调用page->releaseUntil(stop)
方法,对每一个存储于page
上的object
调用objc_release(obj)
方法。
之后,还会根据当前page的状态:page->lessThanHalfFull()
或其他,来决定其child的处理方式:
1.如果当前pag
存储的object
已经不满半页
,则讲page的child释放
2.如果当前page
存储的object仍满半页
,则保留一个空的child
,并且将空child
之后的所有child都释放掉
。
何时需要autoreleasePool
以上就是autoreleasepool
流程的内容。那么在ARC的环境下,我们何时需要用@autoreleasepool
呢?
一般的,有下面两种情况:
- 创建子线程。当我们创建子线程的时候,需要将子线程的runloop用@autoreleasepool包裹起来,进而达到自动释放内存的效果。因为系统并不会为子线程自动包裹一个@autoreleasepool(除非run起来线程的runloop,这种情况就等同于主线程的自动释放内存处理),这样加入到autoreleasepage中的元素就得不到释放。
- 在大循环中创建
autorelease对象
。当我们在一个循环中创建autorelease对象(不是用alloc创建的对象,见下方补充
),该对象会加入到autoreleasepage
中,而这个page中的对象,会等到外部池子结束才会释放。在主线程的runloop
中,会将所有的对象的释放权都交给了RunLoop
的释放池,而RunLoop
的释放池会等待这个事件处理之后才会释放,因此就会使对象无法及时释放,堆积在内存造成内存泄露
。
🌰
for (int i = 0; i < 1000000; i++) {
NSString *str = [NSString stringWithFormat:@"hello world -%d", i];
}
[NSString stringWithFormat:@"hello world -%d", i]方法创建的对象会加入到自动释放池里,对象的释放权交给了RunLoop 的释放池
RunLoop 的释放池会等待Runloop即将进入睡眠或者即将退出的时候释放一次
-
for循环中线程一直在做事情,Runloop不会进入睡眠,而RunLoop的释放池会等待这个事件处理之后才会释放,因此就会使对象无法及时释放,堆积在内存造成内存泄露。
解决办法:
1
1.1stringWithFormat:本质上是调用了alloc + initWithFormat: + autorelease
1.2将stringWithFormat:方法换成了alloc + initWithFormat:
1.3这样做问题就解决了:内存几乎没有变化
1.4(因为对象归当前的局部调用者str所有,在出了for循环每次的大括号{}方法体的时候就被释放了)。
1.5反向验证了内存飙升确实是autorelease创建方式造成的。
for (int i = 0; i < 1000000; i++) {
NSString *str = [[NSString alloc] initWithFormat:@"hello world -%d", i];
}
2
2.1加一个局部的自动释放池autoreleasepool,
2.2主动的调用objc_autoreleasePoolPush()和objc_autoreleasePoolPop()操作
for (int i = 0; i < 1000000; i++) {
@autoreleasepool {
NSString *str = [NSString stringWithFormat:@"hello world -%d", i];
}
}
补充 :
在ARC时代,若方法名以下列词语开头,则其返回对象归调用者所有
(意为需调用者负责释放内存,但对ARC来说,一般都是运行在大括号{}函数体里,保存在函数的栈空间里的指针变量所指向,在出了这个大括号{}函数体的时候,会被系统自动的release,没有手动release的必要)
alloc
new
copy
-
mutableCopy
而不使用这些词语开头的方法,如[NSDictionary dictionary]
,
根据苹果官方文档,当调用[NSDictionary dictionary]
时:会产生一个临时的对象。类似的,还有[NSArray array], [NSData data],[NSString stringWithFormat:]...
。
关于这种形式生成的变量,则表示方法所返回的对象并不归调用者所有
。在这种情况下,返回的对象会自动释放。
其实我们可以理解为:当调用dictionary形式生成对象时,NSDictionary对象的引用计数管理,就不需要用户参与了(这在MRC时代有很大的区别,但是对于ARC来说,其实和alloc形式没有太大的区别了)。用[NSDictionary dictionary]
其实相当于代码
[[NSDictionary alloc] init] autorelease];
这里会将NSDictionary
对象交给了autorelease pool
来管理。