iOS 内存管理之 autoreleasePool

一、autoreleasePool是个啥

  • 一句话总结自动释放池本质就是把一筐对象延迟release;
  • MRC环境下,通过 [obj autorelease] 来延迟内存的释放;
  • ARC环境下,是不能手动调用,系统会自动给对象添加autorelease;

二、autoreleasePool原理

先简单总结下(ps:以下解释略显抽象):

  1. 首先有个筐用来记录要延迟释放的对象,这个筐的结构是由若干个以AutoreleasePoolPage对象为结点的双向链表组成;
  2. 其次得有个方法往这个筐里装对象,也还得有对应从这个筐里批量释放的对象的方法;那这两个方法可以解析源码发现push和pop方法;

这样,就可以实现延迟释放对象的能力;

三、源码赏析

ps:md写的时候,想从苹果开发开源项目平台找源码看,can't be found了;

以main文件为例:

objectivec 复制代码
int main(int argc, char * argv[]) {
    @autoreleasepool {
        NSObject * rpObj = [[NSObject alloc] init];
    }
}
  1. 先转换成main.cpp文件查看底层调用方法:
objectivec 复制代码
在终端运行以下命令:
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main.cpp
  1. 查看main.cpp文件:
objectivec 复制代码
int main(int argc, char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; ///调用了objc_autoreleasePoolPush
        NSObject * rpObj = ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc")), sel_registerName("init"));
    }
}
  1. 有这么一个叫做__AtAutoreleasePool的结构体,这里可以看到装筐和出筐的push和pop方法;
objectivec 复制代码
struct __AtAutoreleasePool {
//构造函数
  __AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
//析构函数
  ~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
  void * atautoreleasepoolobj;
};
  1. 简单看下push和pop方法

先看push方法,核心逻辑有三块:

  • 获取当前线程的 "热页"(hotPage):自动释放池由多个 AutoreleasePoolPage 组成栈结构(每个页大小为 4096 字节),"热页" 是当前正在使用的页(栈顶页)。
  • 推入 "池边界"(POOL_BOUNDARY):每个自动释放池对应一个特殊标记 POOL_BOUNDARY(本质是 nil),push() 方法会将该标记添加到热页中,作为当前池的 "起点"。后续添加的自动释放对象会依次存入页中,直到调用 objc_autoreleasePoolPop 时,释放从 "起点" 到当前位置的所有对象。
  • 无页时创建新页栈:若线程首次使用自动释放池(无页),autoreleaseNoPage() 会创建第一个 AutoreleasePoolPage,并初始化线程的页栈。
objectivec 复制代码
// 全局的自动释放池页栈(每个线程独立)
static pthread_key_t const key = AUTORELEASE_POOL_KEY;

void *objc_autoreleasePoolPush(void) {
    // 获取当前线程的自动释放池页栈
    AutoreleasePoolPage *page = hotPage();
    
    if (page) {
        // 若存在当前页,直接在页中添加一个"池边界"(POOL_BOUNDARY)
        return page->push();
    } else {
        // 若不存在页,创建新的页栈,并添加池边界
        return autoreleaseNoPage();
    }
}

再看pop方法,核心逻辑有四块:

  • 获取热页:先拿到当前线程的 AutoreleasePoolPage 栈顶页(hotPage),确保操作的是当前正在使用的页;
  • 验证令牌:传入的 ctxt 必须是 push 时返回的 POOL_BOUNDARY(池边界标记),避免释放错误范围的对象(如传入无效指针会直接报错);
  • 反向释放对象:调用 releaseUntil(stop) 从栈顶反向遍历,直到遇到 stop(即 POOL_BOUNDARY):
    每次取出栈顶的自动释放对象,调用 objc_release 减少其引用计数(计数为 0 时对象销毁);
    若当前页释放为空,自动切换到前一页(栈底方向),继续释放,直到遍历到目标边界;
  • 清理空页:释放完成后,若当前页为空且不是栈中唯一页,将其从页栈中移除,优化内存占用。
objectivec 复制代码
// pop 函数:传入 push 返回的"池令牌"(POOL_BOUNDARY),释放池内对象
OBJC_EXPORT void objc_autoreleasePoolPop(void *ctxt) OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);

