一、Autorelease 简介
1.基本介绍
AutoreleasePool(自动释放池)
是 OC 中的一种内存管理机制,它持有释放池里的对象的所有权,在自动释放池销毁时,统一给所有对象发送一次 release
消息。通过这个机制,可以延迟对象的释放。
UIImage *img = [UIImage imageNamed:@"xxxx.png"];
这个 UIImage
对象是在类方法 imageNamed
里创建完后再返回,对象的所有权归方法持有,如果不延迟释放,在方法结束时对象就被释放了,返回的就为 nil
。因此,需要将对象先加入 AutoreleasePool
,所有权归自动释放池持有,只有自动释放池销毁时才释放,这样 UIImage
对象才能在方法结束后正常返回。
2.创建
(1)手动创建
通常使用 @autoreleasepool {}
代码块来手动创建一个自动释放池:
@autoreleasepool {
//这里创建自动释放的对象,创建的对象会被加入到AutoreleasePool对象里
... ...
}
这个代码块等价于:
{
//创建一个AutoreleasePool对象
__AtAutoreleasePool *atautoreleasepoolobj = objc_autoreleasePoolPush();
//这里创建自动释放的对象,创建的对象会被加入到AutoreleasePool对象里
... ...
//给所有自动释放的对象发送一次release消息,并销毁AutoreleasePool对象
objc_autoreleasePoolPop(atautoreleasepoolobj)
}
`{}`表示AutoreleasePool对象的作用域
(2)在 Runloop
中的创建和销毁
通常情况下,在平时开发中不需要手动创建自动释放池,因为 Runloop
会自动创建和销毁 AutoreleasePool
对象。
App启动后,系统在主线程 RunLoop
里注册了两个 Observer
,其回调都是 _wrapRunLoopWithAutoreleasePoolHandler()
。
第一个 Observer
监视一个事件:
-
Entry
(即将进入 Loop):调用objc_autoreleasePoolPush
来创建自动释放池。
第二个 Observer
监视了两个事件:
-
Before waiting
(准备进入休眠):先调用objc_autoreleasePoolPop
销毁旧的自动释放池,再调用objc_autoreleasePoolPush
创建一个新的自动释放池。 -
Exit
(即将退出 Loop):调用objc_autoreleasePoolPop
销毁自动释放池。
第一个 Observe
的 order 是 -2147483647,优先级最高,保证创建释放池发生在其他所有回调之前。
第二个 Observer
的 order 是 2147483647,优先级最低,保证销毁自动释放池发生在其他所有回调之后。
在一个 RunLoop
事件开始的时候会自动创建一个 AutoreleasePool
,在事件结束时再自动销毁。上面举例的 imageNamed
方法内部创建的对象也是加入到主线程 RunLoop
创建的 AutoreleasePool
中实现延迟释放的。因此,通常在开发中不需要开发者自己创建 AutoreleasePool
。
(3)手动创建 AutoreleasePool
的场景
虽然 Runloop
会自动创建和销毁自动释放池,但在有些情况下还是需要手动创建 AutoreleasePool
。苹果官方文档建议在下面这三种情况下可能需要开发者创建自动释放池:
- 编写不基于
UI
框架的程序,例如命令行工具。
这一点的原因不是特别清楚,猜测是不基于 UI
框架的程序,可能不响应用户事件,导致不自动创建和销毁自动释放池。
- 编写一个创建大量临时对象的循环。
在循环内使用自动释放池块可以在下一次迭代之前释放这些对象,有助于减少应用程序的最大内存占用,即降低内存峰值。
- 编写非
Cocoa
程序时创建子线程。
Cocoa
程序中的每个线程都维护自己的自动释放池块堆栈。而编写一个非 Cocoa
程序,比如 Foundation-only program
,这时如果创建了子线程,若不手动创建自动释放池,自动释放的对象将会堆积得不到释放,导致内存泄漏。
第二种情况举例:
//情况一:循环内不使用AutoreleasePool
for (int i = 0; i<1000000; i++) {
NSString *string = [NSString stringWithFormat:@"%@", @"0123456789"];
NSLog(@" ==== %p", string);
}
//情况二:循环内使用AutoreleasePool
for (int i = 0; i<1000000; i++) {
@autoreleasepool {
NSString *string = [NSString stringWithFormat:@"%@", @"0123456789"];
NSLog(@" ==== %p", string);
}
}
分别运行上面两种情况可以看到,在循环过程中,第一种情况的内存占用一直在增加,第二种情况的内存不会增加。这是因为:
情况一:循环过程中,创建的 NSString
对象一直在堆积,只有在循环结束才一起释放,所以内存一直在增加。
情况二:每一次迭代中都会创建并销毁一个 AutoreleasePool
,而每一次创建的 NSString
对象都会加入到 AutoreleasePool
中,所以在每次 AutoreleasePool
销毁时,NSString
对象就会被释放,这样内存就不会增加。
这个场景中 AutoreleasePool
是通过立即释放对象来降低内存峰值,而前面又说自动释放池用来延迟对象的释放,这两者其实不矛盾,本质是一样的,都是在自动释放池销毁时调用 objc_autoreleasePoolPop
来释放池中的对象。只不过调用的时机不同,这里的 @autoreleasepool {}
是在超出自己的作用域时就调用函数来销毁,而前面的是在 Runloop
休眠或退出时才调用函数来销毁,所以调用的时机不同,才会实现立即或者延迟释放的目的。
3.哪些对象可以被添加到自动释放池
在 MRC 模式下,只要给对象发送 autorelease
消息,这个对象就会被添加到自动释放池。
但在 ARC 模式下,是由编译器自动给对象发送 autorelease
消息,且不会给所有的对象都发送,只会给被编译器识别为自动释放的对象发送。一般来说,使用类方法(工厂方法)实例化的对象才是自动释放的对象,才能被添加到自动释放池,而使用 new
、alloc
、copy
关键字生成的对象和 retain
了的对象,不会被添加到自动释放池中。
以 UIImage
对象为例:
for (int i = 0; i<1000000; i++) {
@autoreleasepool {
//1.自动释放的对象,需要被添加到自动释放池中
UIImage *image = [UIImage imageNamed:@"test.png"];
//2.非自动释放的对象,不能被添加到自动释放池中
UIImage *image = [[UIImage alloc] init];
NSLog(@" ==== image = %p", image);
}
}
二、原理分析
自动释放池是一个栈的结构,被划分成了一个以 page
为结点的双向链表,根据需要添加和删除页面。每一页的大小为 4096 字节,里面存储着指向自动释放的对象或者 pool_boundary
哨兵的指针。hot page
表示当前活跃的页,存储新添加的自动释放的对象。
使用 @autoreleasePool
代码块可以创建一个自动释放池,通过 Clang
编译后发现底层实现如下:
{
//创建一个AutoreleasePool对象
__AtAutoreleasePool *atautoreleasepoolobj = objc_autoreleasePoolPush();
//这里创建自动释放的对象,创建的对象会被加入到AutoreleasePool对象里
... ...
//给所有自动释放的对象发送一次release消息,并销毁AutoreleasePool对象
objc_autoreleasePoolPop(atautoreleasepoolobj)
}
因此,单个 Autoreleasepool
的运行过程可以简单地理解为 objc_autoreleasePoolPush
、[对象 autorelease]
和 objc_autoreleasePoolPop
这三个过程。
1. push
操作
objc_autoreleasePoolPush
函数在底层是调用 AutoreleasePoolPage
的 push
函数。在源码中断点调试可知,push
函数的调用链为:push
-> autoreleaseFast(POOL_BOUNDARY)
-> autoreleaseNoPage(POOL_BOUNDARY)
。
(1)push
static inline void *push()
{
id *dest;
//这个判断不知道哪个实际场景可以触发,运行后都是false。源码注释:halt when autorelease pools are popped out of order, and allow heap debuggers to track autorelease pools
//老版本源码里没有这个判断,直接走 dest = autoreleaseFast(POOL_BOUNDARY)
if (slowpath(DebugPoolAllocation)) {
//这里基本不会进,而且全局搜索发现autoreleaseNewPage函数只在这里有调用
// 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);
//返回的dest就是后面执行pop函数的入参
return dest;
}
push
里直接调用 autoreleaseFast(POOL_BOUNDARY)
函数,返回的 dest
即为后面执行 pop
函数的入参,本质是哨兵地址。
(2)autoreleaseFast(POOL_BOUNDARY)
static inline id *autoreleaseFast(id obj)
{
//获取当前操作的page,hot page
AutoreleasePoolPage *page = hotPage();
//判断当前页
if (page && !page->full()) {
//如果page存在并且没有满
return page->add(obj);
} else if (page) {
//如果page存在并且已经满了
return autoreleaseFullPage(obj, page);
} else {
//如果page不存在
return autoreleaseNoPage(obj);
}
}
这个方法里先获取当前操作的 page
,然后进行判断:
如果当前页存在,并且没有满,就调用 add
函数压栈对象。
如果当前也存在且已经满了,就调用 autoreleaseFullPage
函数创建新页面,并压栈对象。
如果当前页不存在,即自动释放池为 nil
或者为 empty
(空的),就调用 autoreleaseNoPage
函数来设置空白占位符,或者创建第一页。
这里 push
操作调用的 autoreleaseFast
,会执行 autoreleaseNoPage
函数,入参为 POOL_BOUNDARY
。
(3)autoreleaseNoPage(POOL_BOUNDARY)
static __attribute__((noinline))
id *autoreleaseNoPage(id obj)
{
//"No page"表示还没创建自动释放池,或者创建了一个空内容的释放池。
// "No page" could mean no pool has been pushed
// or an empty placeholder pool has been pushed and has no contents yet
ASSERT(!hotPage());
//是否需要压栈哨兵
bool pushExtraBoundary = false;
//若设置了空白占位符,则需要压栈哨兵
if (haveEmptyPoolPlaceholder()) {
// We are pushing a second pool over the empty placeholder pool
// or pushing the first object into the empty placeholder pool.
// Before doing that, push a pool boundary on behalf of the pool
// that is currently represented by the empty placeholder.
pushExtraBoundary = true;
}
//若当前obj不是哨兵,且释放池为nil则报错
else if (obj != POOL_BOUNDARY && DebugMissingPools) {
// We are pushing an object with no pool in place,
// and no-pool debugging was requested by environment.
_objc_inform("MISSING POOLS: (%p) Object %p of class %s "
"autoreleased with no pool in place - "
"just leaking - break on "
"objc_autoreleaseNoPool() to debug",
objc_thread_self(), (void*)obj, object_getClassName(obj));
objc_autoreleaseNoPool(obj);
return nil;
}
//若当前obj为哨兵,就设置空白占位符并返回。只有push操作时当前obj才是哨兵
else if (obj == POOL_BOUNDARY && !DebugPoolAllocation) {
//push操作会走到这里,然后返回
// We are pushing a pool with no pool in place,
// and alloc-per-pool debugging was not requested.
// Install and return the empty pool placeholder.
return setEmptyPoolPlaceholder();
}
//从这里开始,下面就是第一次autorelease操作处理的事项
// We are pushing an object or a non-placeholder'd pool.
// Install the first page.
//压栈第一个第一个对象时,需要创建第一页
AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
//将第一页设置为hot page
setHotPage(page);
// Push a boundary on behalf of the previously-placeholder'd pool.
//压栈第一个对象时,上面判断已经设置了空白占位符,所以需要先压栈哨兵
if (pushExtraBoundary) {
page->add(POOL_BOUNDARY);
}
// Push the requested object or pool.
//压栈对象并返回
return page->add(obj);
}
autoreleaseNoPage
函数在 push
操作和第一次 autorelease
操作时会调用。在 push
操作时,会在这里调用 setEmptyPoolPlaceholder
函数来设置一个空白占位符,表示已创建了一个自动释放池。
//设置空白占位符
static inline id* setEmptyPoolPlaceholder()
{
ASSERT(tls_get_direct(key) == nil);
//通过"key-value"形式将空白占位符存储在tls中,并返回空白占位符的地址
//tls为当前线程的本地存储,用于保存当前线程的一些信息
tls_set_direct(key, (void *)EMPTY_POOL_PLACEHOLDER);
return EMPTY_POOL_PLACEHOLDER;
}
//判断是否设置空白占位符
static inline bool haveEmptyPoolPlaceholder()
{
//通过key-value获取值来判断
id *tls = (id *)tls_get_direct(key);
return (tls == EMPTY_POOL_PLACEHOLDER);
}
//自动占位符,地址为 0x1
# define EMPTY_POOL_PLACEHOLDER ((id*)1)
//key值
static pthread_key_t const key = AUTORELEASE_POOL_KEY;
# define AUTORELEASE_POOL_KEY ((tls_key_t)__PTK_FRAMEWORK_OBJC_KEY3)
因此,push
操作本质上是设置一个空白占位符,地址为 0x1
,并存储在当前线程的 tls
中,通过这个空白占位符来判断自动释放池是否已创建。在 push
过程中,既没有创建 page
,也没有压栈哨兵。
2. autorelease
操作
给对象发送 autorelease
消息即可加入到自动释放池,autorelease
方法在底层是调用 AutoreleasePoolPage
的 autorelease
函数,入参为对象本身。
inline id
objc_object::rootAutorelease()
{
//若是TaggedPointer对象,直接返回,不处理
if (isTaggedPointer()) return (id)this;
if (prepareOptimizedReturn(ReturnAtPlus1)) return (id)this;
return rootAutorelease2();
}
__attribute__((noinline,used))
id
objc_object::rootAutorelease2()
{
ASSERT(!isTaggedPointer());
return AutoreleasePoolPage::autorelease((id)this);
}
//autorelease操作
static inline id autorelease(id obj)
{
ASSERT(obj);
ASSERT(!obj->isTaggedPointer());
//压栈对象
id *dest __unused = autoreleaseFast(obj);
ASSERT(!dest || dest == EMPTY_POOL_PLACEHOLDER || *dest == obj);
return obj;
}
从代码中可知,若对象是 TaggedPointer
类型,则不能被添加到自动释放池。 autorelease
操作实际上是通过调用 autoreleaseFast
函数来压栈对象,实现逻辑为(源码上面已附):
若 hot page
不存在,即压栈的是第一个对象,此时是空白的自动释放池,就调用 autoreleaseNoPage
函数来创建第一页 page
,并设为 hot page
,先压栈哨兵,然后压栈对象。
若 hot page
存在且没满时,就调用 add
函数来压栈对象。
若 hot page
存在且已满,就调用 autoreleaseFullPage
函数来新建一页 page
,再压栈对象。
(1)hotPage
//设置hot page
static inline void setHotPage(AutoreleasePoolPage *page)
{
if (page) page->fastcheck();
//通过key-value形式,将当前page存储在tls中,即为hot page
tls_set_direct(key, (void *)page);
}
//获取hot page
static inline AutoreleasePoolPage *hotPage()
{
//从当前线程的tls中通过key获取到page,即为hot page
AutoreleasePoolPage *result = (AutoreleasePoolPage *)
tls_get_direct(key);
if ((id *)result == EMPTY_POOL_PLACEHOLDER) return nil;
if (result) result->fastcheck();
return result;
}
hot page
和上面的空白占位符一样,存储在当前线程的 tls
中,通过 key
来读写。
(2)add
id *add(id obj)
{
ASSERT(!full());
unprotect();
id *ret = next; // faster than `return next-1` because of aliasing
//先从next指针指向的地址里开始存放obj的地址,为8字节
//再将next指针像高地址偏移8字节,指向的地址用于存放下一次添加的obj的地址
*next++ = obj;
protect();
return ret;
}
add(obj)
实际上是先从当前 next
指针指向的地址里开始存放 obj
的地址,为 8 字节大小,再将 next
指针像高地址偏移 8 字节,新指向的地址用于存放下一次添加的 obj
的地址。当压栈第一个对象时,需要先压栈哨兵。
//在第一页里先压栈哨兵
page->add(POOL_BOUNDARY);
# define POOL_BOUNDARY nil
POOL_BOUNDARY
为 nil
,说明压栈哨兵其实只是将 next
指针偏移8字节,从新地址上开始存放对象地址。因此,哨兵其实是自动释放池中对象的边界,而这个边界地址就是在后面执行 pop
操作时的入参。
(3)autoreleaseFullPage
压栈对象时,若 hot page
满了,就调用 autoreleaseFullPage
函数来新建 page
,再压栈。
static __attribute__((noinline))
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);
//这个函数传进来的已经是hot page
//do-while循环获取没满的page
do {
//如果存在子页面,就用子页面替换当前page来判断
if (page->child) page = page->child;
//如果没有子页面,而当前page已满,则新建页面
else page = new AutoreleasePoolPage(page);
} while (page->full()); //判断page是否已满
//将page设置为hot page
setHotPage(page);
//压栈对象
return page->add(obj);
}
//判断page是否已满
bool full() {
//通过next的值和page的结束地址来判断
return next == end();
}
//page的end地址
id * end() {
//this为当前page的首地址,通过首地址加上4096字节,就可得出当前page的结束地址
return (id *) ((uint8_t *)this+SIZE);
}
//SIZE即为每页AutoreleasePoolPage的大小,为4096字节
static size_t const SIZE = 4096
因此,压栈对象时,若 hot page
已满,通过 do-while
循环来判断每一个子页面是否已满,若没满,则将当前子页面设为 hot page
,若都满了,就新建一个页面并设为 hot page
,再压栈对象。判断 page
是否已满,是通过将 page
的结束地址和 next
指针对比,若相等,则表示 page
已满。
3.pop
操作
pop
操作是向自动释放池里的每个对象发送一次 release
消息,然后销毁掉释放池里的页面。objc_autoreleasePoolPop
函数在底层是调用 AutoreleasePoolPage
的 pop
函数,入参为哨兵地址。
static inline void
pop(void *token)
{
//传进来的token即为哨兵地址
AutoreleasePoolPage *page;
id *stop;
//如果token为空白占位符
if (token == (void*)EMPTY_POOL_PLACEHOLDER) {
// Popping the top-level placeholder pool.
//获取hot page
page = hotPage();
if (!page) {
// Pool was never used. Clear the placeholder.
//如果page不存在,说明这个自动释放池没有压栈任何对象,直接清除占位符
//hotPage和空白占位符在tls中存储都是同一个key,所以可以清空
return setHotPage(nil);
}
// Pool was used. Pop its contents normally.
// Pool pages remain allocated for re-use as usual.
//容错处理,hot page存在,而哨兵地址异常,则需要更正
//如果页面存在,获取到第一页
page = coldPage();
//将token的值设置为第一页的哨兵地址
token = page->begin();
} else {
//获取token所在的页
page = pageForPointer(token);
}
//将token赋值给stop,则stop此时就是哨兵地址
stop = (id *)token;
//容错判断,若stop指向的不是哨兵地址
if (*stop != POOL_BOUNDARY) {
if (stop == page->begin() && !page->parent) {
//若stop指向当前页的begin(),且当前页没有父页面,说明当前也是第一页,stop指向的就是哨兵地址,不做任何处理
// 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 {
//若stop指向当前页的begin(),而当前页有父页面,说明stop指向的不是哨兵地址,则报异常并返回
// Error. For bincompat purposes this is not
// fatal in executables built with old SDKs.
return badPop(token);
}
}
//这个判断基本为false
if (slowpath(PrintPoolHiwat || DebugPoolAllocation || DebugMissingPools)) {
return popPageDebug(token, page, stop);
}
//出栈页
return popPage<false>(token, page, stop);
}
pop
操作中传进来的 token
即为哨兵地址,这里主要处理两件事:
如果当前是只有空白占位符的自动释放池,则清除掉空白符,然后返回,这个释放池也就被销毁了。
如果不是空白的自动释放池,先确保 token
为哨兵地址,然后获取到 token
所在的页 page
,并设置结束位 stop
(也是哨兵地址),最后调用 popPage
出栈页,完成 pop
操作。
也就是说,如果当前是空白的自动释放池, pop
操作就会清除掉空白占位符,销毁释放池;如果不是空白的自动释放池,pop
操作是通过调用 popPage
函数来出栈页,这个函数的三个入参:token
和 stop
均为哨兵地址,page
为当前哨兵所在的页。如果自动释放池没有嵌套,page
就为自动释放池的第一页,如果有嵌套,每个子 pool
都会有一个哨兵,page
就为当前 pool
哨兵所在的最外层 pool
中的页。
(1)coldPage
static inline AutoreleasePoolPage *coldPage()
{
//获取到当前页
AutoreleasePoolPage *result = hotPage();
//while循环获取到自动释放池的第一页
if (result) {
while (result->parent) {
result = result->parent;
result->fastcheck();
}
}
return result;
}
这个函数其实就是通过 hotPage
来获取到自动释放池的第一页。
(2)begin
id * begin() {
//当前页的首地址 + 自身成员占用的内存(56字节),即为begin的地址
return (id *) ((uint8_t *)this+sizeof(*this));
}
页面的首地址加上自身成员占用的 56 字节大小,就是 begin
位置。第一页,begin
位置为哨兵的起始地址,其它页则为该页第一个对象的起始地址。
(3)pageForPointer
static AutoreleasePoolPage *pageForPointer(const void *p)
{
//p为传进来的token,即哨兵地址
return pageForPointer((uintptr_t)p);
}
static AutoreleasePoolPage *pageForPointer(uintptr_t p)
{
AutoreleasePoolPage *result;
//SIZE是一页page的大小,为4096字节
//p % SIZE,是因为当pool嵌套时,需要先release子pool里的对象,通过这个方法可以获取到子pool距其在父pool中所在页起始位置的偏移量
uintptr_t offset = p % SIZE;
ASSERT(offset >= sizeof(AutoreleasePoolPage));
//p - offset,即可获取到子pool所在的父pool的页的起始地址
result = (AutoreleasePoolPage *)(p - offset);
result->fastcheck();
return result;
}
这个函数就是获取哨兵所在的页,当 pool
嵌套时,先 release
子 pool
,就需要获取到子 pool
在父 pool
里所处的页。
(4)popPage
template<bool allowDebug>
static void
popPage(void *token, AutoreleasePoolPage *page, id *stop)
{
/**
* 出栈页,这个函数的三个入参:
* token:哨兵地址
* page:哨兵所在的页,一般为自动释放池的第一页
* stop:哨兵地址
*/
//pop操作入参的allowDebug为false
if (allowDebug && PrintPoolHiwat) printHiwat();
//release all objs,给自动释放池中哨兵后的所有对象发送一次release消息
//执行完这一步,自动释放池里就只剩下空页面了
page->releaseUntil(stop);
// memory: delete empty children
//kill page,移除页面,至少会保留第一页
if (allowDebug && DebugPoolAllocation && page->empty()) {
// special case: delete everything during page-per-pool debugging
//debug判断,pop操作不会进这里,因为传的allowDebug为false
AutoreleasePoolPage *parent = page->parent;
page->kill();
setHotPage(parent);
} else if (allowDebug && DebugMissingPools && page->empty() && !page->parent) {
// special case: delete everything for pop(top)
// when debugging missing autorelease pools
//debug判断,pop操作不会进这里,因为传的allowDebug为false
page->kill();
setHotPage(nil);
} else if (page->child) {
// hysteresis: keep one empty child if page is more than half full
//有多个页面的自动释放池,在pop操作时会进入这里
if (page->lessThanHalfFull()) {
//若当前页的对象数量比一半还少,从第一页的子页面开始kill,只保留第一页
//如果没有嵌套pool,实际上在上面执行完releaseUntil(stop)后,page都是空页面,所以pop操作基本会走这里
//如果有嵌套pool,传进来的page可能为子pool处于最外层pool的页,此时release完子pool的对象指针后,当前页保存的外层pool的对象指针数可能会大于页数量的一半,就会触发moreThanHalfFull的条件
page->child->kill();
}
else if (page->child->child) {
//当前页的对象指针数量比一半还多,且页面数量大于3,就从第三个页开始kill,保留第一页和第二页
page->child->child->kill();
}
}
}
从代码分析可知,pop
操作调用 popPage
来出栈页时,主要做了两个处理:
release all objs
:给自动释放池中哨兵后的所有对象发送一次 release
消息。这一步执行完,自动释放池里就只剩下空页面了。
kill page
:移除页面,只保留第一页,其他页面都会被移除。
接下来再分析 popPage
函数中几个关键函数的实现:
releaseUntil
void releaseUntil(id *stop)
{
// Not recursive: we don't want to blow out the stack
// if a thread accumulates a stupendous amount of garbage
//popPage函数传进来的stop为哨兵地址,this为自动释放池的第一页
//this ->next,即第一页的next指针
//判断第一页的对象是否被release完
while (this->next != stop) {
// Restart from hotPage() every time, in case -release
// autoreleased more objects
//获取到hot page,当前操作的页面,即最后一页
AutoreleasePoolPage *page = hotPage();
// fixme I think this `while` can be `if`, but I can't prove it
//如果当前page为空,则获取到父页面,并将父页面设为hot page
//确保下一次获取的hot page为这个父页面,实现了页面从后往前的遍历
while (page->empty()) {
page = page->parent;
setHotPage(page);
}
page->unprotect();
//每次都获取当前页的最后一个对象,实现了对象从后往前遍历释放
id obj = *--page->next;
//获取完后偏移next指针,确保一直指向当前页的最后一个对象地址
memset((void*)page->next, SCRIBBLE, sizeof(*page->next));
page->protect();
//如果对象不为nil(不是哨兵),就释放
if (obj != POOL_BOUNDARY) {
objc_release(obj);
}
}
//释放完所有的对象后,就将第一页设为hot page
setHotPage(this);
#if DEBUG
// we expect any children to be completely empty
for (AutoreleasePoolPage *page = child; page; page = page->child) {
ASSERT(page->empty());
}
#endif
}
从代码分析可知,releaseUntil
函数的处理逻辑为:从后往前遍历自动释放池里的页面,在每一页中再从后往前遍历释放对象,直到哨兵为止。释放完对象后,释放池中就只剩下空的页面,并将第一页设为 hot page
。
lessThanHalfFull
//判断当前页是否还没存满一半
bool lessThanHalfFull() {
//next:当前页最后一个对像指针在页里的地址
//begin():前面分析了,为当前页的首地址 + 58字节(自身成员的大小)
//end():当前页的结束地址
return (next - begin() < (end() - begin()) / 2);
}
//当前页的结束地址
id * end() {
//当前页的首地址 + 4096字节,就为当前页的结束地址
//每页大小为4096字节
return (id *) ((uint8_t *)this+SIZE);
}
从代码分析可知,是通过当前页最后一个对象指针在页中地址的偏移量计算,来判断当前页是否存满一半。
kill
void kill()
{
// Not recursive: we don't want to blow out the stack
// if a thread accumulates a stupendous amount of garbage
//pop操作是从自动释放池的第二页开始kill
//因此这里的this是第二页,可以通过打印输出来印证
//从第二页开始,循环遍历子页面,最终获取到自动释放池的最后一页
AutoreleasePoolPage *page = this;
while (page->child) page = page->child;
//从最后一页开始往前遍历销毁,直到this(即第二页)销毁后停止
//每页的处理逻辑为:先获取到当前页面的父页面,再将父页面的子页面设为nil,即销毁了当前页面
AutoreleasePoolPage *deathptr;
do {
deathptr = page;
//获取到父页面
page = page->parent;
if (page) {
page->unprotect();
//销毁子页面
page->child = nil;
page->protect();
}
delete deathptr;
} while (deathptr != this);
}
从代码分析可知,kill
函数是从自重释放池的队后一页开始往前遍历销毁,直到销毁第二页后停止。每一次销毁页面,都是先获取到当前页面的父页面,再将父页面的子页面设为 nil
,即实现了页面销毁。
因此,pop
操作时,如果不是空白的自动释放池,是先从后往前遍历自动释放池里的所有页面,将每一页里的对象从后往前逐步 release
,然后再次从后往前遍历销毁这些空页面,第一页保留不销毁,最后自动释放池里就只剩下一页空的 page
,并且为 hot page
,这就完成了 pop
操作。
4.总结
经过对源码的深入分析,我们可以得出以下结论:
(1)一个自动释放池的运行过程分为这三步:push
操作、[对象 autorelease]
以及 pop
操作。
(2)push
操作本质上是设置一个空白占位符,地址为 0x1
,并存储在当前线程的本地存储 tls
中,设置了这个空白占位符就表示已经创建了一个空的自动释放池。在 push
过程中,既没有创建 page
,也没有压栈哨兵。
(3)autorelease
操作就是压栈对象,给对象发送 autorelease
消息,将对象添加到自动释放池中,在这个过程中会做如下处理:
- 如果压栈的是第一个对象,此时是空白的自动释放池,就创建第一页
page
,并设为hot page
,然后先压栈哨兵,再压栈对象。 - 若
hot page
存在且没满时,直接压栈对象。 - 若
hot page
存在且已满,新建一页page
,再压栈对象。
(4)pop
操作出栈时,会根据自动释放池的情况分别做不同的处理:
- 若是空白的自动释放池,即内部只有空白占位符,此时pop操作会清除占位符,销毁自动释放池。
- 若自动释放池存有压栈对象,就先
release
对象,再kill
页面。最后,自动释放池并没有被销毁,里面还存有一张空的page
,也是之前的第一页。
(5)对于不是空白的自动释放池,pop
操作过程中具体做了如下处理:
-
release
对象:从后往前遍历自动释放池里的页面,在每一页中再从后往前遍历释放对象,直到哨兵为止。release
后,池中只剩下空白的页面,然后将第一页设为hot page
。 -
kill
页面:从后往前遍历销毁这些空页面,第一页保留不销毁。
三、AutoreleasePool嵌套分析
//释放池嵌套
int main(int argc, char * argv[]) {
//pool1,里面添加1个自动释放对象
@autoreleasepool {
NSObject *obj = [[NSObject alloc] autorelease];
//pool2,里面添加3个自动释放对象
@autoreleasepool {
NSInteger count = 3;
for(NSInteger i=0; i<count; i++){
NSObject *obj = [[NSObject alloc] autorelease];
}
//pool3,里面添加2个自动释放对象
@autoreleasepool {
NSInteger count = 2;
for(NSInteger i=0; i<count; i++){
NSObject *obj = [[NSObject alloc] autorelease];
}
//创建完三个pool后打印
_objc_autoreleasePoolPrint();
}
//超出pool3的作用域时打印
_objc_autoreleasePoolPrint();
}
//超出pool2的作用域时打印
_objc_autoreleasePoolPrint();
}
//超出pool1作用域打印
_objc_autoreleasePoolPrint();
return 0;
}
如代码所示,在 main
函数里嵌套创建了 3 个自动释放池,然后在不同时期打印内存结构。
创建后打印
objc[41194]: ##############
objc[41194]: AUTORELEASE POOLS for thread 0x114550dc0
objc[41194]: 9 releases pending.
objc[41194]: [0x7fe8e9815000] ................ PAGE (hot) (cold)
objc[41194]: [0x7fe8e9815038] ################ POOL 0x7fe8e9815038
objc[41194]: [0x7fe8e9815040] 0x600001c5c060 NSObject
objc[41194]: [0x7fe8e9815048] ################ POOL 0x7fe8e9815048
objc[41194]: [0x7fe8e9815050] 0x600001c5c070 NSObject
objc[41194]: [0x7fe8e9815058] 0x600001c5c080 NSObject
objc[41194]: [0x7fe8e9815060] 0x600001c5c090 NSObject
objc[41194]: [0x7fe8e9815068] ################ POOL 0x7fe8e9815068
objc[41194]: [0x7fe8e9815070] 0x600001c5c0a0 NSObject
objc[41194]: [0x7fe8e9815078] 0x600001c5c0b0 NSObject
objc[41194]: ##############
可以看到,线程里维护了一个自动释放池的堆栈。
- 第一个创建的
pool
为最外层释放池,当前只新建了一页page
。 - 嵌套创建的子
pool
在内存结构上来看,按顺序存于最外层释放池的page
中。
超出作用域时打印
//超出pool3
objc[41194]: ##############
objc[41194]: AUTORELEASE POOLS for thread 0x114550dc0
objc[41194]: 6 releases pending.
objc[41194]: [0x7fe8e9815000] ................ PAGE (hot) (cold)
objc[41194]: [0x7fe8e9815038] ################ POOL 0x7fe8e9815038
objc[41194]: [0x7fe8e9815040] 0x600001c5c060 NSObject
objc[41194]: [0x7fe8e9815048] ################ POOL 0x7fe8e9815048
objc[41194]: [0x7fe8e9815050] 0x600001c5c070 NSObject
objc[41194]: [0x7fe8e9815058] 0x600001c5c080 NSObject
objc[41194]: [0x7fe8e9815060] 0x600001c5c090 NSObject
objc[41194]: ##############
//超出pool2
objc[41194]: ##############
objc[41194]: AUTORELEASE POOLS for thread 0x114550dc0
objc[41194]: 2 releases pending.
objc[41194]: [0x7fe8e9815000] ................ PAGE (hot) (cold)
objc[41194]: [0x7fe8e9815038] ################ POOL 0x7fe8e9815038
objc[41194]: [0x7fe8e9815040] 0x600001c5c060 NSObject
objc[41194]: ##############
//超出pool1
objc[41194]: ##############
objc[41194]: AUTORELEASE POOLS for thread 0x114550dc0
objc[41194]: 0 releases pending.
objc[41194]: [0x7fe8e9815000] ................ PAGE (hot) (cold)
objc[41194]: ##############
从三次打印结果可知,虽然子 pool
按顺序存在最外层释放池的 page
中,但每个 pool
还是各自管理自己的自动释放对象。
- 当超出子
pool
的作用域时,子pool
会被销毁,里面的对象也都会被释放。 - 当超出最外层
pool
的作用域时,里面的对象会被释放,但释放池不会被销毁,会保留一个空白的page
。 - 每个线程都会维护自身的自动释放池堆栈,只有当线程被销毁时,最外层
pool
才会被销毁。
网友评论