【iOS】——AutoReleasePool底层原理及总结

自动释放池

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

自动释放池的生命周期

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

AutorealeasePool结构

每一个AutorealeasePool都是由一系列的 AutoreleasePoolPage 组成的,并且每一个 AutoreleasePoolPage 的大小都是 4096 字节 。

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

AutoreleasePoolPage的定义如下:

objective-c 复制代码
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字节
    ...
}

自动释放池中的栈

如果我们的一个 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实现

在每个文件中的main.m文件中都有下面这段代码:

objective-c 复制代码
int main(int argc, char * argv[]) {
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

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

将main.m文件通过$ clang -rewrite-objc main.m重新编译生成发现aotuoreleasepool 被转换为为一个 __AtAutoreleasePool 结构体:

objective-c 复制代码
struct __AtAutoreleasePool {
  __AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
  ~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
  void * atautoreleasepoolobj;
};

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

所以实际上 main 函数在实际工作时其实是这样的:

objective-c 复制代码
int main(int argc, const char * argv[]) {
    {
        void * atautoreleasepoolobj = objc_autoreleasePoolPush();

        // do whatever you want

        objc_autoreleasePoolPop(atautoreleasepoolobj);
    }
    return 0;
}

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

objective-c 复制代码
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()

objective-c 复制代码
void *objc_autoreleasePoolPush(void) {
    return AutoreleasePoolPage::push();
}
AutoreleasePoolPage::push()
objective-c 复制代码
static inline void *push() {
   return autoreleaseFast(POOL_SENTINEL);
}

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

autoreleaseFast()

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

这个方法其实就是一个压栈的操作,将对象加入 AutoreleasePoolPage 然后移动栈顶的指针

autoreleaseFullPage(当前 hotPage 已满)
objective-c 复制代码
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)
objective-c 复制代码
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);
}

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

AutoreleasePoolPage::pop

objective-c 复制代码
template<bool allowDebug>
    static void
    popPage(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();
            }
        }
    }

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

  1. 使用 pageForPointer 获取当前 token 所在的 AutoreleasePoolPage
  2. 调用 releaseUntil 方法释放栈中的 对象,直到 stop
  3. 调用 childkill 方法
  1. pop之后,所有child page肯定都为空了,且当前page一定是hotPage
  2. 系统为了节约内存,判断,如果当前page空间使用少于一半,就释放掉所有的child page,如果当前page空间使用大于一半,就从孙子page开始释放,预留一个child page
pageForPointer 获取 AutoreleasePoolPage

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

objective-c 复制代码
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

result = 0x100816000

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

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

releaseUntil 释放对象
objective-c 复制代码
void releaseUntil(id *stop) 
{
    // 这里没有使用递归, 防止发生栈溢出
    while (this->next != stop) { // 一直循环到 next 指针指向 stop 为止
        // Restart from hotPage() every time, in case -release 
        // autoreleased more objects
        AutoreleasePoolPage *page = hotPage(); // 取出 hotPage

        while (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 指向的内存清空为SCRIBBLE
        page->protect(); // 如果需要的话, 设置内存锁定

        if (obj != POOL_BOUNDARY) { // 如果取出的对象不是边界符
            objc_release(obj); // 给取出来的对象进行一次 release 操作
        }
    }

    setHotPage(this); // 将本节点设置为 hotPage

#if DEBUG
    // we expect any children to be completely empty
    for (AutoreleasePoolPage *page = child; page; page = page->child) {
        assert(page->empty());
    }
#endif
}

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

kill() 方法
objective-c 复制代码
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方法

入栈(push)

在页中压栈普通对象主要是通过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对象为当前页
Autorelease对象什么时候释放

当创建了局部释放池时,会在@autoreleasepool{}的右大括号结束时释放,及时释放对象大幅度降低程序的内存占用。在没有手动加@autoreleasepool的情况下,Autorelease对象是在当前的runloop迭代结束时释放的(每个线程对应一个runloop),而它能够释放的原因是系统在每个runloop迭代中都加入了自动释放池。

AutoreleasePool能否嵌套使用

可以嵌套使用,其目的是可以控制应用程序的内存峰值,使其不要太高 可以嵌套的原因是因为自动释放池是以栈为节点,通过双向链表的形式连接的,且是和线程一一对应的 自动释放池的多层嵌套其实就是不停的push哨兵对象,在pop时,会先释放里面的,在释放外面的

哪些对象可以加入AutoreleasePool?alloc创建可以吗?

使用new、alloc、copy关键字生成的对象和retain了的对象需要手动释放,不会被添加到自动释放池中 设置为autorelease的对象不需要手动释放,会直接进入自动释放池 所有 autorelease 的对象,在出了作用域之后,会被自动添加到最近创建的自动释放池中

AutoreleasePool的释放时机是什么时候?

App 启动后,苹果在主线程 RunLoop 里注册了两个 Observer ,其回调都是_wrapRunLoopWithAutoreleasePoolHandler()

第一个 Observer 监视一个事件:

监听 Entry(即将进入 Loop),其回调内会调用 _objc_autoreleasePoolPush() 创建自动释放池。其 order 是 -2147483647,优先级最高,保证创 建释放池发生在其他所有回调之前。

第二个 Observer 监视了两个事件:

BeforeWaiting (准备进入休眠) 时调用 _objc_autoreleasePoolPop()_objc_autoreleasePoolPush() 释放旧的池并创建新池;

Exit (即 将退出 Loop) 时调用_objc_autoreleasePoolPop()来释放自动释放池。这个 Observer 的 order 是 2147483647,优先级最低,保证其释放池子发生在其他所有回调之后。

thread 和 AutoreleasePool的关系

每个线程都有与之关联的自动释放池堆栈结构,新的pool在创建时会被压栈到栈顶,pool销毁时,会被出栈,对于当前线程来说,释放对象会被压栈到栈顶,线程停止时,会自动释放与之关联的自动释放池

RunLoop 和 AutoreleasePool的关系

主程序的RunLoop在每次事件循环之前之前,会自动创建一个 autoreleasePool 并且会在事件循环结束时,执行drain操作,释放其中的对象

相关推荐
dr李四维7 分钟前
iOS构建版本以及Hbuilder打iOS的ipa包全流程
前端·笔记·ios·产品运营·产品经理·xcode
️ 邪神15 分钟前
【Android、IOS、Flutter、鸿蒙、ReactNative 】自定义View
flutter·ios·鸿蒙·reactnative·anroid
比格丽巴格丽抱12 小时前
flutter项目苹果编译运行打包上线
flutter·ios
网络安全-老纪13 小时前
iOS应用网络安全之HTTPS
web安全·ios·https
1024小神16 小时前
tauri2.0版本开发苹果ios和安卓android应用,环境搭建和最后编译为apk
android·ios·tauri
lzhdim17 小时前
iPhone 17 Air看点汇总:薄至6mm 刷新苹果轻薄纪录
ios·iphone
安和昂17 小时前
【iOS】知乎日报第四周总结
ios
麦田里的守望者江19 小时前
KMP 中的 expect 和 actual 声明
android·ios·kotlin
_黎明21 小时前
【Swift】字符串和字符
开发语言·ios·swift
ZVAyIVqt0UFji1 天前
iOS屏幕共享技术实践
macos·ios·objective-c·cocoa