当前位置: 首页 > news >正文

「iOS」————自动释放池底层原理

iOS学习

  • AutoReleasePool底层原理及总结
    • 自动释放池
      • 生命周期
      • 结构
        • 自动释放池中的栈
        • POOL_SENTINEL(哨兵对象)
      • 实现
        • 入栈
          • autoreleaseFast()
          • page->add 添加对象
          • autoreleaseFullPage(当前 hotPage 已满)
          • autoreleaseNoPage(没有 hotPage)
        • 出栈
          • objc_autoreleasePoolPop
          • AutoreleasePoolPage::pop
          • pageForPointer 获取 AutoreleasePoolPage
          • releaseUntil 释放对象
          • kill() 方法
    • 总结
    • 冷页(coldPage)和热页(hotPage)


AutoReleasePool底层原理及总结

自动释放池

AutoreleasePool自动释放池用来延迟对象的释放时机,将对象加入到自动释放池后,这个对象不会立即释放,等到自动释放池销毁后才会将里面的对象释放

AutoreleasePool机制图示

生命周期

  1. 从程序启动到加载完成,主线程对应的runloop会处于休眠状态,等待用户来唤醒runloop
  2. 用户的每一次交互都会开启一次runloop,用于处理用户的所有点击、触摸事件
  3. runloop在监听到交互事件后,就会创建自动释放池,并将所有延迟释放的对象添加到自动释放池中
  4. 在一次完整的runloop结束之前,会将自动释放池中所有对象发送releae消息,然后销毁自动释放池。

总结一下:首先一个Observer监视Entry,在即将进入Loop时,创建自动释放池,并且这件事优先级最高,确保创建自动释放池发生在其他所有回调之前。然后另一个Observer监视:BeforeWaiting(准备进入休眠)和Exit(即将退出Loop)。

BeforeWaiting调用_objc_autoreleasePoolPop()

Exit调用 _objc_autoreleasePoolPush() 释放旧的池并创建新池;

结构

自动释放池其本质也是一个对象,其类型__AtAutoreleasePool是一个结构体,有构造函数 + 析构函数,结构体定义的对象在作用域结束后,会自动调用析构函数。在创建时,一般也会调用构造函数

每一个AutorealeasePool都是由一系列的 AutoreleasePoolPage 组成的,并且一个page的大小是4096字节。而在其构造函数中对象的压栈位置,是从首地址+56开始的,所以可以一页中实际可以存储 4096-56 = 4040字节,转换成对象是 4040 / 8 = 505个,即一页最多可以存储505个对象,其中第一页有哨兵对象只能存储504个。

AutorealeasePool就是由AutoreleasePoolPage构成的双向链表,AutoreleasePoolPage是双向链表的节点

img

AutoreleasePoolPage的定义如下:

class AutoreleasePoolPage 
{//magic用来校验AutoreleasePoolPage的结构是否完整magic_t const magic;                   // 16字节//指向最新添加的autoreleased对象的下一个位置,初始化时指向begin();id *next;                              // 8字节//thread指向当前线程pthread_t const thread;                // 8字节//parent指向父节点,第一个节点的parent指向nil;AutoreleasePoolPage * const parent;    // 8字节 //child 指向子节点,第一个节点的child指向nil;AutoreleasePoolPage *child;            // 8字节//depth 代表深度,从0开始往后递增1;uint32_t const depth;                  // 4字节//hiwat 代表high water mark;uint32_t hiwat;                        // 4字节...
}
  • magic 检查校验完整性的变量
  • next 指向新加入的autorelease对象
  • thread page当前所在的线程,AutoreleasePool是按线程一一对应的(结构中的thread指针指向当前线程)
  • parent 父节点 指向前一个page
  • child 子节点 指向下一个page
  • depth 链表的深度,节点个数
  • hiwat high water mark 数据容纳的一个上限
  • EMPTY_POOL_PLACEHOLDER 空池占位
  • POOL_BOUNDARY 是一个边界对象 nil,之前的源代码变量名是 POOL_SENTINEL哨兵对象,用来区别每个page即每个 AutoreleasePoolPage 边界
  • PAGE_MAX_SIZE = 4096, 为什么是4096呢?其实就是虚拟内存每个扇区4096个字节,4K对齐的说法。
  • COUNT 一个page里对象数
自动释放池中的栈

如果我们的一个 AutoreleasePoolPage 被初始化在内存的 0x100816000 ~ 0x100817000 中,它在内存中的结构如下:

