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的底层原理的主要内容,如果未能解决你的问题,请参考以下文章

iOS之深入解析weak关键字的底层原理

ios自动释放池

iOS之深入解析内存管理MRC与ARC机制

读书笔记iOS-自动释放池

iOS自动释放池_原理_如何工作

iOS自动释放池_原理_如何工作