自动释放池 @autoreleasepool
最常见的地方就是我们项目的 main
函数 。我们今天来深入探索下其底层结构和实现原理。先查看一下编译后的情形:
int main(int argc, char * argv[]) {
NSString * appDelegateClassName;
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
appDelegateClassName = NSStringFromClass(((Class (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("AppDelegate"), sel_registerName("class")));
}
return UIApplicationMain(argc, argv, __null, appDelegateClassName);
}
可以看到 编译后 将 @autoreleasepool
注释掉,替换为了 __AtAutoreleasePool __autoreleasepool;
。__AtAutoreleasePool
是一个结构体:
struct __AtAutoreleasePool {
__AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
void * atautoreleasepoolobj;
};
编译后 的 __AtAutoreleasePool __autoreleasepool;
这行代码就相当于 在作用域开始的地方,调用 构造函数, 在作用域结束的时候,调用析构函数。也就是分别调用了:
objc_autoreleasePoolPush()
objc_autoreleasePoolPop(atautoreleasepoolobj)
void *
objc_autoreleasePoolPush(void)
{
return AutoreleasePoolPage::push();
}
/***********************************************************************
Autorelease pool implementation
线程的自动释放池是一堆指针。
每个指针要么是要释放的对象,要么是POOL_BOUNDARY这是自动释放池边界的对象。
池令牌是指向该池的POOL_BOUNDARY的指针。当池弹出时,每个比哨兵热的对象都会被释放。
堆栈被划分为一个双链页面列表。根据需要添加和删除页面。
线程本地存储指向新创建自动释放热页(其中存储了新自动释放的对象)。
**********************************************************************/
BREAKPOINT_FUNCTION(void objc_autoreleaseNoPool(id obj));
BREAKPOINT_FUNCTION(void objc_autoreleasePoolInvalid(const void *token));
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)
可以判断出:
AutoreleasePoolPage
是继承自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
// 用来校验 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(__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)
{
}
};
struct magic_t {
static const uint32_t M0 = 0xA1A1A1A1;
# define M1 "AUTORELEASE!"
static const size_t M1_len = 12;
uint32_t m[4];
...
}
发现其中有AutoreleasePoolPage
对象,所以有以下一个关系链AutoreleasePoolPage -> AutoreleasePoolPageData -> AutoreleasePoolPage
,从这里可以说明自动释放池除了是一个页,还是一个双向链表结构。
AutoreleasePoolPageData
结构体的内存大小为56字节。
magic
的类型是magic_t
结构体,所占内存大小为m[4];
所占内存(即4*4=16字节)next
(指针)、thread
(对象)、parent
(对象)、child
(对象)均占8字节(即4*8=32字节)depth
、hiwat
类型为uint32_t
,实际类型是unsigned int
类型,均占4字节// 入栈
static inline void *push()
{
id *dest;
// 判断是否有pool
if (slowpath(DebugPoolAllocation)) {
// Each autorelease pool starts on a new pool page.
// 每个自动释放池都会在一个新池页上启动。
// 没有则创建
dest = autoreleaseNewPage(POOL_BOUNDARY);
} else {
// 压栈一个哨兵(POOL_BOUNDARY)
dest = autoreleaseFast(POOL_BOUNDARY);
}
ASSERT(dest == EMPTY_POOL_PLACEHOLDER || *dest == POOL_BOUNDARY);
return dest;
}
autoreleaseNewPage
方法创建autoreleaseFast
压栈哨兵对象// 创建新页
static __attribute__((noinline))
id *autoreleaseNewPage(id obj)
{
// 获取当前操作页
AutoreleasePoolPage *page = hotPage();
// 如果存在,则压栈对象
if (page) return autoreleaseFullPage(obj, page);
// 如果不存在,创建页
else return autoreleaseNoPage(obj);
}
// 获取当前操作页
static inline AutoreleasePoolPage *hotPage()
{
// 获取当前页
AutoreleasePoolPage *result = (AutoreleasePoolPage *)
tls_get_direct(key);
// 如果是空池返回nil,否则返回当前线程的自动释放池
if ((id *)result == EMPTY_POOL_PLACEHOLDER) return nil;
if (result) result->fastcheck();
return result;
}
// 添加自动释放对象,当没页的时候使用这个方法
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 pushExtraBoundary = false;
// 判断是否是空占位符,如果是,则压栈哨兵标识符置为YES
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;
}// 如果对象是哨兵对象,且没有申请自动释放池内存,则设置一个空占位符存储在t1s中,其目的是为了节省内存
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);// 设置page为当前聚焦页(热页)
// Push a boundary on behalf of the previously-placeholder'd pool.
// 压栈哨兵的标识符为YES,则压栈哨兵对象
if (pushExtraBoundary) {
// 压栈哨兵
page->add(POOL_BOUNDARY);
}
// Push the requested object or pool.压栈对象
return page->add(obj);
}
autoreleaseFullPage
后面会重点分析。下面看一下创建页流程:
autoreleaseFullPage
方法进行压栈对象autoreleaseNoPage
方法创建页
autoreleaseNoPage
方法中可知当前线程的自动释放池是通过AutoreleasePoolPage
创建的(AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
)AutoreleasePoolPage
的构造方法是通过实现父类AutoreleasePoolPageData
的初始化方法实现的上面说了当前线程的自动释放池是通过AutoreleasePoolPage
创建,看下AutoreleasePoolPage
构造方法:
AutoreleasePoolPage(AutoreleasePoolPage *newParent) :
AutoreleasePoolPageData(begin(),// 开始存储的位置
objc_thread_self(),// 传的是当前线程,当前线程时通过t1s获取的
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(__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)
{
}
其中AutoreleasePoolPageData
方法传入的参数含义为:
(1)begin()
表示压栈的位置(即下一个要释放对象的压栈地址)。可以通过源码调试begin
,发现其具体实现等于页首地址+56,其中的56就是结构体AutoreleasePoolPageData
的内存大小
(2)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);
}
(3)newParent
表示父节点
(4)后续两个参数是通过父节点的深度
、最大入栈个数
计算depth
以及hiwat
由于在ARC模式下,是无法手动调用autorelease
,所以将Demo切换至MRC模式。
int main(int argc, const char * argv[]) {
@autoreleasepool {
for (int i = 0; i < 5; i++) {
[[[NSObject alloc] init] autorelease];
}
// 打印池子
_objc_autoreleasePoolPrint();
}
return 0;
}
输出:
通过运行结果,我们发现release
是6个,但是我们压栈对象是5个,其中的POOL表示哨兵,即边界,其目的是为了防止越界。我们再看下打印地址,发现页的首地址和哨兵对象相差0x38,转成10进制正好是56,这也是AutoreleasePoolPage
自身的内存大小。
循环次数改成505,再来运行一次。
第一页:
第二页:
通过上图我们发现第一页满了,存储了504个要释放的对象,第二页只存储了一个。我们再改下循环次数,改为1015次,再看看是不是一页只能存504个对象。
第一页:
第二页:
505 * 8 = 4040
AutoreleasePoolPage
的SIZE是就说了,一页的大小为4096字节,而在其构造函数中对象的压栈位置,是从首地址+56
开始的,所以可以一页中实际可以存储4096-56 = 4040
字节,转换成对象是4040 / 8 = 505
个,即一页最多可以存储505个对象,其中第一页有哨兵对象
,只能存储504个通过上面可以知道:
autoreleasepool
其本质是一个结构体对象,一个自动释放池对象就是页,是栈结构存储,符合先进后出的原则即可autoreleasepool
在加入要释放的对象时,底层调用的是objc_autoreleasePoolPush
方法autoreleasepool
在调用析构函数释放时,内部的实现是调用objc_autoreleasePoolPop
方法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);
}
}
add
方法压栈对象autoreleaseFullPage
方法安排新的页面autoreleaseNoPage
方法创建新页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
循环查找子节点对应的页,如果不存在,则新建页,并压栈对象从上面AutoreleasePoolPage
初始化方法中可以看出,主要是通过操作child
对象,将当前页的child
指向新建页面,由此可以得出页是通过双向链表连接。
// 添加释放对象,next指向下一个存对象的地址
id *add(id obj)
{
ASSERT(!full());
unprotect();
// 传入对象存储的位置
id *ret;
#if SUPPORT_AUTORELEASEPOOL_DEDUP_PTRS
...
#endif
ret = next; // faster than `return next-1` because of aliasing
// 将obj压栈到next指针位置,然后next进行++,即下一个对象存储的位置
*next++ = obj;
#if SUPPORT_AUTORELEASEPOOL_DEDUP_PTRS
...
done:
protect();
return ret;
}
这个方法主要是添加释放对象
,其底层是实现是通过next
指针存储释放对象,并将next
指针递增,表示下一个释放对象存储的位置。从这里可以看出页是通过栈结构存储。
在objc_autoreleasePoolPop
方法中有个参数,在调试分析时,发现传入的参数ctxt
与push
压栈后返回的哨兵对象相同,其目的是避免出栈混乱,防止将别的对象出栈,其内部是调用AutoreleasePoolPage
的pop
方法,接下来看下pop
源码:
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.
// 如果当前页不存在,则清除空占位符
return setHotPage(nil);
}
// Pool was used. Pop its contents normally.
// Pool pages remain allocated for re-use as usual.
// 如果当前页存在,则将当前页设置为coldPage
// token设置为coldPage的开始位置
page = coldPage();
token = page->begin();
} else {
// 获取token所在的页
page = pageForPointer(token);
}
stop = (id *)token;
// 判断最后一个位置,是否是哨兵
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.
return badPop(token);
}
}
if (slowpath(PrintPoolHiwat || DebugPoolAllocation || DebugMissingPools)) {
return popPageDebug(token, page, stop);
}
// 出栈页
return popPage<false>(token, page, stop);
}
token
获取page
popPage
出栈页查看popPage
源码
// 出栈页面
template<bool allowDebug>
static void
popPage(void *token, AutoreleasePoolPage *page, id *stop)
{
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
// 调试期间删除每个特殊情况下的所有池
// 荻取当前页面的父节点
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();
}
}
}
进入popPage
源码,其中传入的allowDebug
为false
,则通过releaseUntil
出栈当前页stop位置之前的所有对象
,即向栈中的对象发送release消息
,直到遇到传入的哨兵对象
。
// 释放到stop位置之前的所有对象
void releaseUntil(id *stop)
{
// Not recursive: we don't want to blow out the stack
// 不是递归的:我们不想破坏堆栈
// if a thread accumulates a stupendous amount of garbage
// 判断下一个对象是否等于stop,如果不等于,则进入whi1e循环
while (this->next != stop) {
// Restart from hotPage() every time, in case -release
// autoreleased more objects
// 每次从hotPage()重新启动,以防-release自动释放更多对象
// 获取当前操作页面,即hot页面
AutoreleasePoolPage *page = hotPage();
// fixme I think this `while` can be `if`, but I can't prove it
// 如果当前页是空的
while (page->empty()) {
// 将page赋值为父节点页
page = page->parent;
// 并设置当前页为父节点页
setHotPage(page);
}
page->unprotect();
// next进行一一操作,即出栈
id obj = *--page->next;
// 将页索引位置置为SCRIBBLE,表示已经被释放
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
}
releaseUntil
实现,主要是通过循环遍历,判断对象是否等于stop
,其目的是释放stop
之前的所有的对象page
的next
释放对象(即page
的最后一个对象),并对next
进行递减,获取上一个对象objc_release
释放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();
// 子节点为nil
page->child = nil;
page->protect();
}
delete deathptr;
} while (deathptr != this);
}
通过kill
的实现我们知道,主要是销毁当前页,将当前页赋值为父节点页,并将父节点页的child
对象指针置为nil
。
在自动释放池的压栈(即push)操作中
所以,综上所述,objc_autoreleasePush
的整体底层的流程如下图所示
在自动释放池的出栈(即pop)操作中
综上所述,objc_autoreleasePoolPop
出栈的流程如下所示
AutoreleasePoolPage
结构体对象,是一个栈结构存储的页,每一个AutoreleasePoolPage
都是以双向链表的形式连接objc_autoreleasePoolPush
和objc_autoreleasePoolPop
,实际上是调用AutoreleasePoolPage
的push
和pop
两个方法push
操作其实就是创建一个新的AutoreleasePoolPage
,而AutoreleasePoolPage
的具体操作就是插入一个POOL_BOUNDARY
,并返回插入POOL_BOUNDARY
的内存地址。而push
内部调用autoreleaseFast
方法处理,主要有以下三种情况
page
存在,且不满时,调用add
方法将对象添加至page
的next
指针处,并next
递增page
存在,且已满时,调用autoreleaseFullPage
初始化一个新的page
,然后调用add
方法将对象添加至page
栈中page
不存在时,调用autoreleaseNoPage
创建一个hotPage
,然后调用add方法将对象添加至page
栈中pop
操作时,会传入一个值,这个值就是push
操作的返回值,即POOL_BOUNDARY
的内存地址token
。所以pop
内部的实现就是根据token
找到哨兵对象所处的page
中,然后使用 objc_release
释放token
之前的对象,并把next
指针到正确位置push
哨兵对象,在pop
时,会先释放里面的,在释放外面的new
、alloc
、copy
关键字生成的对象和retain
了的对象需要手动释放,不会被添加到自动释放池中autorelease
的对象不需要手动释放,会直接进入自动释放池autorelease
的对象,在出了作用域之后,会被自动添加到最近创建的自动释放池中AutoreleasePool
,所以创建会自动放入自动释放池Observer
,其回调都是_wrapRunLoopWithAutoreleasePoolHandler()
。Observer
监视的事件是Entry
(即将进入 Loop),其回调内会调用 _objc_autoreleasePoolPush()
创建自动释放池。其order
是-2147483647,优先级最高
,保证创建释放池发生在其他所有回调之前。Observer
监视了两个事件:BeforeWaiting
(准备进入休眠) 时调用 _objc_autoreleasePoolPop()
和_objc_autoreleasePoolPush()
释放旧的池并创建新池;Exit
(即将退出Loop)时调用_objc_autoreleasePoolPop()
来释放自动释放池。这个Observer
的order
是 2147483647,优先级最低,保证其释放池子发生在其他所有回调之后。总结:每个线程都有与之关联的自动释放池堆栈结构,新的pool在创建时会被压栈到栈顶,pool销毁时,会被出栈,对于当前线程来说,释放对象会被压栈到栈顶,线程停止时,会自动释放与之关联的自动释放池
扩展:
autoreleasePool
事件循环结束
时,执行drain
操作,释放其中的对象