【iOS】—— autoreleasePool以及总结

【iOS】------ autoreleasePool以及总结

      • [1. 什么是autoreleasePool](#1. 什么是autoreleasePool)
      • [2. autoreleasePoolPage](#2. autoreleasePoolPage)
      • [3. 总结](#3. 总结)
      • [3.1 autoreleasePool的原理](#3.1 autoreleasePool的原理)
      • [3.2 autoreleasePool的问题](#3.2 autoreleasePool的问题)
        • [3.2.1 autoreleasepool的嵌套操作](#3.2.1 autoreleasepool的嵌套操作)
        • [3.2.2 autoreleasepool的释放时机](#3.2.2 autoreleasepool的释放时机)
        • [3.2.3 那些对象可以加入到autoreleasePool中](#3.2.3 那些对象可以加入到autoreleasePool中)
        • [3.2.4 关于哨兵对象和next指针](#3.2.4 关于哨兵对象和next指针)
        • [3.2.5 next和child:](#3.2.5 next和child:)
        • [3.2.6 thread 和 AutoreleasePool的关系](#3.2.6 thread 和 AutoreleasePool的关系)
        • [3.2.7 RunLoop 和 AutoreleasePool的关系](#3.2.7 RunLoop 和 AutoreleasePool的关系)

1. 什么是autoreleasePool

AutoreleasePool(自动释放池)是在Objective-CSwift中用于管理内存释放的机制。通过创建自动释放池,可以将需要延迟释放的对象放入其中,在自动释放池被销毁时,其中的对象会被释放,从而帮助避免内存泄漏并优化内存管理。

大概的意思就是:

  • 自动释放池是栈结构,存储的是指针。
  • 指针指向需要自动释放的对象或者 POOL_BOUNDARY 边界值。以 POOL_BOUNDARY 为边界,当释放池释放时,在边界之内的对象会被释放。
  • page 以双向链表的形式构成 pool,page 会自动创建或释放。
  • 线程本地存储指向当前最新的 page。

2. autoreleasePoolPage

每个autoreleasePool都是由一系列autoreleasePoolPage组成的,并且每个autoreleasePoolPage大小都为4096字节,相关源码如下:

objective-c 复制代码
#define I386_PGBYTES 4096
#define PAGE_SIZE I386_PGBYTES

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构成的双向链表,其结构中的childparent分别指向其前趋和后继。

单个AutoreleasePoolPage结构如下:

其中有 56 bit 用于存储AutoreleasePoolPage的成员变量。

  • 该结构体的第一个成员变量是magic,我们在isa中也学习过,isa中是分判对象是否未完成初始化,在这里也一样,用来检查这个节点是否已经被初始化了。
  • begin()和end()这两个类的实例方法帮助我们快速获取 0x100816038 ~ 0x100817000 这一范围的边界地址。
  • next:指向下一个为空的内存地址,如果next指向的地址加入一个object,它就会如下图所示移动到下一个为空的内存地址中,就像栈顶指针一样。
  • thread:保存了当前页所在的线程。
  • depth:表示page的深度,首次为0,每个page的大小都是4096字节(16进制0x1000),每次初始化一个page,depth都加一。
  • POOL_BOUNDARY:就是哨兵对象,它只是nil的别名,用于分隔Autoreleasepool。POOL_BOUNDARY直译过来就是POOL的边界。它的作用是隔开page中的对象。因为并不是每次push与pop之间存进的对象都刚好占满一个page,可能会不满,可能会超过,因此这个POOL_BOUNDARY帮助我们分隔每个@autoreleasepool块之间的对象。也就是说这个page可能存储很多个@autoreleasepool块的对象,使用POOL_BOUNDARY来隔开每个@autoreleasepool块的对象。
objective-c 复制代码
 #define POOL_BOUNDARY nil
objc_autoreleasePoolPush方法:
objective-c 复制代码
 void *objc_autoreleasePoolPush(void) {
    return AutoreleasePoolPage::push();
}

这里调用了AutoreleasePoolPage::push()方法:

objective-c 复制代码
 static inline void *push() 
{
    id *dest;
    // POOL_BOUNDARY就是nil
    // 首先将一个哨兵对象插入到栈顶
    if (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;
}

其中调用了autoreleaseFast方法,hotPage指的是当前正在使用的AutoreleasePoolPage

objective-c 复制代码
static inline id *autoreleaseFast(id obj)
{
   AutoreleasePoolPage *page = hotPage();
   if (page && !page->full()) {//有 hotPage 并且当前 page 不满,将object加入当前栈中
       return page->add(obj);
   } else if (page) {//有hotPage 但当前page已满,找未满页或创建新页,将object添加到新页中
       return autoreleaseFullPage(obj, page);
   } else {//无hotPage,创建hotPage,加入其中
       return autoreleaseNoPage(obj);
   }
}
 

有hotPage但当前page未满,直接调用page->add(obj)方法将对象添加到自动释放池中。

objective-c 复制代码
// 这其实就是一个压栈操作,将对象加入AutoreleasePoolPage,然后移动栈顶指针
id *add(id obj) {
    id *ret = next;
    *next = obj;
    next++;
    return ret;
}
 

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

有hotPage但当前page已满,(找到未满页或者创建新页,将object添加到新页中autoreleaseFullPage当前page满的时候调用)

objective-c 复制代码
static id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page) {
//一直遍历,直到找到一个未满的 AutoreleasePoolPage,如果找到最后还没找到,就新建一个 AutoreleasePoolPage
    do {
        if (page->child) 
        	page = page->child;
        else page = new AutoreleasePoolPage(page);
    } while (page->full());
	
	//将找到的,或者构建的page作为hotPage,然后将obj加入
    setHotPage(page);
    return page->add(obj);
}

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

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

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

无hotPage,创建hotPage,加入其中:

这个时候,由于内存中没AutoreleasePoolPage,就要从头开始构建这个自动释放池的双向链表,那么当前页表作为第一张页表,是没有parent指针的。并且我们在第一次创建page时其首位都是要加POOL_SENTINEL标识的,方便让page知道在哪就结束了。

objective-c 复制代码
 static id *autoreleaseNoPage(id obj) {
    AutoreleasePoolPage *page = new AutoreleasePoolPage(nil); // 创建AutoreleasePoolPage
    setHotPage(page); // 设置page为当前页
 
    if (obj != POOL_SENTINEL) { // 加POOL_SENTINEL哨兵
        page->add(POOL_SENTINEL);
    }
 
    return page->add(obj); // 将obj加入
}
objc_autoreleasePoolPop方法:
objective-c 复制代码
 void objc_autoreleasePoolPop(void *ctxt) {
    AutoreleasePoolPage::pop(ctxt);
}

其调用的pop方法如下:

objective-c 复制代码
 static inline void pop(void *token) {
    AutoreleasePoolPage *page = pageForPointer(token);//使用 pageForPointer 获取当前 token 所在的 AutoreleasePoolPage
    id *stop = (id *)token;

    page->releaseUntil(stop);//调用 releaseUntil 方法释放栈中的对象,直到 stop 位置,stop就是传递的参数,一般为哨兵对象

	//调用 child 的 kill 方法,系统根据当前页的不同状态kill掉不同child的页面
	//releaseUntil把page里的对象进行了释放,但是page本身也会占据很多空间,所以要通过kill()来处理,释放空间
    if (page->child) {
        if (page->lessThanHalfFull()) { // 当前page小于一半满
            page->child->kill(); // 把当前页的孩子杀掉
        } else if (page->child->child) { // 否则,留下一个孩子,从孙子开始杀
            page->child->child->kill();
        }
    }
}

假设当前page一半都没满,说明剩余的page空间已经暂时够了,把多余的儿子page就可以kill掉,如果超过一半页,就认为下一半page还有存在的必要,所以kill孙子page,保留一个儿子page。

token
  • token是指向该pool的POOL_BOUNDARY指针
  • token的本质就是指向哨兵对象的指针,存储着每次push时插入的POOL_BOUNDARY的地址
  • 只有第一次push的时候会在page中插入一个POOL_BOUNDARY【或者page满了,或者没有hotPage需要使用新的page了】,并不是page的开头都一定是POOL_BOUNDARY
kill()方法
objective-c 复制代码
void kill() {
    AutoreleasePoolPage *page = this; // 获取当前页
    while (page->child) page = page->child; // child存在就一直往下找,直到找到一个不存在的

    AutoreleasePoolPage *deathptr;
    do {
        deathptr = page;
        page = page->parent;
        if (page) {
            page->unprotect();
            page->child = nil; // 将其child指向置nil,防止出现悬垂指针
            page->protect();
        }
        delete deathptr; // 删除
    } while (deathptr != this); // 直到this处停止
}
 

3. 总结

3.1 autoreleasePool的原理

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

大小可根据宏定义查得数值为:4096字节

自动释放池的出栈和入栈主要通过objc_autoreleasePoolPushobjc_autoreleasePoolPop,实际上调用的是autoreleasePoolPagepushpop方法。

**push操作:**每次调用push操作都会创建一个新的autoreleasePoolPage,而autoreleasePoolPagePush的具体操作就是插入一个POOL BOUNDARY,并且返回插入POOL BOUNDARY的内存地址。在push中的操作,需要调用autoreleaseFast方法处理,具体情况分下面三个:

  • page存在并且未满,直接调用add方法将对象添加到page的next指针处,next指针递增。
  • page存在并且已满,需要调用autoreleaseFullPage,初始化一个新的page,然后通过add方法添加。
  • page不存在,需要先调用autoreleaseNoPage,创建一个hotPage,然后调用add方法添加对象到栈中。

**pop操作:**当执行pop操作的时候,会传入一个参数,这个参数是push操作的返回值,也就是POOLBOUNDARY的内存地址token,因此pop操作,就是根据token找到哨兵对象的位置,然后objc_release释放token之前的对象,把next指针指到正确的位置。

POOL_BOUNDARY是一种特殊标记,用于标记池的边界,就是说这个page可能存储很多个@autoreleasepool块的对象,在早期可能是一个特殊的指针或者整数值;在现代时,POOL_BOUNDARY可能是一个特殊的结构体或者无效指针。

整体思路:

通过push创建一个autoreleasePoolPage对象,会在开始的位置存放POOL BOUNDARY哨兵对象,然后将注册了autorelease的对象添加到存储到里面,调用pop的时候,会根据push返回的token的位置,释放到token之前的位置。

3.2 autoreleasePool的问题

3.2.1 autoreleasepool的嵌套操作

可以使用嵌套操作,其目的是控制应用程序的内存峰值,是其不要太高。

嵌套原因:自动释放池是以栈为节点,通过双向链表的形式链接,且与线程一一对应。

3.2.2 autoreleasepool的释放时机

在没有手动添加autoreleasePool的情况下,autorelease对象是在当前的runloop迭代结束的时候释放。

  1. 在准备进入runloop的时候,创建一个autoreleasePool(优先级最高)。
  2. runloop准备休眠的时候,会先释放掉autoreleasePool(优先级最低),然后创建一个新的autoreleasePool
  3. 在退出runloop的时候,就释放掉autoreleasePool
3.2.3 那些对象可以加入到autoreleasePool中
  1. 在MRC情况下,使用new,copy,alloc关键字修饰的对象或者retain持有的对象,不能加入到自动释放池中。
  2. MRC中设置为autorelease的对象,不需要手动,自动加入到autoreleasePool中。
  3. 所有autorelease的对象,在出了作用域之后,会被自动添加到最近创建的自动释放池中。
3.2.4 关于哨兵对象和next指针

next指针只有一个,永远指向下一个能存放autoreleasepool的地址,而哨兵对象有很多个,每个autoreleasepool都对应一个哨兵对象,标示这个autoreleasepool对象从哪里开始存。

3.2.5 next和child:

next指向下一个能存放autoreleasepool对象的地址,child是autoreleasePoolPage的参数,指向下一个page。

3.2.6 thread 和 AutoreleasePool的关系

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

3.2.7 RunLoop 和 AutoreleasePool的关系

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

相关推荐
若水无华2 天前
fiddler 配置ios手机代理调试
ios·智能手机·fiddler
Aress"2 天前
【ios越狱包安装失败?uniapp导出ipa文件如何安装到苹果手机】苹果IOS直接安装IPA文件
ios·uni-app·ipa安装
Jouzzy2 天前
【iOS安全】Dopamine越狱 iPhone X iOS 16.6 (20G75) | 解决Jailbreak failed with error
安全·ios·iphone
瓜子三百克2 天前
采用sherpa-onnx 实现 ios语音唤起的调研
macos·ios·cocoa
左钦杨2 天前
IOS CSS3 right transformX 动画卡顿 回弹
前端·ios·css3
努力成为包租婆2 天前
SDK does not contain ‘libarclite‘ at the path
ios
安和昂3 天前
【iOS】Tagged Pointer
macos·ios·cocoa
I烟雨云渊T3 天前
iOS 阅后即焚功能的实现
macos·ios·cocoa
struggle20253 天前
适用于 iOS 的 开源Ultralytics YOLO:应用程序和 Swift 软件包,用于在您自己的 iOS 应用程序中运行 YOLO
yolo·ios·开源·app·swift
Unlimitedz3 天前
iOS视频编码详细步骤(视频编码器,基于 VideoToolbox,支持硬件编码 H264/H265)
ios·音视频