iOS AutoReleasePool的实现原理
[TOC]
本文也属于iOS Objective-C 内存管理的范畴,AutoReleasePool
就是自动释放池,下面我们来探索一下。
1. 什么是AutoReleasePool
AutoReleasePool
是Objective—C中一种内存自动回收的机制,他可以将加入AutoReleasePool中的变量release
的时机延迟。也就是说,当你创建一个对象,在正常情况下,变量会在超出其作用域的时候立即release
,如果将该对象加入到自动释放池中,这个对象并不会立即释放,而是等到runloop
休眠或者超出AutoReleasePool的作用域{}之后才会被释放。
自动释放池示意图:
image- 这幅图演示了从程序启动到加载完成,主线程对应的runloop会处于休眠状态,等待用户交互来唤醒runloop
- 用户的每一次交互都会启动一次runloop,用于处理用户的所有点击、触摸事件等
- runloop 在监听到交互时间后,就会创建自动释放池,并将所有延迟释放的对象添加到自动释放池中
- 在 一次完整的runloop结束前,会想自动释放池中所有对象发送release消息,然后销毁自动释放池
2. AutoReleasePool 的实现
2.1 通过clang分析
分析AutoReleasePool
我们首先从clang
入手:
编写如下代码:
int main(int argc, const char * argv[]) {
@autoreleasepool {
}
return 0;
}
使用如下命令,通过clang
编译为底层代码:
xcrun -sdk iphonesimulator clang -arch x86_64 -rewrite-objc main.m
编译后的代码为:
struct __AtAutoreleasePool {
// 构造函数
__AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
// 析构函数
~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
void * atautoreleasepoolobj;
};
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ {
// 结构体
__AtAutoreleasePool __autoreleasepool;
}
return 0;
}
通过clang
编译后的代码我们可以发现,AutoReleasePool
的本质是一个结构体。
- 在底层是
__AtAutoreleasePool
- 有构造函数和析构函数
- 结构体在其作用域结束的时候会自动调用析构函数
- 这里的作用域指的就是
{}
- 构造函数中会调用
objc_autoreleasePoolPush
- 析构函数中会调用
objc_autoreleasePoolPop
析构函数:
关于C++中的析构函数,下面我们举个例子来说明一下:
struct Test{
Test
(){
printf("1111 - %s\n", __func__);
}
~Test(){
printf("2222 - %s\n", __func__);
}
};
int main(int argc, const char * argv[]) {
{
Test test;
}
return 0;
}
<!--打印结果-->
1111 - Test
2222 - ~Test
根据打印结果我们可以轻松的得出如下结论:
- 对于
Test
结构体创建对象的时候会自动调用构造函数 - 在出了作用域
{}
后会自动调用析构函数
2.2 通过汇编分析
还是这段代码:
int main(int argc, const char * argv[]) {
@autoreleasepool {
}
return 0;
}
在main
那行添加断点,开始汇编调试Debug
->Debug Workflow
->Always Show Disassembly
,运行,结果如下:
我们可以看到这里面调用了两个符号objc_autoreleasePoolPush
和objc_autoreleasePoolPop
,这跟我们在clang
在的结果是一样的。
2.3 小结
通过初步的探索,我们初步了解了AutoReleasePool
下面稍作总结:
-
AutoReleasePool
本质是一个结构体对象 - 加入自动释放池时调用
objc_autoreleasePoolPush
方法 - 在调用析构函数的时候会释放这些对象,通过
objc_autoreleasePoolPop
方法
3. 底层分析
这里我使用的是objc4-818.2
这个版本的源码:
3.1 初步探索
首先我们搜索了一下objc_autoreleasePoolPush
,这里就可以看到objc_autoreleasePoolPush
和objc_autoreleasePoolPop
的实现。
void *
objc_autoreleasePoolPush(void)
{
return AutoreleasePoolPage::push();
}
NEVER_INLINE
void
objc_autoreleasePoolPop(void *ctxt)
{
AutoreleasePoolPage::pop(ctxt);
}
void *
_objc_autoreleasePoolPush(void)
{
return objc_autoreleasePoolPush();
}
void
_objc_autoreleasePoolPop(void *ctxt)
{
objc_autoreleasePoolPop(ctxt);
}
通过这些找到了AutoreleasePoolPage
,在NSObject.mm
文件中,这里我们大概就能够知道自动释放池是一个页,看看注释:
/***********************************************************************
Autorelease pool implementation
A thread's autorelease pool is a stack of pointers.
Each pointer is either an object to release, or POOL_BOUNDARY which is
an autorelease pool boundary.
A pool token is a pointer to the POOL_BOUNDARY for that pool. When
the pool is popped, every object hotter than the sentinel is released.
The stack is divided into a doubly-linked list of pages. Pages are added
and deleted as necessary.
Thread-local storage points to the hot page, where newly autoreleased
objects are stored.
**********************************************************************/
Autorelease pool implementation
- A thread's autorelease pool is a stack of pointers.
线程的自动释放池是指针的堆栈
- Each pointer is either an object to release, or POOL_BOUNDARY which is an autorelease pool boundary.
每个指针都是要释放的对象,或者是POOL_BOUNDARY,它是自动释放池的边界。
- A pool token is a pointer to the POOL_BOUNDARY for that pool. When the pool is popped, every object hotter than the sentinel is released.
池令牌是指向该池的POOL_BOUNDARY的指针。弹出池后,将释放比哨点更热的每个对象。
- The stack is divided into a doubly-linked list of pages. Pages are added and deleted as necessary.
堆栈分为两个双向链接的页面列表。根据需要添加和删除页面。
- Thread-local storage points to the hot page, where newly autoreleased objects are stored.
线程本地存储指向热页面,该页面存储新自动释放的对象。
总结下来就是:
- 自动释放池是一个 关于 指针 的栈结构
- 其中指针指的是要释放的对象,或者
POOL_BOUNDARY
,也就是哨兵,现在也叫边界。 - 自动释放池是一个页的结构,而这个页是一个双向链表
- 自动释放池与线程是息息相关的
了解了这些后我们就要探索一下如下问题:
- 自动释放池是什么时候创建的呢?
- 对象是如何加入到自动释放池的呢?
- 哪些对象才会加入到自动释放池呢?
- 这个池子的大小是怎样的?
3.2 AutoreleasePoolPage
这里我们研究一下AutoreleasePoolPage
,从上面的代码就可以看出:
-
push
和pop
都是AutoreleasePoolPage中的方法 - AutoreleasePoolPage是一个类
- 也是一个页,,这个页的大小是4096字节
关于4096的定义如下:
class AutoreleasePoolPage : private AutoreleasePoolPageData
{
friend struct thread_data_t;
public:
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_MAX_SHIFT 14
#define PAGE_MAX_SIZE (1 << PAGE_MAX_SHIFT)
#define PAGE_MAX_MASK (PAGE_MAX_SIZE-1)
#define PAGE_MIN_SHIFT 12
#define PAGE_MIN_SIZE (1 << PAGE_MIN_SHIFT)
#define PAGE_MIN_MASK (PAGE_MIN_SIZE-1)
}
这里的1左移12位就是4096。
剩下的代码很多,这里就不都不都放了,感兴趣的自己去下载一下objc4_debug
这里我们看到AutoreleasePoolPage
继承自AutoreleasePoolPageData
,并且它的属性也是来自父类,下面我们看看AutoreleasePoolPageData
:
3.3 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;
__unsafe_unretained id *next;
pthread_t const thread;
AutoreleasePoolPage * const parent;
AutoreleasePoolPage *child;
uint32_t const depth;
uint32_t hiwat;
// 初始化函数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)
{
}
};
从源码中我们可以看到很多属性,下面我们拆解开来:
//用来校验AutoreleasePoolPage的结构是否完整
magic_t const magic;//16个字节
//指向最新添加的autoreleased对象的下一个位置,初始化时指向begin()
__unsafe_unretained id *next;//8字节
//指向当前线程
pthread_t const thread;//8字节
//指向父节点,第一个结点的parent值为nil
AutoreleasePoolPage * const parent;//8字节
//指向子节点,最后一个结点的child值为nil
AutoreleasePoolPage *child;//8字节
//表示深度,从0开始,往后递增1
uint32_t const depth;//4字节
//表示high water mark 最大入栈数量标记
uint32_t hiwat;//4字节
其中AutoreleasePoolPageData
结构体的内存大小为56
字节:
- 其中属性
magic
的类型是magic_t
结构体,所占内存大小为m[4]
;也就是4*4 = 16字节 -
next指针
占8字节 -
thread
、parent
、child
也都占8字节 -
depth
和hiwat
类型为uint32_t
,也就是unsigned int
类型,均占4字节
所以这些加起来就是16+8+8+8+8+4+4 = 56。
3.4 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;
}
代码不多,其逻辑分析如下:
- 首先判断是否有
pool
,这里也就是有没有页 - 如果没有则通过
autoreleaseNewPage
方法创建一页 - 如果有,则调用
autoreleaseFast
方法
3.5 autoreleaseNewPage
3.5.1 autoreleaseNewPage
下面我们在看看autoreleaseNewPage
,该方法就是创建一个新的页。源码如下:
// 创建新页
static __attribute__((noinline))
id *autoreleaseNewPage(id obj)
{
// 获取当前操作页
AutoreleasePoolPage *page = hotPage();
if (page) return autoreleaseFullPage(obj, page);
else return autoreleaseNoPage(obj);
}
根据上面的代码我们可以知道:
- 首先获取当前的操作页,也就是热页
- 如果存在则调用
autoreleaseFullPage
方法 - 如果不存在则调用
autoreleaseNoPage
方法
3.5.2 hotPage
获取hotpage
static inline AutoreleasePoolPage *hotPage()
{
AutoreleasePoolPage *result = (AutoreleasePoolPage *)
tls_get_direct(key);
if ((id *)result == EMPTY_POOL_PLACEHOLDER) return nil;
if (result) result->fastcheck();
return result;
}
- 通过
tls_get_direct
方法获取当前线程的的页 - 如果是一个空的就返回
nil
- 不是则进一步处理
3.5.3 autoreleaseNoPage
这一节我们先来看看autoreleaseNoPage
方法,其他的放在下面在分析。
static __attribute__((noinline))
id *autoreleaseNoPage(id obj)
{
// "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值变量
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;
}
// 如果压栈的不是哨兵对象,并且没有pool 则报错
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;
}
// 如果是哨兵对象,并且没有申请自动释放池的内存,则设置一个空占位符存储在tls中,其目的是为了节省内存
else if (obj == POOL_BOUNDARY && !DebugPoolAllocation) {
// 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();// 设置空的占位符
}
// We are pushing an object or a non-placeholder'd pool.
// Install the first page.
// 初始化一页
AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
// 并将该页设置为聚焦页
setHotPage(page);
// Push a boundary on behalf of the previously-placeholder'd pool.
// pushExtraBoundary为true则压栈哨兵对象
if (pushExtraBoundary) {
page->add(POOL_BOUNDARY);
}
// Push the requested object or pool.
// 添加对象到自动释放池的这一页
return page->add(obj);
}
根据上面的代码我们可以看到,当前线程的自动释放池页是由AutoreleasePoolPage
的构造方法直接创建的,代码如下:
AutoreleasePoolPage(AutoreleasePoolPage *newParent) :
AutoreleasePoolPageData(begin(),
objc_thread_self(),// 当前线程
newParent,
newParent ? 1+newParent->depth : 0,如果是第一页深度为0,然后不断+1
newParent ? newParent->hiwat : 0)
{
if (objc::PageCountWarning != -1) {
checkTooMuchAutorelease();
}
if (parent) {
parent->check();
ASSERT(!parent->child);
parent->unprotect();
// this 表示新建页面,将当前页面的子节点 赋值为新建页面
parent->child = this;
parent->protect();
}
protect();
}
代码中AutoreleasePoolPageData
方法传入的参数含义为:
-
begin()
表示压栈的位置,也就是下一个能压栈的位置
id * begin() {
//等于 首地址+56(AutoreleasePoolPage类所占内存大小)
return (id *) ((uint8_t *)this+sizeof(*this));
}
-
objc_thread_self()
表示的是当前线程
,而当前线程是通过tls
获取的
__attribute__((const))
static inline pthread_t objc_thread_self()
{
//通过tls获取当前线程
return (pthread_t)tls_get_direct(_PTHREAD_TSD_SLOT_PTHREAD_SELF);
}
-
newParent
表示父节点 - 后面这俩就是通过父节点计算深度和最大入栈个数,也就是计算得出
depth
和hiwat
3.5.4 通过lldb查看内存结构
由于在ARC模式下无法直接调用autorelease
,所以我们将demo
切换至MRC模式(Build Settings -> Objectice-C Automatic Reference Counting
)设置为NO
。这里我是在源码工程里面进行操作的,objc
源码的下载上面有提到。编写如下代码:
// 打印自动释放池结构的方法这里未来路绿绿调用直接extern一下
extern void _objc_autoreleasePoolPrint(void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
//循环创建对象,并加入自动释放池
for (int i = 0; i < 5; i++) {
NSObject *objc = [[NSObject alloc] autorelease];
}
//打印
_objc_autoreleasePoolPrint();
}
}
运行查看结果:
image我们可以看到有6个,其中第一个是哨兵对象,后面的5个是我们压栈的NSObject
对象。
但是前面还有些内容,这些占用的内存大小是0x38
也就是56字节,与AutoreleasePoolPage
中属性占用内存的大小一致。
下面我们就不禁有个疑问,这一页能存储多少个对象呢?前面有提到这一页的size
是4096,所以剩下的能存储505个8字节的对象,所以我们将上面示例代码的5修改为505,运行查看结果:
太多了,截图了两个图,我们可以看到,此时就有两页了,第一页的状态是PAGE (full) (cold)
,第二页的状态是PAGE (hot)
,第一页存储了除哨兵外的504
个需要释放的对象,第二有人存储了1
个要释放的对象。第二页的一开始并没有存储哨兵对象。下面我们增加数量为505+506
,运行查看结果如下:
image image
可以发现第一页还是存储504
个需要释放的对象,第二页505
个,第三页2
个。
基本我们可以得出如下结论:
- 第一页拥有一个哨兵对象,加上504个需要释放的对象,当第一页满了,就会开辟新的一页
- 从第二页开始不再有哨兵对象了,而是全部用来存储需要释放的对象,最多
505
个 - 所以一页的大小等于505*8 = 4040字节,加上56刚好为4096
盗个图:
image
其实还有个问题,如果是多线程呢?这个页面中是怎么存储的呢?
我们编写如下代码
:
// 打印自动释放池结构的方法这里未来路绿绿调用直接extern一下
extern void _objc_autoreleasePoolPrint(void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
NSLog(@"thread 1 %@", [NSThread currentThread]);
NSObject *objc = [[NSObject alloc] autorelease];
NSLog(@"first objc = %@",objc);
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSLog(@"thread 2 %@", [NSThread currentThread]);
@autoreleasepool {
NSObject *objc1 = [[NSObject alloc] autorelease];
NSLog(@"two objc = %@",objc1);
_objc_autoreleasePoolPrint();
}
});
_objc_autoreleasePoolPrint();
}
sleep(2);
return 0;
}
打印结果:
image我们可以看到在多线程和@autoreleasepool
嵌套的时候会有2个哨兵的情况出现,所以一页存储需要释放的对象个数就又减少了一个。
如果我们在嵌套一层呢?修改代码为如下:
// 打印自动释放池结构的方法这里未来路绿绿调用直接extern一下
extern void _objc_autoreleasePoolPrint(void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
NSLog(@"thread 1 %@", [NSThread currentThread]);
NSObject *objc = [[NSObject alloc] autorelease];
NSLog(@"first objc = %@",objc);
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSLog(@"thread 2 %@", [NSThread currentThread]);
@autoreleasepool {
NSObject *objc1 = [[NSObject alloc] autorelease];
NSLog(@"two objc = %@",objc1);
_objc_autoreleasePoolPrint();
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSLog(@"thread 3 %@", [NSThread currentThread]);
@autoreleasepool {
NSObject *objc2 = [[NSObject alloc] autorelease];
NSLog(@"three objc = %@",objc2);
_objc_autoreleasePoolPrint();
}
});
}
});
_objc_autoreleasePoolPrint();
}
sleep(5);
return 0;
}
打印结果:
image根据打印结果我们可以看到最多还是两个哨兵。
3.6 autoreleaseFast
研究完了新建一页后我们再来研究一下压栈对象时的这个方法,源码如下:
static inline id *autoreleaseFast(id obj)
{
// 获取聚焦页
AutoreleasePoolPage *page = hotPage();
if (page && !page->full()) {
// 没满就add
return page->add(obj);
} else if (page) {
return autoreleaseFullPage(obj, page);
} else {
return autoreleaseNoPage(obj);
}
}
代码也很简单:
- 首先获取到
hotpage
- 判断page存在且没满的话将压栈
- 满了后
page
存在则调用autoreleaseFullPage
,满处理方法 - 最后就是没有
page
则调用autoreleaseNoPage
方法,这个在上面讲过了
3.6.1 autoreleaseFullPage
autoreleaseFullPage方法主要是在页满的时候进行处理的,源码如下:
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);
do {
if (page->child) page = page->child;
else page = new AutoreleasePoolPage(page);
} while (page->full());
setHotPage(page);
return page->add(obj);
}
- 这里面通过一个
do while
循环查找子页面,并判断子页面是否也是满的 - 如果满就继续循环,如果没有子页面,就创建个新页面
3.6.2 add
其实真正的压栈方法是add
,下面我们就看看这个add是怎么实现的,源码如下:
id *add(id obj)
{
ASSERT(!full());
unprotect();
id *ret;
#if SUPPORT_AUTORELEASEPOOL_DEDUP_PTRS
if (!DisableAutoreleaseCoalescing || !DisableAutoreleaseCoalescingLRU) {
if (!DisableAutoreleaseCoalescingLRU) {
if (!empty() && (obj != POOL_BOUNDARY)) {
AutoreleasePoolEntry *topEntry = (AutoreleasePoolEntry *)next - 1;
for (uintptr_t offset = 0; offset < 4; offset++) {
AutoreleasePoolEntry *offsetEntry = topEntry - offset;
if (offsetEntry <= (AutoreleasePoolEntry*)begin() || *(id *)offsetEntry == POOL_BOUNDARY) {
break;
}
if (offsetEntry->ptr == (uintptr_t)obj && offsetEntry->count < AutoreleasePoolEntry::maxCount) {
if (offset > 0) {
AutoreleasePoolEntry found = *offsetEntry;
memmove(offsetEntry, offsetEntry + 1, offset * sizeof(*offsetEntry));
*topEntry = found;
}
topEntry->count++;
ret = (id *)topEntry; // need to reset ret
goto done;
}
}
}
} else {
if (!empty() && (obj != POOL_BOUNDARY)) {
AutoreleasePoolEntry *prevEntry = (AutoreleasePoolEntry *)next - 1;
if (prevEntry->ptr == (uintptr_t)obj && prevEntry->count < AutoreleasePoolEntry::maxCount) {
prevEntry->count++;
ret = (id *)prevEntry; // need to reset ret
goto done;
}
}
}
}
#endif
ret = next; // faster than `return next-1` because of aliasing
*next++ = obj;
#if SUPPORT_AUTORELEASEPOOL_DEDUP_PTRS
// Make sure obj fits in the bits available for it
ASSERT(((AutoreleasePoolEntry *)ret)->ptr == (uintptr_t)obj);
#endif
done:
protect();
return ret;
}
看着好多代码,其实很简单,主要就是通过next
指针的偏移,来进行不断的添加。
3.7 autorelease
刚刚我们测试每页能存储多少对象,以及查看内存结构的时候使用到了autorelease
,下面我们看看它的底层是如何实现的。
直接查看其源码:
// Equivalent to [this autorelease], with shortcuts if there is no override
inline id
objc_object::autorelease()
{
ASSERT(!isTaggedPointer());
if (fastpath(!ISA()->hasCustomRR())) {
return rootAutorelease();
}
return ((id(*)(objc_object *, SEL))objc_msgSend)(this, @selector(autorelease));
}
// Base autorelease implementation, ignoring overrides.
inline id
objc_object::rootAutorelease()
{
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);
}
public:
static inline id autorelease(id obj)
{
ASSERT(!obj->isTaggedPointerOrNil());
id *dest __unused = autoreleaseFast(obj);
#if SUPPORT_AUTORELEASEPOOL_DEDUP_PTRS
ASSERT(!dest || dest == EMPTY_POOL_PLACEHOLDER || (id)((AutoreleasePoolEntry *)dest)->ptr == obj);
#else
ASSERT(!dest || dest == EMPTY_POOL_PLACEHOLDER || *dest == obj);
#endif
return obj;
}
可以看到,最后还是调用的autoreleaseFast
方法。
3.8 pop
下面我们再来看看pop
在objc_autoreleasePoolPop
方法中有个参数,在clang
分析时,发现传入的参数是push
压栈后返回的哨兵对象,也就是ctxt
,其目的是避免出栈混乱,防止将别的对象出栈。
pop源码如下:
__attribute__((noinline, cold))
static void
popPageDebug(void *token, AutoreleasePoolPage *page, id *stop)
{
popPage<true>(token, page, stop);
}
static inline void
pop(void *token)
{
AutoreleasePoolPage *page;
id *stop;
// 判断对象是否是空占位符
if (token == (void*)EMPTY_POOL_PLACEHOLDER) {
// Popping the top-level placeholder pool.
// 获取聚焦页
page = hotPage();
if (!page) {
// Pool was never used. Clear the placeholder.
// 如果页不存在,则将聚焦页设置为nil
return setHotPage(nil);
}
// Pool was used. Pop its contents normally.
// Pool pages remain allocated for re-use as usual.
// 如果存在则将该页设置为clodPage,token设置为开始的位置
page = coldPage();
token = page->begin();
} else {
// 获取token所在的页
page = pageForPointer(token);
}
stop = (id *)token;
// 判断stop 的值是否是哨兵,也就是最后的是不是哨兵
if (*stop != POOL_BOUNDARY) {
// 如果最后一个位置不是哨兵,最后也是第一个位置,并且没有父节点什么也不做
if (stop == page->begin() && !page->parent) {
// 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 {
// Error. For bincompat purposes this is not
// fatal in executables built with old SDKs.
// 到这里就是出现了混乱,调用badPop方法
return badPop(token);
}
}
if (slowpath(PrintPoolHiwat || DebugPoolAllocation || DebugMissingPools)) {
return popPageDebug(token, page, stop);
}
// 出栈该页
return popPage<false>(token, page, stop);
}
根据pop
源码,其主要是找到要pop
的这页,进行一些容错处理,通过popPage
出栈页。
3.8.1 popPage
下面我们看看popPage
方法:
template<bool allowDebug>
static void
popPage(void *token, AutoreleasePoolPage *page, id *stop)
{
//allowDebug传入的是false
if (allowDebug && PrintPoolHiwat) printHiwat();
// 释放当前操作页面的对象
page->releaseUntil(stop);
// memory: delete empty children 删除空的子页
if (allowDebug && DebugPoolAllocation && page->empty()) {
// special case: delete everything during page-per-pool debugging
// allowDebug为false也到不了这个分支
// 获取到父页,将该页kill,设置父页为hot
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
//特殊情况:调试丢失的自动释放池时删除pop(top)的所有内容
page->kill();
setHotPage(nil);
} else if (page->child) {
// hysteresis: keep one empty child if page is more than half full
if (page->lessThanHalfFull()) {
page->child->kill();
}
else if (page->child->child) {
page->child->child->kill();
}
}
}
3.8.2 releaseUntil
下面我们看看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
while (this->next != stop) {
// Restart from hotPage() every time, in case -release
// autoreleased more objects
AutoreleasePoolPage *page = hotPage();
// fixme I think this `while` can be `if`, but I can't prove it
while (page->empty()) {
page = page->parent;
setHotPage(page);
}
page->unprotect();
#if SUPPORT_AUTORELEASEPOOL_DEDUP_PTRS
AutoreleasePoolEntry* entry = (AutoreleasePoolEntry*) --page->next;
// create an obj with the zeroed out top byte and release that
id obj = (id)entry->ptr;
int count = (int)entry->count; // grab these before memset
#else
id obj = *--page->next;
#endif
memset((void*)page->next, SCRIBBLE, sizeof(*page->next));
page->protect();
if (obj != POOL_BOUNDARY) {
#if SUPPORT_AUTORELEASEPOOL_DEDUP_PTRS
// release count+1 times since it is count of the additional
// autoreleases beyond the first one
for (int i = 0; i < count + 1; i++) {
objc_release(obj);
}
#else
objc_release(obj);
#endif
}
}
setHotPage(this);
#if DEBUG
// we expect any children to be completely empty
for (AutoreleasePoolPage *page = child; page; page = page->child) {
ASSERT(page->empty());
}
#endif
}
这里面的源码也很简单,就是一个while
循环,通过next指针循环获取上一个对象,对于哨兵对象最后通过objc_release
进行释放。
3.8.3 kill
下面我们看看kill
的实现,源码如下:
void kill()
{
// Not recursive: we don't want to blow out the stack
// if a thread accumulates a stupendous amount of garbage
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);
}
这里也很简单,首先找到最后一个子节点,然后循环置空子节点,从子向父遍历。
4. 总结
至此我们对自动释放池的分析就基本完毕了,下面总结一下:
首先总结一下push
- 当没有
pool
时,即只有空占位符(存在tls中)时,则创建页,压栈哨兵对象 - 在页中压栈普通对象主要通过next指针递增进行的
- 当页满了,则新建一个子页,当前页的
child
指向子页,继续向子页添加要释放的对象
盗个图:
image
- 出栈的时候则在页中通过
next
指针递减进行查找对象最后通过objc_release
释放对象 - 当该页为空的时候则将
page
赋值为父页
盗个图:
image
网友评论