iOS-自动释放池
一、什么是自动释放池
- OC中的一种
内存自动回收机制
,它可以延迟加入AutoreleasePool
中的变量release
的时机 - 当创建一个对象,在正常情况下,变量会在
超出其作用域时
立即 release ,如果将其加入到自动释放池中,这个对象并不会立即释放,而会等到runloop 休眠 / 超出autoreleasepool作用域之后
进行释放
- 从程序启动到加载完成,主线程对应的 Runloop 会处于休眠状态,等待用户交互来唤醒 Runloop
- 用户每次交互都会启动一次 Runloop ,用于处理用户的所有点击、触摸等事件
- Runloop 在监听到交互事件后,就会创建自动释放池,并将所有延迟释放的对象添加到自动释放池中
- 在一次完整的 Runloop 结束之前,会向自动释放池中所有对象发送 release 消息,然后销毁自动释放池
二、Autorelease的底层实现
#import <Foundation/Foundation.h> int main(int argc, const char * argv[]) { @autoreleasepool { NSLog(@"Hello, World!"); } return 0; }
将其转化为cpp文件:
int main(int argc, const char * argv[]) { /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; NSLog((NSString *)&__NSConstantStringImpl__var_folders_jl_d06jlfkj2ws74_5g45kms07m0000gn_T_main_da0d58_mi_0); } return 0; }
将autorelease变成了_AtAutorelease类型声明的代码:我们可以看到其中调用了objc_autoreleasePoolPush和objc_autoreleasePoolop
struct __AtAutoreleasePool { __AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();} ~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);} void * atautoreleasepoolobj; };
2.1 objc_autoreleasePoolPush的底层结构
首先我们看一下objc_autoreleasePoolPush的底层结构:我们可以看到这里出现了一个新的函数命名空间autoreleasePoolPage
void * objc_autoreleasePoolPush(void) { // 调用 AutoreleasePoolPage 命名空间下的 push 函数 return AutoreleasePoolPage::push(); }
2.1.1 AutoreleasePoolPage
/*********************************************************************** 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自动释放池边界;超出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 的指针。当池被弹出时,每个比哨兵更热的对象都会被释放。(在调用pop函数的时候,整个自动释放池里面的对象都会被释放。) The stack is divided into a doubly-linked list of pages. Pages are added and deleted as necessary. 堆栈被分成一个双链接的页面列表。根据需要添加和删除页面(自动释放池是一个双向列表 的栈结构。它的每一个节点都是AutoreleasePoolPage) Thread-local storage points to the hot page, where newly autoreleased objects are stored. 线程本地存储指向热页,其中存储新自动释放的对象(线程缓存也有自动释放池里面的数据)
接着,我们来看一下 AutoreleasePoolPage的结构:AutoreleasePoolPage继承自AutoreleasePoolData(一些有用的信息都在Data中)
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: static pthread_key_t const key = AUTORELEASE_POOL_KEY; static uint8_t const SCRIBBLE = 0xA3; // 0xA3A3A3A3 after releasing static size_t const COUNT = SIZE / sizeof(id); static size_t const MAX_FAULTS = 2; ... }
2.1.2 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) { } };
我们来详细看一下AutoreleasePoolData结构体中存储的信息(可以看出一个page占据56个字节的大小):
magic:用来校验 AutoreleasePoolPage 的结构是否完整(16字节)
next:指向最新添加的autoreleased对象的下一个位置,初始化时执行begin():获取对象压栈的起始位置(8字节)
thread:指向当前线程(8字节)
parent:指向父节点,第一个节点的parent值为nil(8字节)
child:指向子节点,最后一个节点的child值为nil(8字节)
depth:代表深度,从0开始,往后递增1(4字节)
hiwat:代表high water mark最大入栈数量标记(4字节)
2.1.3 Push函数
了解完AutoreleasePoolPage,接下来我们看一下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; }
DebugPoolAllocation
:当自动释放池按顺序弹出时停止,并允许堆调试器跟踪自动释放池- 如果自动释放池不存在,就调用
autoreleaseNewPage
函数,创建新页 - 如果自动释放池存在,调用autoreleaseFast函数,将哨兵对象压栈
(1)autoreleaseNewPage的实现:
-
autoreleaseNewPage先从线程的缓存中找hotpage
- 如果找到调用FullPage;如果满了就会一直做while循环直到找到空白页,将当前页设置为热页,然后将对象加入当前页
- 如果没有找到调用NoPage
(2)AutoreleaseFast
- 若存在 page,且未存满,调用
add
函数 - 若存在 page,但已存满,调用
autoreleaseFullPage
函数(与newPage一样):
1.遍历链表,找到最后一个空白的子页面 2.对其进行创建新页 3.设置为热页面 4.添加对象
- 如果不存在Page,就调用autoreleaseNoPage;然后按照下面的步骤走,走完之后,再设置为热页,然后将
pushExtraBoundary
为YES
,并哨兵对象压栈,对象压栈。
1.通过父类 AutoreleasePoolPageData 进行初始化 2.begin:获取对象压栈的起始位置(sizeof(*this):大小取决于自身结构体中的成员变量、返回对象可压栈的真正开始地址,在成员变量以下) 3.objc_thread_self:通过tls获取当前线程 4.链接双向链表
2.1.4 自动释放池的具体结构(参考自:https://juejin.cn/post/7114677313718976519#heading-3):页之间通过parent、child的指针双向链接;页的深度依次加一。
2.1.5 池页容量
int main(int argc, const char * argv[]) { @autoreleasepool { for (int i = 0; i < 505; i++) { NSObject *objc = [[[NSObject alloc] init] autorelease]; } _objc_autoreleasePoolPrint(); } return 0; } ------------------------- objc[1804]: ############## objc[1804]: AUTORELEASE POOLS for thread 0x1000ebe00 objc[1804]: 506 releases pending. objc[1804]: [0x10200c000] ................ PAGE (full) (cold) objc[1804]: [0x10200c038] ################ POOL 0x10200c038 objc[1804]: [0x10200c040] 0x100638420 NSObject objc[1804]: [0x10200c048] 0x100637a40 NSObject objc[1804]: [0x10200c050] 0x100636970 NSObject ... objc[1804]: [0x100809000] ................ PAGE (hot) objc[1804]: [0x100809038] 0x10063a0b0 NSObject objc[1804]: ##############
- 可以看出当第505个对象加入自动释放池的时候,就会开辟新的页存储,可知一个自动释放池可存放504个对象
- 一页的大小 = 504 * 8 +56(存储信息) + 8(哨兵) = 4096字节 = 4KB
- 一个自动释放池只会压栈一个哨兵对象
2.2 autoreleasePop
void objc_autoreleasePoolPop(void *ctxt) { AutoreleasePoolPage::pop(ctxt); }
2.2.1 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. //不存在热页面,将标记设置为nil return setHotPage(nil); } // Pool was used. Pop its contents normally. // Pool pages remain allocated for re-use as usual. //存在热页面,通过双向链表循环向上找到最冷页面 page = coldPage(); //将token设置为起始位置 token = page->begin(); } else { //获取token所在的页 page = pageForPointer(token); } //赋值给stop 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 //最冷页面的起始可能不是POOL_BOUNDARY: //1. 弹出顶级池,保留冷页面 //2. 对象在没有池的情况下被自动释放 } 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); }
2.3.2 popPage
tatic void popPage(void *token, AutoreleasePoolPage *page, id *stop) { if (allowDebug && PrintPoolHiwat) printHiwat(); //当前页中对象出栈,到stop位置停止 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 //销毁当前页面 page->kill(); //将热页面标记设置为nil 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(); } } }
2.3.3 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 //向下遍历,到stop停止 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 }
2.3.4 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) { //将子页面设置为nil page->unprotect(); page->child = nil; page->protect(); } //销毁子页面 delete deathptr; //遍历销毁到this为止 } while (deathptr != this); }
三、autoreleasepool的嵌套使用:在嵌套使用的时候,添加好各自堆栈的哨兵对象,出栈时,先释放内部,在释放外部。
int main(int argc, const char * argv[]) { @autoreleasepool { NSObject *objc = [[[NSObject alloc] init] autorelease]; @autoreleasepool { NSObject *objc = [[[NSObject alloc] init] autorelease]; } _objc_autoreleasePoolPrint(); } return 0; } ------------------------- objc[2511]: ############## objc[2511]: AUTORELEASE POOLS for thread 0x1000ebe00 objc[2511]: 4 releases pending. objc[2511]: [0x10680d000] ................ PAGE (hot) (cold) objc[2511]: [0x10680d038] ################ POOL 0x10680d038 objc[2511]: [0x10680d040] 0x101370c40 NSObject objc[2511]: [0x10680d048] ################ POOL 0x10680d048 objc[2511]: [0x10680d050] 0x101365fb0 NSObject objc[2511]: ##############
四、总结
4.1 基本结构
1.自动释放池的压栈和出栈,通过结构体的构造函数和析构函数触发
2.压栈:调用objc_autoreleasePoolPush函数
3.出栈:调用objc_autoreleasePoolPop函数
4.2 特点
1.自动释放池是一个存储指针的栈结构
2.指针要么是一个要释放的对象,要么是POOL_BOUNDARY自动释放池边界,俗称:哨兵对象
3.哨兵对象的作用:当自动释放池将对象进行pop操作时,需要知道边界在哪里,否则会破坏别人的内存空间。而哨兵对象,就是边界标识
4.自动释放池的栈空间被分成一个 双链接 结构的页面列表,可添加和删除页面
5.双向链表的特别,一个页中同时存在父节点和子节点。可向前找到父页面,也可向后找到子页面
6.线程本地存储指向热页,其中存储新自动释放的对象
7.栈原则,先进后出,可以理解为最后一个页面就是热页。里面的对象最后被push,最先被pop
4.3 容量
池页大小为4096字节,每一页都包含56字节的成员变量,但一个自动释放池中,只会压栈一个哨兵对象,占8字节
4.4 原理
1.自动释放池的本质是__AtAutoreleasePool结构体,包含构造函数和析构函数
2.结构体声明,触发构造函数,调用objc_autoreleasePoolPush函数,本质是对象压栈的push方法
3.当结构体出作用域空间,触发析构函数,调用objc_autoreleasePoolPop函数,本质是对象出栈的pop方法
4.4.1 对象压栈:Push函数
如果存在page,并且没有存满,调用add函数 使用*next++进行内存平移 将对象压栈 如果存在page,但存储已满,调用autoreleaseFullPage函数 遍历链表,找到最后一个空白的子页面 对其进行创建新页 设置为热页面 添加对象 否则,不存在page,调用autoreleaseNoPage函数 通过父类AutoreleasePoolPageData进行初始化 begin:获取对象压栈的起始位置 objc_thread_self:通过tls获取当前线程 链接双向链表 设置为热页面 pushExtraBoundary为YES,哨兵对象压栈 对象压栈
4.4.2 对象出栈:Pop函数
调用popPage函数,传入stop为哨兵对象的位置
当前页中对象出栈,到stop位置停止
调用kill函数,销毁当前页面
4.5 嵌套使用
线程的自动释放池是一个指针堆栈,当嵌套使用时,添加好各自堆栈的哨兵对象。出栈时,先释放内部,再释放外部
4.6 其他注意点
4.6.1 是所有的对象都会被加入到自动释放池中嘛?
1. 使用alloc、new、copy、mutableCopy前缀开头的方法进行对象创建,不会加入到自动释放池;它们的空间开辟由开发者申请,释放也由开发者进行管理 2.还有一些由TaggerPoint管理的小对象也不会加入到自动释放池中
4.6.2 自动释放池和runloop的关系
runloop会自动开启autorelease 1.主程序在事件循环的每个循环开始时在主线程上创建一个自动释放池 2.并在结束时将其排空,从而释放在处理事件时生成的任何自动释放对象
4.6.3 自动释放池的使用
1.现在的ARC模式下只能通过调用@autorelease来将对象加入到自动释放池中; 2.像NSAutorealease已经不能使用了; 3 还有autorelease的方法也不能使用了 4. 代码写在主线程中的没有添加autoreleasepool{}中的对象也会加入自动释放池,因为主线程有runloop,runloop会自动开启autorelease
4.6.4 autorelease的好处
for循环中大量创建对象时,使用 autorelease 可以有效控制内存的快速增长(虽然不添加autorelease也会调用delloc销毁对象,但是这样的释放没有创建快,内存虽然最后也会降下来,但是如果加autorelease 可以减少内存峰值)
for (int i = 0; i<100000000; i++) { @autoreleasepool { NSLog(@"%d",i); __autoreleasing LZPerson *p =[LZPerson new]; } }