void objc_autoreleasePoolPop(void *ctxt) {
    // 1. 获取当前线程的热页(正在使用的 AutoreleasePoolPage 栈顶页)
    AutoreleasePoolPage *page = hotPage();
    if (!page) {
        // 无有效页,直接返回(异常场景)
        return;
    }

    // 2. 验证传入的"池令牌"是 POOL_BOUNDARY(push 时存入的边界标记)
    if (ctxt != POOL_BOUNDARY) {
        // 若令牌无效(如传入错误指针),释放到栈底并报错
        page->releaseUntil(POOL_BOUNDARY);
        _objc_fatal("invalid autorelease pool token");
    }

    // 3. 释放从"当前池边界"到"栈顶"的所有自动释放对象
    page->releaseUntil(ctxt);

    // 4. 清理空页:若当前页释放后无对象,且不是唯一页,将其从栈中移除
    if (page->empty() && !page->isSinglePage()) {
        page->kill();
    }
}

// 核心辅助方法:释放从当前位置到目标边界(ctxt)的所有对象
void AutoreleasePoolPage::releaseUntil(void *stop) {
    // 从栈顶开始,反向遍历对象
    while (this->next != stop) {
        // 获取当前栈顶对象
        AutoreleasePoolPage *page = hotPage();
        while (page->empty()) {
            // 若当前页空,切换到前一页(栈底方向)
            page = page->parent;
            setHotPage(page);
        }

        // 取出栈顶对象,指针下移
        id obj = *--page->next;
        memset((void*)page->next, SCRIBBLE, sizeof(*page->next)); // 擦除痕迹,避免野指针

        // 释放对象(调用 objc_release,触发引用计数-1)
        objc_release(obj);
    }

    // 更新热页(确保后续操作指向正确页)
    setHotPage(this);
}
  1. 从上面看源码引出了AutoreleasePoolPage的概念,也就是这个承载延迟释放对象的筐;
objectivec 复制代码
class AutoreleasePoolPage {
    // 1. 常量定义
    static const size_t SIZE = 4096; // 每页大小(4KB,固定)
    static const size_t COUNT = SIZE / sizeof(id); // 每页可存储的对象数(约 1024 个,因头部占部分空间)
    static const id POOL_BOUNDARY = nil; // 池边界标记(push/pop 的核心标记)

    // 2. 成员变量(页的元数据 + 存储区)
    magic_t magic; // 校验页完整性的魔术值(调试用)
    id *next; // 指向栈顶的下一个空闲位置(即下一个对象的存储地址)
    pthread_t thread; // 所属线程(线程隔离,每个线程的页栈独立)
    AutoreleasePoolPage *parent; // 前一页(栈底方向)
    AutoreleasePoolPage *child; // 后一页(栈顶方向)
    uint32_t depth; // 页在栈中的深度(用于调试)
    uint32_t hiwat; // 历史最高存储量(用于调试)

    // 3. 存储区(紧跟在成员变量后,占 SIZE - 元数据大小的空间)
    id objects[0]; // 柔性数组,实际存储自动释放对象的区域
};

综上,有筐,有进筐方法,出筐方法;

相关推荐
2501_915921435 小时前
查看iOS App实时日志的正确方式,多工具协同打造高效调试与问题定位体系(2025最新指南)
android·ios·小程序·https·uni-app·iphone·webview
ajassi20007 小时前
开源 Objective-C IOS 应用开发(四)Xcode工程文件结构
ios·开源·objective-c
G佳伟8 小时前
如何解决解决,微信小程序ios无法长按复制问题<text>设置 selectable=“true“不起作用
ios·微信小程序·小程序
Nick56838 小时前
Apple Pay 与 Google Pay 开发与结算全流程文档
ios·安卓·android-studio
全栈派森8 小时前
初见 Dart:这门新语言如何让你的 App「动」起来?
android·flutter·ios
HarderCoder9 小时前
Swift 内存管理:吃透 ARC 、weak、unowned
ios·swift
Digitally10 小时前
5种将照片从iPhone传输到戴尔PC/笔记本电脑的方法
ios·电脑·iphone
ajassi200010 小时前
开源 Objective-C IOS 应用开发(三)第一个iPhone的APP
ios·开源·objective-c
Daniel_Coder16 小时前
iOS Widget 开发-9:可配置 Widget:使用 IntentConfiguration 实现参数选择
ios·swiftui·swift·widget·intents