在这里插入图片描述

其中有 56 bit 用于存储 AutoreleasePoolPage 的成员变量,剩下的 0x100816038 ~ 0x100817000 都是用来存储加入到自动释放池中的对象

begin()end() 这两个类的实例方法帮助我们快速获取 0x100816038 ~ 0x100817000 这一范围的边界地址。

next 指向了下一个为空的内存地址,如果 next 指向的地址加入一个 object,它就会如下图所示移动到下一个为空的内存地址中

POOL_SENTINEL(哨兵对象)

POOL_SENTINEL就是一个哨兵对象,它是一个宏,值为nil,标志着一个自动释放池的边界。

在每个自动释放池初始化调用 objc_autoreleasePoolPush 的时候,都会把一个 POOL_SENTINEL push 到自动释放池的栈顶,并且返回这个 POOL_SENTINEL 哨兵对象。

而当方法 objc_autoreleasePoolPop 调用时,就会向自动释放池中的对象发送 release 消息,直到第一个 POOL_SENTINEL

在这里插入图片描述

@autoreleasepool { // 外层池id a = ...; // Aid b = ...; // B@autoreleasepool { // 内层池id c = ...; // Cid d = ...; // D} // 内层池销毁id e = ...; // Eid f = ...; // F
} // 外层池销毁

对于以上代码,在内层池销毁前,链表的存储类似D->C->S2->B->A->S1。其中S为哨兵节点,销毁时外层池时,遇到第一个哨兵节点停止

注意: 每个自动释放池只有一个哨兵对象,并且哨兵对象在第一页。

实现

在我们的编程中,每一个main文件都有以下代码:

int main(int argc, char * argv[]) {@autoreleasepool {return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));}
}

在整个main函数中,只有一个autoreleasepool块,在块中之包含了一行代码,这行代码将所有的事件、消息全部交给了UIApplication 来处理也就是说整个 iOS 的应用都是包含在一个autoreleasepool的 block 中的

上述的aotuoreleasepool 通过源码编译发现,被转换为为一个 __AtAutoreleasePool 结构体:

struct __AtAutoreleasePool {__AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}void * atautoreleasepoolobj;
};

这个结构体会在初始化时调用 objc_autoreleasePoolPush() 方法,会在析构时调用 objc_autoreleasePoolPop 方法。

至此,我们可以分析出,单个自动释放池的执行过程就是objc_autoreleasePoolPush() —> [object autorelease] —> objc_autoreleasePoolPop(void *)

因此得到这个main函数的实际工作逻辑:

