前言
在内存管理的学习中自动释放池的原理学习是必须的,作为一个合格的iOS开发者,必须要明白自动释放池的操作原理,这篇文章的目的就是探索自动释放池的底层原理。
准备工作
- Objc-818.2
1. 自动释放池
1.1 相关概念
- 如果在函数、方法的开始处将对象的引用计数
加1
,在函数、方法不需要该对象的时候将其引用计数减1
,这思想基本OK
。 - 有些函数、方法需要返回一个对象,而系统可能在该对象被返回之前,就已经销毁了对象。那么为了保证函数、方法返回的对象在被返回之前不被销毁,我们就要使用自动释放池进行
延迟销毁
(NSAutoreleasePool
)。 - 所谓
自动释放池
,是指它是一个存放对象的容器
(集合
),而自动释放池
会保证延迟销毁该池中所有的对象
。出于自动释放池
的考虑,所有的对象都应该添加到自动释放池
中,这样可以让自动释放池
在销毁之前,先销毁池中的所有对象。 -
autorelease
方法。该方法不会改变对象的引用计数
,只是将该对象添加到自动释放池
中。该方法会返回调用该方法的对象本身
。 - 当程序在
自动释放池
上下文中调用某个对象的autorelease
方法时,该方法只是将对象添加到自动释放池
中,当该自动释放池
释放时,自动释放池
会让池中所有的对象执行release
方法。 -
自动释放池的销毁
和其他普通对象相同,只要其引用计数为0
,系统就会自动销毁自动释放池对象
。系统会在调用NSAotoreleasePool
的dealloc
方法时回收该池中的所有对象
。 -
NSAutoreleasePool
还提供了一个drain
方法来销毁自动释放池
中的对象。与release
不同,release
会使自动释放池自身的引用计数变为0
,从而让系统回收NSAutoreleasePool
对象,在回收NSAutoreleasePool
对象之前,系统会回收该池中的所有对象。而drain
方法则只是回收池中的所有对象,并不会销毁自动释放池
。
1.2 运行逻辑
AutoReleasePool
是OC
的内存自动回收机制
,将加入到AutoReleasePool
中的变量release
时机延迟
。在正常情况下,创建的变量会在超出其作用域的时候release
,但是如果将变量加入AutoreleasePool
,那么release
将延迟执行,即使超出作用域也不会立即释放
,直到runloop休眠
或者超出AutoReleasePool作用域才会释放
。
自动释放池的运行机制:
- 程序启动到加载完成,主线程对应的
Runloop
处于休眠状态
,直到用户点击交互唤醒Runloop
- 用户每次交互都会启动一次
Runloop
用来处理用户的点击、交互事件 -
Runloop
被唤醒后,会自动创建AutoReleasePool
,并将所有延迟释放的对象添加到AutoReleasePool
- 在一次完整的
Runloop
执行结束前,会自动向AutoReleasePool
中的对象发送release
消息,然后销毁AutoReleasePool
注意:AutoreleasePool
和Runloop
的运行机制和关系,在后面讲解Runloop
时会详细说明。
1.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
,在viewWillAppear
中打印这个对象的时候,能够输出,说明此时对象还没有被释放。但是在viewDidAppear
中打印这个对象的时候就成了null
了,那么这个对象一定是在viewWillAppear
和viewDidAppear
方法之间的某个时候被释放了,并且是由于它所在的autoreleasepool
被release
的时候释放的。我们可以在lldb
调试中设置观察点
(watchpoint set v weakSrtingAutoRelease
),来查看对象的释放过程,如下:
在运行栈中可以发现,
weakSrtingAutoRelease
对象在自动释放池释放时
完成了释放
。
- 案例2
案例1看起来不够直接,那么我们来一个直接点的案例。代码中手动添加了一个@autoreleasepool
,在自动释放池内,weakSrtingAutoRelease
一直不会释放,而出了自动释放池就会释放。如下:
明显看出使用方式2
创建的对象weakSrtingAutoRelease
在自动释放池内都能够正常使用,出了自动释放池就会被释放,起到延迟释放
的效果。
但是使用方式1
创建的字符串weakSrting
,为什么在自动释放池内就释放了呢?他不会加入到自动释放池吗?带着疑问继续往下走!!
2. 自动释放池原理分析
2.1 原理初探
通过clang
查看自动释放池的实现原理,如下:
@autoreleasepool
在编译后变成了以下代码:
__AtAutoreleasePool __autoreleasepool;
main.cpp文件中全局搜索__AtAutoreleasePool
的定义,找到__AtAutoreleasePool
结构体的定义,如下:
struct __AtAutoreleasePool {
__AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
void * atautoreleasepoolobj;
};
该结构体提供了一个构造函数objc_autoreleasePoolPush
和一个析构函数objc_autoreleasePoolPop
。所以自动释放池在底层其实是一个结构体
,其通过objc_autoreleasePoolPush
完成自动释放池的创建
,objc_autoreleasePoolPop
来释放自动释放池
。
设置objc_autoreleasePoolPush
的符号断点,的确能够进入,汇编如下:
可以确定自动释放池其实现源码在我们最熟悉的
libobjc.A.dylib
库。那就非常的nice
了!
2.2 结构分析
下面通过源码进行分析。跟踪objc_autoreleasePoolPush
的方法实现,如下:
其调用了
objc_autoreleasePoolPush()
方法,继续跟踪代码:
在该方法的实现中,其调用了
AutoreleasePoolPage
的push
方法。那么AutoreleasePoolPage
的结构是怎么的呢?如下:
通过
AutoreleasePoolPage
类的注释可以得到以下关键信息:
-
一个线程
的自动释放池是一堆指针
- 每个指针要么是一个
要释放
的对象,要么是POOL_BOUNDARY
(自动释放池边界-哨兵对象
) -
堆栈
被分成一个双向链接
的页面列表
, 页面已添加对象并根据需要删除 - 线程本地存储指向新自动释放的热点页面对象被存储
AutoreleasePoolPage
继承于AutoreleasePoolPageData
,那么查看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
值为nil
-
child
指向⼦结点,最后⼀个结点的child
值为nil
-
depth
代表深度,从0
开始,往后递增1
-
hiwat
代表high water mark
最⼤⼊栈数量
标记
2.3 源码实现
objc_autoreleasePoolPush(void)
{
return AutoreleasePoolPage::push();
}
那么我们继续跟踪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()
方法所确定下来的值。
id * begin() {
return (id *) ((uint8_t *)this+sizeof(*this));
}
断点调试如下:
如果
页满
时,就会调用上面的autoreleaseFullPage
方法,见下面实现源码:
id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page)
{
// The hot page is full.
// Step to the next non-full page, adding a new page if necessary.
// Then add the object to that page.
ASSERT(page == hotPage());
ASSERT(page->full() || DebugPoolAllocation);
do {
if (page->child) page = page->child;
else page = new AutoreleasePoolPage(page);
} while (page->full());
setHotPage(page);
return page->add(obj);
}
do..while
循环找到最后一个page
,如果page
没有满,就将page
设置为hotPage
。如果page
已满,则会新建一个page
,也将page
设置为hotPage
。最后往page
中添加obj
。
综合上面的数据结构和源码实现,我们可以得出以下结论:
-
Autoreleasepool
是由多个AutoreleasePoolPage
以双向链表
的形式连接起来的 -
Autoreleasepool
的基本原理:在自动释放池创建的时候,会在当前的AutoreleasePoolPage
中设置一个标记位
(边界
),在此期间,当有对象调用autorelease
时,会把对象添加到AutoreleasePoolPage
中 - 如果当前页加
满了
,会初始化一个新页,然后用双向链表链接起来,并把初始化的一页设置为hotPage
,当自动释放池pop
时,从最下面依次往上pop
,调用每个对象的release
方法,直到遇到标志位
2.4 满页临界值
自动释放池一页能够存储多少个对象呢?如果能够打印输出自动释放池的数据,会更便于我们对自动释放池的了解。在源码中也提供了相关的打印数据结构的方法,如下:
void
_objc_autoreleasePoolPrint(void)
{
AutoreleasePoolPage::printAll();
}
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. 自动释放池注意点
3.1 对象release而非销毁
引入案例,如下:
当自动释放池结束的时候,仅仅是对存储在自动释放池中的对象发送
1
条release
消息,而不是销毁对象
。
3.2 自动释放池的嵌套
引入案例,如下:
通过该案例可以发现,自动释放池嵌套并不会影响数据结构,只是
多插入一个哨兵对象
。
3.3 哪些对象可以放入自动释放池
引入案例,如下:
-
MRC
环境
-
ARC
环境
总结: - 主动调用
autorelase
方法的,用alloc
,init
,copy
等方法创建的对象,这些我们自己持有的,我们想让他延迟释放,就调用autorelase
方法,这样在自动释放池出栈
的时候,对象就会释放掉
。 - 对于那种
stringWithFormt
这种从名字来看,没有被调用者持有的情况,要么是自动加到自动释放池里
的,要么是常量字符串
,不用引用计数来管理
。