[iOS开发]@autoreleasepool原理探究
Posted Billy Miracle
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[iOS开发]@autoreleasepool原理探究相关的知识,希望对你有一定的参考价值。
自动释放池
自动释放池 @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();
(一)AutoreleasePoolPage
/***********************************************************************
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)
可以判断出:
- 自动释放池是一个页,同时也是一个对象,这个页的大小是4096字节。
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
// 用来校验 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字节
(三)objc_autoreleasePoolPush
// 入栈
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;
- 首先进行判断是否存在pool
- 如果没有,则通过
autoreleaseNewPage
方法创建 - 如果有,则通过
autoreleaseFast
压栈哨兵对象
1. autoreleaseNewPage创建页
// 创建新页
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
的初始化方法实现的
2. AutoreleasePoolPage
上面说了当前线程的自动释放池是通过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
3. 查看自动释放池内存结构
由于在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个对象。
第一页:
第二页:
第三页:
- 第一页可以存放504个对象,且只有第一页有哨兵,当一页压栈满了,就会开辟新的一页
- 第二页开始,最多可以存放505个对象
- 一页的大小等于
505 * 8 = 4040
上面的结论我们之前讲AutoreleasePoolPage
的SIZE是就说了,一页的大小为4096字节,而在其构造函数中对象的压栈位置,是从首地址+56
开始的,所以可以一页中实际可以存储4096-56 = 4040
字节,转换成对象是4040 / 8 = 505
个,即一页最多可以存储505个对象,其中第一页有哨兵对象
,只能存储504个
通过上面可以知道:
- 一个自动释放池只有一个哨兵对象,且哨兵在第一页
- 第一页最多可以存504个对象,第二页开始最多存 505个
(四)小结
autoreleasepool
其本质是一个结构体对象,一个自动释放池对象就是页,是栈结构存储,符合先进后出的原则即可- 页的栈底是一个56字节大小的空占位符,一页总大小为4096字节
- 只有第一页有哨兵对象,最多存储504个对象,从第二页开始最多存储505个对象
autoreleasepool
在加入要释放的对象时,底层调用的是objc_autoreleasePoolPush
方法autoreleasepool
在调用析构函数释放时,内部的实现是调用objc_autoreleasePoolPop
方法
(五)压栈对象 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);
- 获取当前操作页,并判断页是否存在以及是否满了
- 如果页存在,且未满,则通过
add
方法压栈对象 - 如果页存在,且满了,则通过
autoreleaseFullPage
方法安排新的页面 - 如果页不存在,则通过
autoreleaseNoPage
方法创建新页
1. 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
循环查找子节点对应的页,如果不存在,则新建页,并压栈对象从上面AutoreleasePoolPage
初始化方法中可以看出,主要是通过操作child
对象,将当前页的child
指向新建页面,由此可以得出页是通过双向链表连接。
2. add
// 添加释放对象,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
在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
源码
1. popPage
// 出栈页面
template<bool allowDebug>
static void
popPage(void *token, AutoreleasePoolPage *page, id *stop)
iOS之深入解析自动释放池autoreleasepool的底层原理
iOS基础 - autorelease 和 autoreleasepool
iOS基础 - autorelease 和 autoreleasepool
iOS开发-34自己主动释放池@autoreleasepool的使用注意事项以及ARC机制——面试必考内容