int main(int argc, const char * argv[]) {{void * atautoreleasepoolobj = objc_autoreleasePoolPush();// do whatever you wantobjc_autoreleasePoolPop(atautoreleasepoolobj);}return 0;
}

所以autoreleasepool的实现主要靠objc_autoreleasePoolPush()objc_autoreleasePoolPop() 来实现

void *objc_autoreleasePoolPush(void) {return AutoreleasePoolPage::push();
}void objc_autoreleasePoolPop(void *ctxt) {AutoreleasePoolPage::pop(ctxt);
}

而在objc_autoreleasePoolPush()objc_autoreleasePoolPop() 中又分别调用了AutoreleasePoolPage类的push和pop方法。

入栈

在这里插入图片描述

objc_autoreleasePoolPush()

首先调用objc_autoreleasePoolPush()

void *objc_autoreleasePoolPush(void) {return AutoreleasePoolPage::push();
}

AutoreleasePoolPage::push()

static inline void *push() {return autoreleaseFast(POOL_SENTINEL);
}

该函数就是调用了关键的方法 autoreleaseFast,并传入哨兵对象POOL_SENTINEL

autoreleaseFast()
static inline id *autoreleaseFast(id obj){//1. 获取当前操作页AutoreleasePoolPage *page = hotPage();//2. 判断当前操作页是否满了if (page && !page->full()) {//如果未满,则压桟return page->add(obj);} else if (page) {//如果满了,则安排新的页面return autoreleaseFullPage(obj, page);} else {//页面不存在,则新建页面return autoreleaseNoPage(obj);}}

hotPage 可以为当前正在使用的 AutoreleasePoolPage

上述代码一共分为三种情况

  • hotPage 并且当前 page 不满

调用 page->add(obj) 方法将对象添加至 AutoreleasePoolPage 的栈中

  • hotPage 并且当前 page 已满

调用 autoreleaseFullPage 初始化一个新的页接着调用 page->add(obj) 方法将对象添加至 AutoreleasePoolPage 的栈中

  • 无hotPage

调用 autoreleaseNoPage 创建一个 hotPage,接着调用 page->add(obj) 方法将对象添加至 AutoreleasePoolPage 的栈中

总而言之就是最后调用 page->add(obj) 将对象添加到自动释放池中

page->add 添加对象
//入桟对象
id *add(id obj){ASSERT(!full());unprotect();//传入对象存储的位置(比' return next-1 '更快,因为有别名)id *ret = next; //将obj压桟到next指针位置,然后next进行++,即下一个对象存储的位置*next++ = obj;protect();return ret;}

压栈操作,将对象加入 AutoreleasePoolPage 然后移动栈顶的指针

autoreleaseFullPage(当前 hotPage 已满)
static id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page) {do {if (page->child) page = page->child;else page = new AutoreleasePoolPage(page);} while (page->full());setHotPage(page);return page->add(obj);
}

从传入的 page 开始遍历整个双向链表,直到查找到一个未满的 AutoreleasePoolPage

如果找到最后还是没找到创建一个新的 AutoreleasePoolPage

将找到的或者构建的page标记成 hotPage,然后调动上面分析过的 page->add 方法添加对象。

autoreleaseNoPage(没有 hotPage)
static id *autoreleaseNoPage(id obj) {AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);setHotPage(page);if (obj != POOL_SENTINEL) {page->add(POOL_SENTINEL);}return page->add(obj);
}

创建一个新的page,并且将新的page设置为hotpage。接着调用page->add 方法添加POOL_SENTINEL 对象,来确保在 pop 调用的时候,不会出现异常。最后,将 obj 添加到autoreleasepool中

既然当前内存中不存在 AutoreleasePoolPage,就要从头开始构建这个自动释放池的双向链表,也就是说,新的 AutoreleasePoolPage没有 parent 指针的。

出栈

在这里插入图片描述

objc_autoreleasePoolPop
void objc_autoreleasePoolPop(void *ctxt) {AutoreleasePoolPage::pop(ctxt);
}

该方法传入的参数是push压栈后返回的哨兵对象,即ctxt,其目的是避免出栈混乱,防止将别的对象出栈

但是传入不是哨兵对象而是传入其它的指针也是可行的,会将自动释放池释放到相应的位置。

  • pop源码实现,主要由以下几步
    • 空页面的处理,并根据token获取page
    • 容错处理
    • 通过popPage出栈页
//出栈
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);
}
  1. 判断 token 是否为“空占位符”):

    • 是:处理 hotPage/coldPage 逻辑。

    • 否:找到 token 所在的 page。

  2. 检查 stop 是否为哨兵(POOL_BOUNDARY):

    • 不是:如果是最冷页起始位置且无父节点,正常;否则报错。
  3. .是否调试模式:

    • 是:走调试分支。

    • 否:走正常分支。

  4. 调用 popPage 释放对象。

空占位符(EMPTY_POOL_PLACEHOLDER)

  • 作用:标记“最外层 pool”的存在(即你还没有真正的 autorelease pool,只是做了个占位)。

  • 存储位置:通常存储在 TLS(线程本地存储)中,作为 pool stack 的初始值。

  • 什么时候用:当你还没有创建任何 autorelease pool 时,系统会用 EMPTY_POOL_PLACEHOLDER 作为 pool stack 的初始标记。这样 pop 操作时能区分“真的 pool”还是“只是个占位符”。

  • 不是实际存储在 page 里的对象,而是 pool stack 的特殊标记。

