iOS深入理解Autoreleasepool(自动释放池)

一、 @autoreleasepool{}

OC代码 复制代码
int main(int argc, const char * argv[]) {
    @autoreleasepool {
    
    }
    return 0;
}

我们平时创建一个main函数的代码的时候,就会发现其中有一个这个东西@autoreleasepool{},使用clang编译之后:@autoreleasepool{...}被编译成了{__AtAutoreleasePool __autoreleasepool; ... }。

这个__AtAutoreleasePool到底是什么?

它其实是一个结构体,在创建__AtAutoreleasePool结构体变量的时候调用了objc_autoreleasePoolPush(void),销毁的时候会调动objc_autoreleasePoolPop(void *),其实构造函数和析构函数,所以我们可以看出其其实是一个C++封装的自动释放池变量,会将@autoreleasepool{...}中{}中的内容添加到自动释放池中,方便内存管理。

C++代码 复制代码
struct __AtAutoreleasePool {
    // 构造函数
  __AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
    // 析构函数
  ~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
  void * atautoreleasepoolobj;
};

二、AutoreleasePoolPage

从上边的__AtAutoreleasePool我们可以看到这两个方法objc_autoreleasePoolPush和objc_autoreleasePoolPop

javascript 复制代码
void *objc_autoreleasePoolPush(void) {
    return AutoreleasePoolPage::push();
}
 
void objc_autoreleasePoolPop(void *ctxt) {
    AutoreleasePoolPage::pop(ctxt);
}

我们可以看出这里又引入了新的类AutoreleasePoolPage

arduino 复制代码
class AutoreleasePoolPage {
    magic_t const magic;//AutoreleasePoolPage 完整性校验
    id *next;//存放下一个autorelease对象的地址
    pthread_t const thread; //AutoreleasePoolPage 所在的线程
    AutoreleasePoolPage * const parent;//父节点
    AutoreleasePoolPage *child;//子节点
    uint32_t const depth;//深度,也可以理解为当前page在链表中的位置
    uint32_t hiwat;
}

每一个自动释放池都是由一系列AutoreleasePoolPage组成的,并且每一个AutoreleasePoolPage的大小都是4096字节(16 进制 0x1000)。

所以我们从上述的源码可以看出,自动释放池其实就是一个由AutoreleasePoolPage构成的双向链表 ,其结构中的child和parent分别指向其前趋和后继。

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

  • 调用push方法会将一个POOL_BOUNDARY入栈,并且返回其存放的内存地址

  • 调用pop方法时传入一个POOL_BOUNDARY的内存地址,会从最后一个入栈的对象开始发送release消息,直到遇到这个POOL_BOUNDARY

  • id *next指向了下一个能存放autorelease对象地址的区域

三、Runloop和Autorelease

AutoreleasePool创建

  • App启动后,苹果在主线程RunLoop里注册了两个Observer,其回调都是_wrapRunLoopWithAutoreleasePoolHandler()。
  • 第一个Observer监视的事件是Entry(即将进入Loop),其回调内会调用
    _objc_autoreleasePoolPush() 创建自动释放池。其 order 是-2147483647,优先级最高,保证创建释放池发生在其他所有回调之前。

AutoreleasePool释放

  • 第二个Observer监视了两个事件: BeforeWaiting(准备进入休眠)
    时调用_objc_autoreleasePoolPop()和_objc_autoreleasePoolPush()释放旧的池并创建新池;Exit(即将退出Loop)时调用 _objc_autoreleasePoolPop()来释放自动释放池。这个Observer 的 order 是 2147483647,优先级最低,保证其释放池子发生在其他所有回调之后。
  • 在主线程执行的代码,通常是写在诸如事件回调、Timer回调内的。这些回调会被 RunLoop 创建好的AutoreleasePool环绕着,所以不会出现内存泄漏。

四、面试题AutoreleasePool的原理

  • 自动释放池的本质是一个AutoreleasePoolPage结构体对象,是一个栈结构存储的页,每一个AutoreleasePoolPage都是以双向链表的形式连接。

  • 自动释放池的压栈和出栈主要是通过结构体的构造函数和析构函数调用底层的objc_autoreleasePoolPush和objc_autoreleasePoolPop,实际上是调用AutoreleasePoolPage的push和pop两个方法。

  • 调用push操作其实就是创建一个新的AutoreleasePoolPage,而AutoreleasePoolPage的具体操作就是插入一个哨兵POOL_BOUNDARY,并返回插入哨兵POOL_BOUNDARY的内存地址。而push内部调用autoreleaseFast方法处理,主要有以下三种情况:

    • a.当page存在,且不满时,调用add方法将对象添加至page的next指针处,并将next指向下一位;
    • b.当page存在,且已满时,调用autoreleaseFullPage初始化一个新的page,然后调用add方法将对象添加至page栈中;
    • c.当page不存在时,调用autoreleaseNoPage创建一个hotPage,然后调用add方法将对象添加至page栈中。
  • 调用pop操作时,会传入一个值,这个值就是push操作的返回值,即哨兵POOL_BOUNDARY的内存地址token。所以pop内部的实现就是根据token找到哨兵对象所处的page中,然后使用 objc_release 释放 token之前的对象,并把next 指针到正确位置。

相关推荐
软泡芙3 小时前
【iOS】swift启动屏显示黑屏
ios
用户69371750013845 小时前
当你向领导申请 AI 编程工具预算时,如果领导追问“能提效多少,有量化数据吗”,你该怎么回答?
android·后端·ios
Layer5 小时前
一些脱离 Mac 依赖的 Agent 操作 iPhone 实现
ios·ai编程·测试
Rain的Java大神之路6 小时前
MySQL 被高并发打爆?从定位到分库分表逐层拆解(含秒杀库存实战)
java·后端·面试
用户69371750013846 小时前
当我说 Claude 搭配 GLM-5.3-Flash 太好用了,同事却提醒我:你可能正在失去对代码的掌控
android·后端·ios
彭亚川Allen7 小时前
从零手搓 RAG 之分块策略
前端·后端·面试
茶底世界之下7 小时前
一键裁方会丢掉什么?用 Awaking 给园林照片做两种画幅
ios
利威尔xu8 小时前
Go GMP 调度器 10 个高频追问:3 个线上故障让 P99 从 80ms 飙到 2s、10 万 goroutine 泄漏
开发语言·后端·面试·性能优化·golang
Marco_Marco1358 小时前
定位SDK接入实战:Android/iOS/鸿蒙三端怎么选、怎么接
android·ios·harmonyos
老李IT笔记1 天前
关掉设备上的 AI 扩展:两个键各管一层,配错了等于没配
人工智能·ios·智能手机