1.自动释放池
1.相关概念
-
如果在函数、方法的开始处将对象的引用计数加
1,在函数、方法不需要该对象的时候将其引用计数减1,这思想基本OK。 -
问题:有些函数、方法需要返回一个对象,而系统可能在该对象被返回之前,就已经销毁了对象。那么为了保证函数、方法返回的对象在被返回之前不被销毁,我们就要使用自动释放池进行延迟销毁(
NSAutoreleasePool)。 -
所谓自动释放池,是指它是一个存放对象的容器(集合),而自动释放池会保证延迟销毁该池中所有的对象。出于自动释放池的考虑,所有的对象都应该添加到自动释放池中,这样可以让自动释放池在销毁之前,先销毁池中的所有对象。
-
autorelease方法。该方法不会改变对象的引用计数,只是将该对象添加到自动释放池中。该方法会返回调用该方法的对象本身。 -
当程序在自动释放池上下文中调用某个对象的
autorelease方法时,该方法只是将对象添加到自动释放池中,当该自动释放池释放时,自动释放池会让池中所有的对象执行release方法。 -
自动释放池的销毁和其他普通对象相同,只要其引用计数为0,系统就会自动销毁自动释放池对象。系统会在调用NSAotoreleasePool的dealloc方法时回收该池中的所有对象。 -
NSAutoreleasePool还提供了一个drain方法来销毁自动释放池中的对象。与release不同,release会使自动释放池自身的引用计数变为0,从而让系统回收NSAutoreleasePool对象,在回收NSAutoreleasePool对象之前,系统会回收该池中的所有对象。而drain方法则只是回收池中的所有对象,并不会销毁自动释放池。
2.运行逻辑
AutoReleasePool是OC的内存自动回收机制,将加入到AutoReleasePool中的变量release时机延迟。在正常情况下,创建的变量会在超出其作用域的时候release,但是如果将变量加入AutoreleasePool,那么release将延迟执行,即使超出作用域也不会立即释放,直到runloop休眠或者超出AutoReleasePool作用域才会释放。
自动释放池的运行机制:
- 程序启动到加载完成,主线程对应的
Runloop处于休眠状态,直到用户点击交互唤醒Runloop - 用户每次交互都会启动一次
Runloop用来处理用户的点击、交互事件 Runloop被唤醒后,会自动创建AutoReleasePool,并将所有延迟释放的对象添加到AutoReleasePool- 在一次完整的
Runloop执行结束前,会自动向AutoReleasePool中的对象发送release消息,然后销毁AutoReleasePool
AutoreleasePool和Runloop的运行机制和关系,在后面讲解Runloop时会详细说明。
3.使用效果分析
下面通过一个案例来说明自动释放池的作用。我们常用以下两种方式创建字符串:
// 方式1
NSString * string1 = [[NSString alloc] initWithFormat:@"hello world..."];
// 方式2
NSString * string2 = [NSString stringWithFormat:@"hello world auto relase..."];
那么这两种方式有什么区别呢?我们通过汇编了解其内部实现原理:
-
方式1
NSString * string1 = [[NSString alloc] initWithFormat:@"hello world..."];使用
alloc出来的方式,字符串在调用release的时候被回收(假设该字符串没有被其他东西引用,变量会在超出其作用域的时候release)。 -
方式2
NSString * string2 = [NSString stringWithFormat:@"hello world auto relase..."];使用
stringWith的方式,字符串在api内部会被设置成autorelease,不用手动释放,系统会回收,因此将会在最近的一个自动释放池drain或release时被回收。
下面通过一个案例来深入了解自动释放池的作用。案例中,使用两种方式创建了字符串,并且把字符串赋值给__weak修饰的成员变量。
-
场景1
__weak NSString *weakSrting; __weak NSString *weakSrtingAutoRelease; @implementation ViewController - (void)createStringFunc { // 方式1 NSString * string1 = [[NSString alloc] initWithFormat:@"hello world..."]; weakSrting = string1; // 方式2 NSString * string2 = [NSString stringWithFormat:@"hello world auto relase..."]; weakSrtingAutoRelease = string2; } - (void)viewDidLoad { [super viewDidLoad]; [self createStringFunc]; NSLog(@"weakSrting: %@", weakSrting); NSLog(@"weakSrtingAutoRelease: %@", weakSrtingAutoRelease); } - (void)viewWillAppear:(BOOL)animated { NSLog(@"view will appear weakSrting: %@", weakSrting); NSLog(@"view will appear weakSrtingAutoRelease: %@", weakSrtingAutoRelease); } - (void) viewDidAppear:(BOOL)animated { NSLog(@"view did appear weakSrting: %@", weakSrting); NSLog(@"view did appear weakSrtingAutoRelease: %@", weakSrtingAutoRelease); }查看运行结果:
使用
方式1创建的字符串weakSrting,在createStringFunc方法执行完成后就会释放(作用域结束),弱引用weakSrting也会释放掉。所以weakSrting打印结果都是空。使用
方式2创建的对象weakSrtingAutoRelease,这个对象被系统自动添加到了当前的autoreleasepool中,起到了延迟释放的效果。这个对象是一个autoreleased对象,autoreleased对象是被添加到了当前最近的autoreleasepool中的,只有当这个autoreleasepool自身drain的时候,autoreleasepool中的autoreleased对象才会被release。对象
weakSrtingAutoRelease,在viewDidAppear中打印这个对象的时候,能够输出,说明此时对象还没有被释放。那么这个对象一定是在viewWillAppear和viewDidAppear方法之间的某个时候被释放了,并且是由于它所在的autoreleasepool被release的时候释放的。我们可以在lldb调试中设置观察点(watchpoint set v weakSrtingAutoRelease),来查看对象的释放过程:在运行栈中可以发现,
weakSrtingAutoRelease对象在自动释放池释放时完成了释放。 -
场景2(ARC)
此种方案更直观一些,代码中手动添加了一个
@autoreleasepool,在自动释放池内,weakSrtingAutoRelease一直不会释放,而出了自动释放池就会释放。见下图:但是使用
方式2创建的对象weakSrtingAutoRelease在自动释放池内都能够正常使用,出了自动释放池就会被释放,起到延迟释放的效果。但是使用
方式1创建的字符串weakSrting,为什么在自动释放池内就释放了呢?他不会加入到自动释放池吗?这个问题下面会说明!!!
2.自动释放池原理分析
1.原理初探
下面通过clang查看自动释放池的实现原理,见下图:
@autoreleasepool在编译后变成了以下代码:
__AtAutoreleasePool __autoreleasepool;
全局搜索__AtAutoreleasePool的定义,找到__AtAutoreleasePool结构体的定义:
struct __AtAutoreleasePool {
__AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
void * atautoreleasepoolobj;
};
该结构体提供了一个构造函数objc_autoreleasePoolPush和一个析构函数objc_autoreleasePoolPop。所以自动释放池在底层其实是一个结构体,其通过objc_autoreleasePoolPush完成自动释放池的创建,objc_autoreleasePoolPop来释放自动释放池。
通过设置符号断点,查看汇编,可以确定自动释放池其实现源码在我们最熟悉的libobjc.A.dylib库。
2.结构分析
下面通过源码进行分析。跟踪objc_autoreleasePoolPush的方法实现,见下图:
其调用了objc_autoreleasePoolPush()方法,继续跟踪代码:
在该方法的实现中,其调用了AutoreleasePoolPage的push方法。那么AutoreleasePoolPage的结构是怎么的呢?见下图:
通过AutoreleasePoolPage类的注释可以得到以下关键信息:
- 一个线程的自动释放池是一堆指针
- 每个指针要么是一个要释放的对象,要么是
POOL_BOUNDARY(自动释放池边界-哨兵对象) - 堆栈被分成一个
双向链接的页面列表, 页面已添加对象并根据需要删除 - 线程本地存储指向新自动释放的热点页面对象被存储
上面的注释信息如何理解呢?查看AutoreleasePoolPageData实现:
class AutoreleasePoolPage;
struct AutoreleasePoolPageData
{
#if SUPPORT_AUTORELEASEPOOL_DEDUP_PTRS
struct AutoreleasePoolEntry {
uintptr_t ptr: 48;
uintptr_t count: 16;
static const uintptr_t maxCount = 65535; // 2^16 - 1
};
static_assert((AutoreleasePoolEntry){ .ptr = MACH_VM_MAX_ADDRESS }.ptr == MACH_VM_MAX_ADDRESS, "MACH_VM_MAX_ADDRESS doesn't fit into AutoreleasePoolEntry::ptr!");
#endif
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⽤来校验AutoreleasePoolPage的结构是否完整;next指向最新添加的autoreleased对象的下⼀个位置,初始化时指向begin()thread指向当前线程parent指向⽗结点,第⼀个结点的parent值为nilchild指向⼦结点,最后⼀个结点的child值为nildepth代表深度,从0开始,往后递增1hiwat代表high water mark最⼤⼊栈数量标记
3.源码实现
跟踪push的实现源码:
static inline void *push()
{
id *dest;
if (slowpath(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;
}
在非debug模式下首先调用autoreleaseFast方法,并传入边界对象(哨兵对象)。查看autoreleaseFast实现源码:
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,如果不为空且没有满,则会向该页中添加obj;如果该页已满,则调用autoreleaseFullPage方法;如果当前hotPage不存在,也就是没有page,则调用autoreleaseNoPage方法。autoreleaseNoPage实现源码如下:
在完成AutoreleasePoolPage创建后,首先添加哨兵对象,然后在加入obj。 首先查看AutoreleasePoolPage构造函数,见下图:
通过调用AutoreleasePoolPageData的构造函数实现初始化,并确定页之间的链表关系。通过上面的结构我们可以确定AutoreleasePoolPageData属性占56个字节。见下图:
因为页中next字段用于设置存储obj的位置,那么因为每个页自身有一些属性需要占用一部分空间,所以next的起始值是page首地址平移56个字节,也就是构造函数中begin()方法所确定下来的值。
如果页满时,就会调用上面的autoreleaseFullPage方法,见下面实现源码:
跟踪实现代码,会发现其返回的是AutoreleasePoolPage,综合上面的数据结构和源码实现,我们可以得出以下结论:
Autoreleasepool是由多个AutoreleasePoolPage以双向链表的形式连接起来的Autoreleasepool的基本原理:在自动释放池创建的时候,会在当前的AutoreleasePoolPage中设置一个标记位(边界),在此期间,当有对象调用autorelease时,会把对象添加到AutoreleasePoolPage中- 若当前页加满了,会初始化一个新页,然后用双向链表链接起来,并把初始化的一页设置为
hotPage,当自动释放池pop时,从最下面依次往上pop,调用每个对象的release方法,直到遇到标志位
4.满页临界值
自动释放池一页能够存储多少个对象呢?如果能够打印输出自动释放池的数据,会更便于我们对自动释放池的了解。在源码中也提供了相关的打印数据结构的方法:
void
_objc_autoreleasePoolPrint(void)
{
AutoreleasePoolPage::printAll();
}
__attribute__((noinline, cold))
static void printAll()
{
_objc_inform("##############");
_objc_inform("AUTORELEASE POOLS for thread %p", objc_thread_self());
AutoreleasePoolPage *page;
ptrdiff_t objects = 0;
for (page = coldPage(); page; page = page->child) {
objects += page->next - page->begin();
}
_objc_inform("%llu releases pending.", (unsigned long long)objects);
if (haveEmptyPoolPlaceholder()) {
_objc_inform("[%p] ................ PAGE (placeholder)", EMPTY_POOL_PLACEHOLDER);
_objc_inform("[%p] ################ POOL (placeholder)", EMPTY_POOL_PLACEHOLDER);
}
else {
for (page = coldPage(); page; page = page->child) {
page->print();
}
}
_objc_inform("##############");
}
引入下面就的案例查看其内部存储结构:
通过上面的输出可以发现,该自动释放池的起始页是0x10380a000,地址平移56个字节后放入的是哨兵对象,哨兵对象地址为0x10380a038,紧接着放入4个对象。那么一页能放多少呢?源码中也有定义:
static size_t const SIZE =
#if PROTECT_AUTORELEASEPOOL
PAGE_MAX_SIZE; // must be multiple of vm page size
#else
PAGE_MIN_SIZE; // size and alignment, power of 2
#endif
#define PAGE_MIN_SHIFT 12
#define PAGE_MIN_SIZE (1 << PAGE_MIN_SHIFT)
通过解读源码可以确定,其大小为1<<12,也即是4096,而每页自身属性的占用56个字节,同时第一页需要一个哨兵对象8个字节,所以首页最多可以放(4096 - 56 - 8) / 8 = 504个对象。验证一下:
通过输出自动释放池的数据结构可以发现,当放入505个对象时,会新开辟一页,并且第二页中只有一个对象。(哨兵对象只会放在第一页)所以第一页最多可以放504个对象,之后每页可以存储505个对象。
3.自动释放池注意点
1.对象release而非销毁
先看下面的案例:
当自动释放池结束的时候,仅仅是对存储在自动释放池中的对象发送1条release消息,而不是销毁对象。
2.自动释放池的嵌套
自动释放池可以嵌套!
通过该案例可以发现,自动释放池嵌套并不会影响数据结构,只是多插入一个哨兵对象。
3.哪些对象可以放入自动释放池
依然通过案例进行分析。
-
MRC环境 -
ARC环境
1.主动调用autorelase方法的,用alloc, init,copy等方法创建的对象,这些我们自己持有的,我们想让他延迟释放,就调用autorelase方法,这样在自动释放池出栈的时候,对象就会释放掉。
2.对于那种stringWithFormt这种从名字来看,没有被调用者持有的情况,要么是自动加到自动释放池里的,要么是常量字符串,不用引用计数来管理。