AutoreleasePoolPage::pop
template<bool allowDebug>static voidpopPage(void *token, AutoreleasePoolPage *page, id *stop){if (allowDebug && PrintPoolHiwat) printHiwat();//出桟当前操作页面对象page->releaseUntil(stop);// 删除空子项if (allowDebug && DebugPoolAllocation  &&  page->empty()) {//特殊情况:在每个页面池调试期间删除所有内容//获取当前页面AutoreleasePoolPage *parent = page->parent;//将当前页面杀掉page->kill();//设置父节点页面为当前操作页setHotPage(parent);} else if (allowDebug && DebugMissingPools  &&  page->empty()  &&  !page->parent) {//特殊情况:当调试丢失的自动释放池时,删除所有pop(top)page->kill();setHotPage(nil);} else if (page->child) {//滞后:如果页面超过一半是满的,则保留一个空的子节点if (page->lessThanHalfFull()) {page->child->kill();}else if (page->child->child) {page->child->child->kill();}}}

传入的allowDebug为false则通过releaseUntil出栈当前页stop位置之前的所有对象,即向栈中的对象发送release消息,直到遇到传入的哨兵对象。就是将这整个池释放掉。

该静态方法总共做了三件事情:

  1. 使用 pageForPointer 获取当前 token 所在的 AutoreleasePoolPage
  2. 调用 releaseUntil 方法释放栈中的对象,直到 stop
  3. 调用 childkill 方法

注意:该方法是一个析构函数,即在释放的时候使用。因此:

  • pop之后,所有child page肯定都为空了,且当前page一定是hotPa
  • 系统为了节约内存,判断,如果当前page空间使用少于一半,就释放掉所有的child page,如果当前page空间使用大于一半,就从孙子page开始释放,预留一个child page
pageForPointer 获取 AutoreleasePoolPage

pageForPointer 方法主要是通过内存地址的操作,获取当前指针所在页的首地址:

static AutoreleasePoolPage *pageForPointer(const void *p) {return pageForPointer((uintptr_t)p);
}static AutoreleasePoolPage *pageForPointer(uintptr_t p) {AutoreleasePoolPage *result;uintptr_t offset = p % SIZE;assert(offset >= sizeof(AutoreleasePoolPage));result = (AutoreleasePoolPage *)(p - offset);result->fastcheck();return result;
}

将指针与页面的大小,也就是 4096 取模,得到当前指针的偏移量,因为所有的 AutoreleasePoolPage 在内存中都是对齐的:

p = 0x100816048
p % SIZE = 0x48(SIZE为4096, 0x1000)
result = 0x100816000 :通过 p - (p % SIZE) 得到当前 page 的起始地址(基址)。

而最后调用的方法 fastCheck() 用来检查当前的 result 是不是一个 AutoreleasePoolPage

通过检查 magic_t 结构体中的某个成员是否为 0xA1A1A1A1

releaseUntil 释放对象
void releaseUntil(id *stop) 
{// 这里没有使用递归, 防止发生栈溢出while (this->next != stop) { // 一直循环到 next 指针指向 stop 为止// Restart from hotPage() every time, in case -release // autoreleased more objectsAutoreleasePoolPage *page = hotPage(); // 取出 hotPagewhile (page->empty()) { // 从节点 page 开始, 向前找到第一个非空节点page = page->parent; // page 非空的话, 就向 page 的 parent 节点查找setHotPage(page); // 把新的 page 节点设置为 HotPage}page->unprotect(); // 如果需要的话, 解除 page 的内存锁定id obj = *--page->next; // 先将 next 指针向前移位, 然后再取出移位后地址中的值memset((void*)page->next, SCRIBBLE, sizeof(*page->next)); // 将 next 指向的内存清空为SCRIBBLEpage->protect(); // 如果需要的话, 设置内存锁定if (obj != POOL_BOUNDARY) { // 如果取出的对象不是边界符objc_release(obj); // 给取出来的对象进行一次 release 操作}}setHotPage(this); // 将本节点设置为 hotPage#if DEBUG// we expect any children to be completely emptyfor (AutoreleasePoolPage *page = child; page; page = page->child) {assert(page->empty());}
#endif
}

主要是通过循环遍历,判断对象是否等于stop,其目的是释放stop之前的所有的对象,

调用者是用 pageForPointer() 找到的, token 所在的 page 节点, 参数为 token. 这个函数主要操作流程就是, 从 hotPage 开始, 使用 next 指针遍历存储在节点里的 autorelease 对象列表, 对每个对象进行一次 release 操作, 并且把 next 指向的指针清空, 如果 hotPage 里面的对象全部清空, 则继续循环向前取 parent 并继续用 next 指针遍历 parent, 一直到 next 指针指向的地址为 token 为止. 因为 token 就在 this 里面, 所以这个时候的 hotPage 应该是 this.

kill() 方法
void kill() 
{// 这里没有使用递归, 防止发生栈溢出AutoreleasePoolPage *page = this; // 从调用者开始while (page->child) page = page->child; // 先找到最后一个节点AutoreleasePoolPage *deathptr;do { // 从最后一个节点开始遍历到调用节点deathptr = page; // 保留当前遍历到的节点page = page->parent; // 向前遍历if (page) { // 如果有值page->unprotect(); // 如果需要的话, 解除内存锁定page->child = nil; // child 置空page->protect(); // 如果需要的话, 设置内存锁定}delete deathptr; // 回收刚刚保留的节点, 重载 delete, 内部调用 free} while (deathptr != this);
}

自动释放池中需要 release 的对象都已操作完成, 此时 hotPage 之后的 page 节点都已经清空了, 需要把这些节点的内存都回收, 操作方案就是从最后一个节点, 遍历到调用者节点, 挨个回收

总结

结构上:

  • 自动释放池是由 AutoreleasePoolPage 以双向链表的方式实现的,每一个AutoreleasePoolPage所占内存大小为4096字节,其中56字节用于存储结构体中的成员变量。
  • autoreleasepool在初始化时,内部是调用objc_autoreleasePoolPush方法
  • autoreleasepool在调用析构函数释放时,内部是调用objc_autoreleasePoolPop方法

入栈

在页中压栈普通对象主要是通过next指针递增进行的

  • 当没有pool,即只有空占位符(存储在tls中)时,则创建页,压栈哨兵对象

  • 当页未满,将autorelease对象插入到栈顶next指针指向的位置(向一个对象发送autorelease消息,就是将这个对象加入到当前AutoreleasePoolPage的栈顶next指针指向的位置)

  • 当页满了(next指针马上指向栈顶),建立下一页page对象,设置页的child对象为新建页,新page的next指针被初始化在栈底(begin的位置),下次可以继续向栈顶添加新对象。

请添加图片描述

出桟(pop)

在页中出栈普通对象主要是通过next指针递减进行的

  • 根据传入的哨兵对象地址找到哨兵对象所处的page

  • 在当前page中,将晚于哨兵对象插入的所有autorelease对象都发送一次- release消息,并向回移动next指针到正确位置.(从最新加入的对象一直向前清理,可以向前跨越若干个page,直到哨兵所在的page(在一个page中,是从高地址向低地址清理))

  • 当页空了时,需要赋值页的parent对象为当前页

pop流程

冷页(coldPage)和热页(hotPage)

AutoreleasePoolPage 的结构

AutoreleasePool 的实现是一个双向链表结构,每个 page(页)可以存放一批 autorelease 的对象。多个 page 连接起来,形成一个“池栈”。

  • hotPage:当前正在使用的、最顶层的 page。新 autorelease 的对象会被加到 hotPage 上。

  • coldPage:最底层的 page(链表的头部),也就是最早分配的 page,通常在整个进程生命周期内都不会被释放,作为池的“根”。

http://www.dtcms.com/a/319085.html

相关文章:

  • CSS包含块与百分比取值机制完全指南
  • 数据分析——Pandas库
  • 添加内容溢出时显示完整内容提示的功能
  • QT5.15 mingw
  • c++之 栈浅析
  • Python 数据类型及数据类型转换
  • platform总线简介和使用场景说明
  • 基于Ruby的IP池系统构建分布式爬虫架构
  • 《算法导论》第 9 章 - 中位数和顺序统计量
  • 网页图片视频一键下载+视频去重修改 ,覆盖B站等多个平台
  • 【基础知识】springboot+vue 基础框架搭建(更新中)
  • 中国MCP市场:腾讯、阿里、百度的本土化实践
  • AI绘画:生成唐初李世民全身像提示词
  • 前后端加密传数据实现方案
  • 强反光干扰下读数误差↓79%!陌讯多模态算法在仪表盘识别场景的落地优化​
  • LINUX-文件查看技巧,重定向以及内容追加,man及echo的使用
  • 迅为RK3588开发板Android proc文件系统查询-内核版本查询
  • PyTorch RNN 名字分类器
  • 11-netty基础-手写rpc-支持多序列化协议-03
  • 【MySQL基础篇】:MySQL事务并发控制原理-MVCC机制解析
  • qt的元对象系统详解
  • 2深度学习Pytorch-神经网络--全连接神经网络、数据准备(构建数据类Dataset、TensorDataset 和数据加载器DataLoader)
  • Activiti 中各种 startProcessInstance 接口之间的区别
  • [激光原理与应用-169]:测量仪器 - 皮秒激光器研发过程中所需要的测量仪器
  • 2025年机械工程与自动化技术国际会议(ICMEAT 2025)
  • 力扣 hot100 Day68
  • 主流小程序 SaaS 平台测评,2025年小程序开发避坑指南
  • 移动端录屏需求调研:以小熊录屏为例的轻量级实现方案
  • .NET9 AOT完全自举了吗?
  • 面向对象之类方法,成员变量和局部变量