iOS之深入解析自动释放池autoreleasepool的底层原理
Posted Forever_wj
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了iOS之深入解析自动释放池autoreleasepool的底层原理相关的知识,希望对你有一定的参考价值。
一、自动释放池 autoreleasepool 原理
- 自动释放池是 OC 中的一种内存自动回收机制,它可以将加入 autoreleasePool 中的变量 release 的时机延迟。
- 简单来说,就是当创建一个对象,在正常情况下,变量会在超出其作用域的时立即release。
- 如果将对象加入到了自动释放池中,这个对象并不会立即释放,会等到 runloop 休眠,超出 autoreleasepool 作用域{}之后才会被释放。
- 自动释放池 autoreleasepool 其机制如下图所示:
- autoreleasepool 机制流程:
-
- 从程序启动到加载完成,主线程对应的 runloop 会处于休眠状态,等待用户交互来唤醒 runloop;
-
- 用户的每一次交互都会启动一次 runloop,用于处理用户的所有点击、触摸事件等;
-
- runloop 在监听到交互事件后,就会创建自动释放池,并将所有延迟释放的对象添加到自动释放池中;
-
- 在一次完整的 runloop 结束之前,会向自动释放池中所有对象发送 release 消息,然后销毁自动释放池。
二、自动释放池 autoreleasepool 机制分析
① Clang 分析
- 定义如下代码:
int main(int argc, const char * argv[]) {
@autoreleasepool {
}
}
- 通过 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;
}
- 简单来说,自动释放池其本质也是一个对象:
@autoreleasepool {}
// 等价
{__AtAutoreleasePool __autoreleasepool; }
- __AtAutoreleasePool 是一个结构体,有构造函数 + 析构函数,结构体定义的对象在作用域结束后,会自动调用析构函数;其中 {} 是作用域 ,优点是结构清晰,可读性强,可以及时创建和销毁;
- 关于涉及的构造和析构函数的调用时机,可以通过下面一个案例来验证:
struct YDWTest{
YDWTest
(){
printf("pool - %s\\n", __func__);
}
~YDWTest(){
printf("pool - %s\\n", __func__);
}
};
int main(int argc, const char * argv[]) {
{
YDWTest test;
}
}
// **运行结果**
pool - YDWTest
pool - ~YDWTest
- 从而可以得出,在 YDWTest 创建对象时,会自动调用构造函数,出了{}作用域后,会自动调用析构函数。
② 汇编分析
- 在 main 代码部分加断点,运行程序,并开启汇编调试:
- 通过调试结果发现,可以证明上述 clang 分析的结果。
③ 总结
- autoreleasepool 的本质是一个结构体对象,一个自动释放池对象就是页,页是“栈结构存储”,符合“先进后出”的原则;
- 页的栈底是一个 56 字节大小的空占位符,一页总大小为 4096 字节;
- 只有第一页有哨兵对象,最多存储 504 个对象,从第二页开始最多存储 505 个对象;
- autoreleasepool 在加入需要释放的对象时,底层调用的是 objc_autoreleasePoolPush 方法;
- autoreleasepool 在调用析构函数释放时,内部的实现是调用 objc_autoreleasePoolPop 方法。
三、自动释放池 autoreleasepool 底层原理
- 在 objc 源码中,对 AutoreleasePool 的解释描述如下:
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 哨兵(现常被称为“边界”);
- 自动释放池是一个页的结构 ,并且这个页是一个双向链表(表示有父节点和子节点,即类的继承链);
- 自动释放池和线程有关系。
- 对于自动释放池,主要需要探究以下问题:
-
- 自动释放池什么时候创建?
-
- 对象是如何加入自动释放池的?
-
- 哪些对象才会加入自动释放池?
① AutoreleasePoolPage
- 从上文中,我们了解到了自动释放池其底层是调用的 objc_autoreleasePoolPush 和 objc_autoreleasePoolPop 两个方法,在源码的体现如下:
// push方法
void *
objc_autoreleasePoolPush(void) {
return AutoreleasePoolPage::push();
}
// pop方法
void
objc_autoreleasePoolPop(void *ctxt) {
AutoreleasePoolPage::pop(ctxt);
}
- 从源码中可以发现,objc_autoreleasePoolPush 和 objc_autoreleasePoolPop 是调用 AutoreleasePoolPage 的 push 和 pop 实现,从定义中可以看出,自动释放池是一个页,同时也是一个对象,这个页的大小是 4096 字节,如下:
// 宏定义
#define PAGE_MIN_SIZE PAGE_SIZE
#define PAGE_SIZE I386_PGBYTES
#define I386_PGBYTES 4096 /* bytes per 80386 page */
// 类定义
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
private:
...
// 构造函数
AutoreleasePoolPage(AutoreleasePoolPage *newParent) :
AutoreleasePoolPageData(begin(),// 开始存储的位置
objc_thread_self(), // 传的是当前线程,当前线程时通过tls获取的
newParent,
newParent ? 1+newParent->depth : 0,// 如果是第一页深度为0,往后是前一个的深度+1
newParent ? newParent->hiwat : 0)
{...}
// 析构函数
~AutoreleasePoolPage() {...}
...
// 页的开始位置
id * begin() {...}
// 页的结束位置
id * end() {...}
// 页是否为空
bool empty() {...}
// 页是否满了
bool full() {...}
// 页的存储是否少于一半
bool lessThanHalfFull() {...}
// 添加释放对象
id *add(id obj){...}
// 释放所有对象
void releaseAll() {...}
// 释放到stop位置之前的所有对象
void releaseUntil(id *stop) {...}
// 杀掉
void kill() {...}
// 释放本地线程存储空间
static void tls_dealloc(void *p) {...}
// 获取AutoreleasePoolPage
static AutoreleasePoolPage *pageForPointer(const void *p) {...}
static AutoreleasePoolPage *pageForPointer(uintptr_t p) {...}
// 是否有空池占位符
static inline bool haveEmptyPoolPlaceholder() {...}
// 设置空池占位符
static inline id* setEmptyPoolPlaceholder(){...}
// 获取当前操作页
static inline AutoreleasePoolPage *hotPage(){...}
// 设置当前操作页
static inline void setHotPage(AutoreleasePoolPage *page) {...}
// 获取coldPage
static inline AutoreleasePoolPage *coldPage() {...}
// 快速释放
static inline id *autoreleaseFast(id obj){...}
// 添加自动释放对象,当页满的时候调用这个方法
static __attribute__((noinline))
id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page) {...}
// 添加自动释放对象,当没页的时候使用这个方法
static __attribute__((noinline))
id *autoreleaseNoPage(id obj){...}
// 创建新页
static __attribute__((noinline))
id *autoreleaseNewPage(id obj) {...}
public:
// 自动释放
static inline id autorelease(id obj){...}
// 入栈
static inline void *push() {...}
// 兼容老 SDK 出栈方法
__attribute__((noinline, cold))
static void badPop(void *token){...}
// 出栈页面
template<bool allowDebug>
static void
popPage(void *token, AutoreleasePoolPage *page, id *stop){...}
__attribute__((noinline, cold))
static void
popPageDebug(void *token, AutoreleasePoolPage *page, id *stop){...}
// 出栈
static inline void
pop(void *token){...}
static void init(){...}
// 打印
__attribute__((noinline, cold))
void print(){...}
// 打印所有
__attribute__((noinline, cold))
static void printAll(){...}
// 打印Hiwat
__attribute__((noinline, cold))
static void printHiwat(){...}
- 可以看到:AutoreleasePoolPage 是继承自 AutoreleasePoolPageData,且该类的属性也继承自父类;
- 在 AutoreleasePoolPageData 中,可以看到有 AutoreleasePoolPage 对象,因此可以得出以下关链:AutoreleasePoolPage -> AutoreleasePoolPageData -> AutoreleasePoolPage,可以说明自动释放池除了是一个页,还是一个双向链表结构,如下:
class AutoreleasePoolPage;
struct AutoreleasePoolPageData
{
// 用来校验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)
{
}
};
- 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字节(即2*4=8字节)。
② objc_autoreleasePoolPush
- 进入 push 源码实现,如下所示:
// 入栈
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;
}
- push 源码主要逻辑:判断是否为有 pool,如果没有,则通过 autoreleaseNewPage 方法创建;如果有,则通过 autoreleaseFast 压栈哨兵对象。
- 进入 autoreleaseNewPage 的源码实现,可以看到,通过 hotPage 获取当前页,判断当前页是否存在:
-
- 如果存在,则通过 autoreleaseFullPage 方法压栈对象;
-
- 如果不存在,则通过 autoreleaseNoPage 方法创建页;
// 创建新页
static __attribute__((noinline))
id *autoreleaseNewPage(id obj) {
// 获取当前操作页
AutoreleasePoolPage *page = hotPage();
// 如果存在,则压栈对象
if (page) return autoreleaseFullPage(obj, page);
// 如果不存在,则创建页
else return autoreleaseNoPage(obj);
}
// hotPage方法
// 获取当前操作页
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;
}
// 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 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;
}
// 如果对象是哨兵对象,且没有申请自动释放池内存,则设置一个空占位符存储在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);
// 设置page为当前聚焦页
setHotPage(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);
}
- 在 autoreleaseNoPage 方法中,可以发现当前线程的自动释放池是通过 AutoreleasePoolPage 创建,定义中存在构造方法,而构造方法的实现是通过父类 AutoreleasePoolPageData 的初始化方法:
// AutoreleasePoolPage构造方法
AutoreleasePoolPage(AutoreleasePoolPage *newParent) :
AutoreleasePoolPageData(begin(), // 开始存储的位置
objc_thread_self(), // 传的是当前线程,当前线程时通过tls获取的
newParent,
newParent ? 1+newParent->depth : 0, // 如果是第一页深度为0,往后是前一个的深度+1
newParent ? newParent->hiwat : 0)
{
if (parent) {
parent->check();
ASSERT(!parent->child);
parent->unprotect();
// this 表示新建页面,将当前页面的子节点赋值为新建页面
parent->child = this;
parent->protect();
}
protect();
}
// AutoreleasePoolPageData初始化方法
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 的方法参数:
-
- begin() 表示压栈的位置(即下一个要释放对象的压栈地址),可以通过源码调试 begin,发现其具体实现等于页首地址 +56,其中的 56 就是结构体 AutoreleasePoolPageData 的内存大小:
// 页开始位置
id * begin() {
// 等于首地址+56(AutoreleasePoolPage类所占内存大小)
return (id *) ((uint8_t *)this+sizeof(*this));
}
// 调试
p sizeof(*this)
(unsigned long) $0 = 56
p (unit8_t *)this
(unit8_t *) $1 = 0x000000010100e000 <no value available>
-
- objc_thread_self() 表示的是当前线程,而当前线程时通过 tls 获取的:
__attribute__((const))
static inline pthread_t objc_thread_self() {
以上是关于iOS之深入解析自动释放池autoreleasepool的底层原理的主要内容,如果未能解决你的问题,请参考以下文章