iOS内存管理——内存管理(自动释放池AutoreleasePool)
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
值为nil
child
指向⼦结点,最后⼀个结点的child
值为nil
depth
代表深度,从0
开始,往后递增1
hiwat
代表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这种从名字来看,没有被调用者持有的情况,要么是自动加到自动释放池里的,要么是常量字符串,不用引用计数来管理。
转载自:https://juejin.cn/post/7010726670181253127