标签:
无论是在MRC时期还是ARC时期,做过开发的程序员都接触过autoreleasepool。尽管接触过但本人对它还不是很了解。本文只是将自己的理解说出来。在内存管理的文章中提到了OC的内存管理是通过引用计数来完成的,也介绍了可以通过内存管理的方法(alloc/retain/new/copy等)来使引用计数加1,使用release方法来使引用计数减1。在我们创建了大量对象的时候,如果还是手动调用release方法来释放它们就显得太繁琐了。本文章将介绍内存管理的另外一种机制-autoreleasepool。
自动释放池是NSAutoreleasePool的实例,其中包含了收到autorelease消息的对象。当一个自动释放池自身被销毁(dealloc)时,它会给池中每一个对象发送一个release消息(如果你给一个对象多次发送autorelease消息,那么当自动释放池销毁时,这个对象也会收到同样数目的release消息)。可以看出,一个自动释放的对象,它至少能够存活到自动释放池销毁的时候。这样看来它是一种延迟释放机制,这样保证局部堆上的变量能够被外部正常使用。
这里说一下,在Xcode5以前是通过NSAutoreleasePool创建实例来实现的,代码如下:
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; // code [pool drain];但是在Xcode5以后,它的写法就简单了,代码如下:(所以本文的代码主要以这种写法来讲解)
@autoreleasepool { // code }既然autoreleasepool也是一个对象,它在内存中以什么结构进行存储呢?它存储于内存中的栈中,遵循”先进后出”原则。
下面通过代码来简单的看一下autoreleasepool是如何进行内存管理的。
新建一个HXPerson类,重写其dealloc方法,代码如下:
- (void)dealloc { NSLog(@"HXPerson dealloc"); [super dealloc]; }
无autoreleasepool情况:
int main(int argc, const char * argv[]) { HXPerson *person = [[HXPerson alloc] init]; [person release]; return 0; }有autoreleasepool情况:
int main(int argc, const char * argv[]) { @autoreleasepool { // 根据上面介绍的,我们要在初始化的时候,调用autorelease方法 HXPerson *person = [[[HXPerson alloc] init] autorelease]; } return 0; }可以看到,使用autoreleasepool的情况就算没有调用release方法,该person对象也被销毁了。但是在创建person对象的时候一定要调用autorelease方法。该方法主要的作用就是将person对象放在该autoreleasepool中,且person对象在该autoreleasepool没有销毁之前一直是有效的,也就是说该person对象可以被访问,直到该autoreleasepool被销毁。只要autoreleasepool被销毁,放在autoreleasepool里面的所有对象(调用过autorelease的对象)都会自动执行一次release方法来销毁对象。
上篇文章中讲到runLoop的时候就提到autoreleasepool。 App启动后,系统在主线程runLoop里注册两个Observser,其回调都是_wrapRunLoopWithAutoreleasePoolHandler()。第一个 Observer 监视的事件是 Entry(即将进入Loop),其回调内会调用 _objc_autoreleasePoolPush() 创建自动释放池。其优先级最高,保证创建释放池发生在其他所有回调之前。第二个 Observer 监视了两个事件: BeforeWaiting(准备进入休眠) 时调用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 释放旧的池并创建新池;Exit(即将退出Loop) 时调用 _objc_autoreleasePoolPop() 来释放自动释放池。这个 Observer 优先级最低,保证其释放池子发生在其他所有回调之后。在主线程执行的代码,通常是写在诸如事件回调、Timer回调内的。这些回调会被runLoop创建好的AutoreleasePool环绕着,所以不会出现内存泄漏,开发者也不必显示创建Pool。可见开发过程中我们没有创建autoreleasepool,系统也会帮我们创建。这就解释了,为什么开发中没有创建autoreleasepool也没有内存泄露的原因了。关于runLoop
通过下面的例子,我们来看一下,runLoop创建的autoreleasepool是不是真的帮我们管理了内存。
__weak id reference = nil; - (void)viewDidLoad { [super viewDidLoad]; NSString *str = [NSString stringWithFormat:@"autoreleasePool"]; // str是一个autorelease对象,设置一个weak的引用来观察它 reference = str; NSLog(@"%@", reference); // Console: autoreleasePool } - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; NSLog(@"%@", reference); // Console: autoreleasePool } - (void)viewDidAppear:(BOOL)animated { [super viewDidAppear:animated]; NSLog(@"%@", reference); // Console: (null) }这个实验同时也证明了viewDidLoad和viewWillAppear是在同一个runloop调用的,而viewDidAppear是在之后的某个runloop调用的。由于这个vc在loadView之后便add到了window层级上,所以viewDidLoad和viewWillAppear是在同一个runloop调用的,因此在viewWillAppear中,这个autorelease的变量依然有值。
当然,我们也可以不用等到当前runLoop结束,选择手动干预Autorelease对象的释放时机:
- (void)viewDidLoad { [super viewDidLoad]; @autoreleasepool { NSString *str = [NSString stringWithFormat:@"autoreleasePool"]; } NSLog(@"%@", str); // Console: (null) }通过上面的例子,可以看出,没有调用release也做到了内存管理。可是大家注意到了,str对象没有调用autorelease方法啊,怎么被放到autoreleasepool进行管理的呢?其实静态方法已经在内部自动调用了autorelease方法,所有这里不需要再调用。
现在以ARC环境来分析其原理。runLoop创建的autoreleasepool实例我们就以@autoreleasepool形式呈现。新建项目之后,其中的main函数如下:
int main(int argc, char * argv[]) { @autoreleasepool { return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); } }在这个 @autoreleasepool{}中只包含了一行代码,这行代码将所有的事件、消息全部交给了 UIApplication 来处理,但是这不是本文关注的重点。
继续我们的主题。我们知道autoreleasepool是一个自动释放池,那么它到底是一个什么样的数据结构呢?我们在命令行中使用 clang -rewrite-objc main.m
让编译器重新改写这个文件,编译完后,会在该文件目录下多一个.cpp文件。打开这个文件。滚到最底部。可以看到如下代码:(删除掉多余的代码)
int main(int argc, const char * argv[]) { /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; } return 0; }在这个文件中,有一个非常奇怪的
__AtAutoreleasePool
的结构体,前面的注释写到/* @autoreleasepopl */
。也就是说@autoreleasepool {}
被转换为:{ __AtAutoreleasePool __autoreleasepool; }那么__AtAutoreleasePool又是什么?在文件中可以找到__AtAutoreleasePool数据结构如下:
struct __AtAutoreleasePool { __AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();} ~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);} void * atautoreleasepoolobj; };它是一个结构体,该结构体会在初始化时调用
objc_autoreleasePoolPush()
方法,会在析构时调用objc_autoreleasePoolPop
方法。所以我们可以进一步将main函数中的代码改写为如下:
int main(int argc, const char * argv[]) { { void * atautoreleasepoolobj = objc_autoreleasePoolPush(); // do whatever you want objc_autoreleasePoolPop(atautoreleasepoolobj); } return 0; }@autoreleasepool 只是帮助我们少写了这两行代码而已,让代码看起来更美观,然后要根据上述两个方法来分析自动释放池的实现。
objc_autoreleasePoolPush
和 objc_autoreleasePoolPop
的实现:
void *objc_autoreleasePoolPush(void) { return AutoreleasePoolPage::push(); } void objc_autoreleasePoolPop(void *ctxt) { AutoreleasePoolPage::pop(ctxt); }__AtAutoreleasePool的Push和Pop方法看上去方法看上去是对
AutoreleasePoolPage
对应静态方法push
和
pop
的封装。
AutoreleasePoolPage
那么AutoreleasePoolPage
又是一个什么东东呢?,它的定义可以在NSObject.mm文件中看到,定义如下:
class AutoreleasePoolPage { magic_t const magic; id *next; pthread_t const thread; AutoreleasePoolPage * const parent; AutoreleasePoolPage *child; uint32_t const depth; uint32_t hiwat; };
magic
用于对当前 AutoreleasePoolPage
完整性 的校验thread
保存了当前页所在的线程每一个自动释放池都是由一系列的 AutoreleasePoolPage
组成的,并且每一个 AutoreleasePoolPage
的大小都是4096
字节(16 进制 0x1000)
#define I386_PGBYTES 4096 #define PAGE_SIZE I386_PGBYTES
parent
和 child
就是用来构造双向链表的指针。自动释放池中的 AutoreleasePoolPage
是以 双向链表 的形式连接起来的:
假设我们的一个 AutoreleasePoolPage
被初始化在内存的 0x100816000 ~ 0x100817000
中,它在内存中的结构如下:
其中有 56 bit 用于存储 AutoreleasePoolPage 的成员变量,剩下的 0x100816038 ~ 0x100817000 都是用来存储 加入到自动释放池中的对象 。
begin() 和 end() 这两个类的实例方法帮助我们快速获取 0x100816038 ~ 0x100817000 这一范围的边界地址。
hiwat
和 depth
在文章中并不会进行介绍,因为它们并不影响整个自动释放池的实现,也不在关键方法的调用栈中。 到了这里,你可能想要知道 POOL_SENTINEL
到底是什么,还有它为什么在栈中。首先回答第一个问题:
POOL_SENTINEL
只是 nil
的别名。 定义如下:
#define POOL_SENTINEL nil在每个自动释放池初始化调用
objc_autoreleasePoolPush
的时候,都会把一个POOL_SENTINEL
push 到自动释放池的栈顶,并且返回这个POOL_SENTINEL
哨兵对象。int main(int argc, const char * argv[]) { { void * atautoreleasepoolobj = objc_autoreleasePoolPush(); // do whatever you want objc_autoreleasePoolPop(atautoreleasepoolobj); } return 0; }
上面的 atautoreleasepoolobj
就是一个 POOL_SENTINEL
。而当方法
objc_autoreleasePoolPop
调用时,就会向自动释放池中的对象发送 release
消息,直到POOL_SENTINEL
:
POOL_SENTINEL
,我们来重新回顾一下 objc_autoreleasePoolPush
方法:
void *objc_autoreleasePoolPush(void) { return AutoreleasePoolPage::push(); }它调用
AutoreleasePoolPage
的类方法 push
,也非常简单:
static inline void *push() { return autoreleaseFast(POOL_SENTINEL); }在这里会进入一个比较关键的方法
autoreleaseFast
,并传入哨兵对象 POOL_SENTINEL
:
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); } }
上述方法分三种情况选择不同的代码执行:
hotPage
并且当前 page
不满
page->add(obj)
方法将对象添加至 AutoreleasePoolPage
的栈中 hotPage
并且当前 page
已满
autoreleaseFullPage
初始化一个新的页 page->add(obj)
方法将对象添加至 AutoreleasePoolPage
的栈中 hotPage
autoreleaseNoPage
创建一个 hotPage
page->add(obj)
方法将对象添加至 AutoreleasePoolPage
的栈中 最后的都会调用 page->add(obj)
将对象添加到自动释放池中。hotPage
可以理解为当前正在使用的AutoreleasePoolPage
。 page->add添加对象。id *add(id obj)将对象添加到自动释放池页中:
id *add(id obj) { id *ret = next; *next = obj; next++; return ret; }
笔者对这个方法进行了处理,更方便理解。这个方法其实就是一个压栈的操作,将对象加入 AutoreleasePoolPage
然后移动栈顶的指针。autoreleaseFullPage
会在当前的hotPage
已满的时候调用:
static id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page) { do { if (page->child) page = page->child; else page = new AutoreleasePoolPage(page); } while (page->full()); setHotPage(page); return page->add(obj); }
它会从传入的 page
开始遍历整个双向链表,直到:
AutoreleasePoolPage
parent
创建一个新的 AutoreleasePoolPage
在查找到一个可以使用的 AutoreleasePoolPage
之后,会将该页面标记成 hotPage
,然后调动上面分析过的page->add
方法添加对象。 如果当前内存中不存在hotPage
,就会调用
autoreleaseNoPage
方法初始化一个AutoreleasePoolPage
:
static id *autoreleaseNoPage(id obj) { AutoreleasePoolPage *page = new AutoreleasePoolPage(nil); setHotPage(page); if (obj != POOL_SENTINEL) { page->add(POOL_SENTINEL); } return page->add(obj); }
既然当前内存中不存在 AutoreleasePoolPage
,就要 从头开始构建这个自动释放池的双向链表 ,也就是说,新的AutoreleasePoolPage
是没有parent
指针的。初始化之后,将当前页标记为
hotPage
,然后会先向这个 page
中添加一个POOL_SENTINEL
对象,来确保在pop
调用的时候,不会出现异常。最后,将
obj
添加到自动释放池中。
objc_autoreleasePoolPop
方法:void objc_autoreleasePoolPop(void *ctxt) { AutoreleasePoolPage::pop(ctxt); }
看起来传入任何一个指针都是可以的,但是在整个工程并没有发现传入其他对象的例子。不过在这个方法中 传入其它的指针也是可行的 ,会将自动释放池释放到相应的位置。我们一般都会在这个方法中传入一个哨兵对象POOL_SENTINEL
,如下图一样释放对象:
在继续分析这个方法之前做一个小测试,在 objc_autoreleasePoolPop
传入非哨兵对象,测试一下这个方法的行为。其具体测试方法查看这篇文章
objc_autoreleasePoolPop
方法的分析,也就是 AutoreleasePoolPage::pop
方法的调用:static inline void pop(void *token) { AutoreleasePoolPage *page = pageForPointer(token); id *stop = (id *)token; page->releaseUntil(stop); if (page->child) { if (page->lessThanHalfFull()) { page->child->kill(); } else if (page->child->child) { page->child->child->kill(); } } }
在这个方法中删除了大量无关的代码,以及对格式进行了调整。
该静态方法总共做了三件事情:
pageForPointer
获取当前 token
所在的 AutoreleasePoolPage
releaseUntil
方法释放 栈中的 对象,直到 stop
child
的 kill
方法 我到现在也不是很清楚为什么要根据当前页的不同状态 kill
掉不同 child
的页面。
if (page->lessThanHalfFull()) { page->child->kill(); } else if (page->child->child) { page->child->child->kill(); }
pageForPointer
方法主要是通过内存地址的操作,获取当前指针所在页的首地址:
static AutoreleasePoolPage *pageForPointer(const void *p) { return pageForPointer((uintptr_t)p); } static AutoreleasePoolPage *pageForPointer(uintptr_t p) { AutoreleasePoolPage *result; uintptr_t offset = p % SIZE; assert(offset >= sizeof(AutoreleasePoolPage)); result = (AutoreleasePoolPage *)(p - offset); result->fastcheck(); return result; }将指针与页面的大小,也就是 4096 取模,得到当前指针的偏移量,因为所有的
AutoreleasePoolPage
在内存中都是对齐的:p = 0x100816048 p % SIZE = 0x48 result = 0x100816000
而最后调用的方法 fastCheck()
用来检查当前的 result
是不是一个 AutoreleasePoolPage
。通过检查magic_t
结构体中的某个成员是否为0xA1A1A1A1
。
releaseUntil
方法的实现如下:
void releaseUntil(id *stop) { while (this->next != stop) { AutoreleasePoolPage *page = hotPage(); while (page->empty()) { page = page->parent; setHotPage(page); } page->unprotect(); id obj = *--page->next; memset((void*)page->next, SCRIBBLE, sizeof(*page->next)); page->protect(); if (obj != POOL_SENTINEL) { objc_release(obj); } } setHotPage(this); }
它的实现还是很容易的,用一个 while
循环持续释放 AutoreleasePoolPage
中的内容,直到next
指向了stop
。使用
memset
将内存的内容设置成 SCRIBBLE
,然后使用objc_release
释放对象。 到这里,没有分析的方法就只剩下kill
了,而它会将当前页面以及子页面全部删除:
void kill() { AutoreleasePoolPage *page = this; while (page->child) page = page->child; AutoreleasePoolPage *deathptr; do { deathptr = page; page = page->parent; if (page) { page->unprotect(); page->child = nil; page->protect(); } delete deathptr; } while (deathptr != this); }总结内容:
AutoreleasePoolPage
以双向链表的方式实现的 autorelease
方法时,会将对象加入 AutoreleasePoolPage
的栈中AutoreleasePoolPage::pop
方法会向栈中的对象发送 release
消息 __weak id reference = nil; - (void)viewDidLoad { [super viewDidLoad]; NSString *str = [NSString stringWithFormat:@"autoreleasePool"]; // str是一个autorelease对象,设置一个weak的引用来观察它 reference = str; NSLog(@"%@", reference); // Console: autoreleasePool } - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; NSLog(@"%@", reference); // Console: autoreleasePool } - (void)viewDidAppear:(BOOL)animated { [super viewDidAppear:animated]; NSLog(@"%@", reference); // Console: (null) }和:
- (void)viewDidLoad { [super viewDidLoad]; @autoreleasepool { NSString *str = [NSString stringWithFormat:@"autoreleasePool"]; } NSLog(@"%@", str); // Console: (null) }
总结:
既然autoreleasepool是一种延迟释放机制,当在autoreleasepool中有大量对象被创建而得不到及时释放会出现内存高峰现象。
autoreleasepool与autorelease的关系
参考文章:
http://blog.sunnyxx.com/2014/10/15/behind-autorelease/
http://www.tuicool.com/articles/ABvIjm2
标签:
原文地址:http://blog.csdn.net/shxwork/article/details/51437003