【iOS】autoreleasepool

来说一下最近在了解的autoreleasepool吧,我们可能平时书写过许多脑残代码,其有很多的缺陷但是我们可能当时学的比较浅就也不太了解,就像下面这样的:

for (int i = 0; i < 1000000; i++) {NSNumber *num = [NSNumber numberWithInt:i];
}

一、@autoreleasepool{}

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

这个__AtAutoreleasePool到底是什么?

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

但是它在main这个函数中好像感觉并没有什么用,因为程序结束了那么内存不也就被释放了,那这里为什么要加@autoreleasepool{}

技术上是可行的,去掉main函数中的@autoreleasepool{}并没有什么关系,但是为了严谨,为了使UIApplicationMin创建出来的自动释放对象有自动释放池可添加,并能在自动释放池结束的时候释放对象而不是依赖操作系统的回收,所以加上@autoreleasepool{},可以把它理解为最外层才触发释放机制的自动释放池。

extern "C" __declspec(dllimport) void * objc_autoreleasePoolPush(void);
extern "C" __declspec(dllimport) void objc_autoreleasePoolPop(void *);struct __AtAutoreleasePool {__AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}void * atautoreleasepoolobj;
};

所以我们平时在ARC环境下写的@autoreleasepool{}它其实就是自动释放池创建和释放的简单使用。

在这里插入图片描述

它会在{的时候创建自动释放池,在}的时候销毁自动释放池并发出release通知,让其中的变量自己进行release操作。

二、AutoreleasePoolPage

从上边的__AtAutoreleasePool我们可以看到这两种方法objc_autoreleasePoolPushobjc_autoreleasePoolPop,但是这究竟是什么呢?

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

我们可以看出这里又引入了新的类AutoreleasePoolPage,其相关源码如下:

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)。

#define I386_PGBYTES 4096
#define PAGE_SIZE I386_PGBYTES

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

在这里插入图片描述

单个AutoreleasePoolPage结构如下:
在这里插入图片描述

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

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

如果向上述刚初始化的page添加对象时,就会添加在next的指向处,next再向后移一位。
在这里插入图片描述

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

int main(int argc, const char * argv[]) {{//这里的 atautoreleasepoolobj 就是一个 POOL_SENTINELvoid * atautoreleasepoolobj = objc_autoreleasePoolPush();// do whatever you wantobjc_autoreleasePoolPop(atautoreleasepoolobj);}return 0;
}

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

在这里插入图片描述

这也是autoreleasepool能准确释放其中对象的原因:在该autoreleasepool push的时候会返回一个哨兵对象(POOL_SENTINEL)的地址,并将它给pop,那么这个pop就知道在执行pop释放的时候释放到这个哨兵对象(POOL_SENTINEL)处就可以停止了,而这其中释放的内容就正好是自动释放池中的对象。

1.objc_autoreleasePoolPush方法:

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

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

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

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);}
}

1.1 有hotPage并且当前page不满,直接调用page->add(obj)将对象添加到自动释放池中。

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

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

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

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

static id *autoreleaseNoPage(id obj) {AutoreleasePoolPage *page = new AutoreleasePoolPage(nil); // 创建AutoreleasePoolPagesetHotPage(page); // 设置page为当前页if (obj != POOL_SENTINEL) { // 加POOL_SENTINEL哨兵page->add(POOL_SENTINEL);}return page->add(obj); // 将obj加入
}

autorelease方法

我们现在再想,它dest = autoreleaseFast(POOL_BOUNDARY);操作传递的是POOL_BOUNDARY变量,并没有传对象,那么到底是怎么存入进去的呢?

通过调试查看汇编发现它其实调用的是objc_retainAutorelease方法,之后层层调用发现调用的是autorelease方法:

static inline id autorelease(id obj)
{printf("static inline id autorelease%p\n", obj);assert(obj);assert(!obj->isTaggedPointer());id *dest __unused = autoreleaseFast(obj);assert(!dest  ||  dest == EMPTY_POOL_PLACEHOLDER  ||  *dest == obj);return obj;
}

2.objc_autoreleasePoolPop方法:

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

我们一般都会在这个方法中传入一个哨兵对象POOL_SENTINEL,方便释放,如下图一样释放对象:
在这里插入图片描述

其调用的pop方法如下:

static inline void pop(void *token) {AutoreleasePoolPage *page = pageForPointer(token);//使用 pageForPointer 获取当前 token 所在的 AutoreleasePoolPageid *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();}}
}

Apple假设,当前page一半都没满,说明剩余的page空间已经暂时够了,把多余的child page就可以全kill掉,释放空间,如果超过一半,就认为下一页page还有存在的必要,说不定添加的对象太多就能用的到,所以kill掉孙子page,有个儿子page就暂时够了。

token

  • oken是指向该poolPOOL_BOUNDARY指针
  • token的本质就是指向哨兵对象的指针,存储着每次push时插入的POOL_BOUNDARY的地址
  • 只有第一次push的时候会在page中插入一个POOL_BOUNDARY【或者page满了,或者没有hotPage需要使用新的page了】,并不是page的开头都一定是POOL_BOUNDARY

2.1 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(); // 检查当前的result是不是一个AutoreleasePoolPagereturn result;
}

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

p = 0x100816048
p % SIZE = 0x48
result = 0x100816000

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

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

2.2 releaseUntil 释放对象:

releaseUntil方法的实现如下:

void releaseUntil(id *stop) {while (this->next != stop) { // 不等于stop就继续popAutoreleasePoolPage *page = hotPage(); // 获取当前页while (page->empty()) { // 当前页为空,就找其父页,并将其设置为当前页page = page->parent;setHotPage(page);}page->unprotect();id obj = *--page->next;memset((void*)page->next, SCRIBBLE, sizeof(*page->next)); // 将内存内容标记为SCRIBBLEpage->protect();if (obj != POOL_SENTINEL) { // 该对象不为标识POOL_SENTINEL,就释放对象objc_release(obj);}}setHotPage(this);
}

它的实现还是很容易的,用一个while循环持续释放 AutoreleasePoolPage中的内容,直到next指向了stop

使用memset将内存的内容设置成SCRIBBLE,然后使用 objc_release释放对象。

2.3 kill()方法:
到这里,没有分析的方法就只剩下kill了,而它会将当前页面以及子页面全部删除:

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.autorelease 方法

我们已经对自动释放池生命周期有一个比较好的了解,最后需要了解的话题就是autorelease方法的实现,先来看一下方法的调用栈:

- [NSObject autorelease]
└── id objc_object::rootAutorelease()└── id objc_object::rootAutorelease2()└── static id AutoreleasePoolPage::autorelease(id obj)└── static id AutoreleasePoolPage::autoreleaseFast(id obj)├── id *add(id obj)├── static id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page)│   ├── AutoreleasePoolPage(AutoreleasePoolPage *newParent)│   └── id *add(id obj)└── static id *autoreleaseNoPage(id obj)├── AutoreleasePoolPage(AutoreleasePoolPage *newParent)└── id *add(id obj)

autorelease方法的调用栈中,最终都会调用上面提到的autoreleaseFast方法,将当前对象加到AutoreleasePoolPage中。
这一小节中这些方法的实现都非常容易,只是进行了一些参数上的检查,最终还要调用autoreleaseFast方法:

inline id objc_object::rootAutorelease() {if (isTaggedPointer()) return (id)this;if (prepareOptimizedReturn(ReturnAtPlus1)) return (id)this;return rootAutorelease2();
}__attribute__((noinline,used)) id objc_object::rootAutorelease2() {return AutoreleasePoolPage::autorelease((id)this);
}static inline id autorelease(id obj) {id *dest __unused = autoreleaseFast(obj);return obj;
}

三、开头问题的解答

看到这里你是否明白了开篇所说的问题所在?当一次运行循环结束之前,也就是autoreleasepool释放autorelease对象之前,autoreleasepool的内存一直在增加,APP会出现内存峰值,卡顿,甚至会被系统强制关闭造成crash。

所以加上@autoreleasepool保证每次循环生成的autorelease对象及时的释放才能避免上述问题:

for (int i = 0; i < 1000000; i++) {@autoreleasepool {NSNumber *num = [NSNumber numberWithInt:i];NSLog(@"%@", num);}
}

另外@autoreleasepool还有延迟释放,使对象超出函数作用域存在等用处。

四、总结

总的来说,autoreleasepool就是一个双向链表,链表中的每个节点是一个栈,栈中保存了指向autoreleasepool的指针并且其中加入了需要自动释放池管理的对象,所以在autoreleasepool中的所有对象引用计数都会+1,一旦出了autoreleasepool,没有指针指向对象,对象的引用计数就会-1,ARC下,xcode会为代码自动添加 autoreleasepool

  • 自动释放池是由AutoreleasePoolPage以双向链表的方式实现的
  • 当对象调用autorelease方法时,会将对象加入AutoreleasePoolPage的栈中
  • 调用AutoreleasePoolPage::pop方法会向栈中的对象发送release消息

关于哨兵对象(POOL_BOUNDARY)和next指针:

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

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

autoreleasePoolPage与RunLoop的关系:

在这里插入图片描述
RunLoop和AutoReleasePool是通过线程的方式一一对应的
在非手动添加Autorelease pool下,Autorelease对象是在当前runloop进入休眠等待前被释放的
当一个runloop在不停的循环工作,那么runloop每一次循环必定会经过BeforeWaiting(准备进入休眠):而去BeforeWaiting(准备进入休眠) 时会调用_objc_autoreleasePoolPop()和 _objc_autoreleasePoolPush()释放旧的池并创建新池,那么这两个方法来销毁要释放的对象,所以我们根本不需要担心Autorelease的内存管理问题。

RunLoop创建和释放自动释放池的时机:

在进入RunLoop时,创建一个AutoReleasePool。
在准备休眠的时候,释放旧的AutoReleasePool,再新建一个AutoReleasePool。
在RunLoop退出时,释放AutoReleasePool。

保存autoreleasePoolPage的双向链表只有一个么?也就是所有线程的autoreleasePoolPage都保存在一个链表中,还是每个线程保存一个自己的链表?并且链表头也就是链表的入口位置是保存在哪里呢?谁来控制呢?

一个线程有自己单独autoreleasePool链表,也有可能没有链表。链表的hotPage存储在TLS中,因为链表是双向的,通过hotpage就可以找到表头和表尾,不需要再单独存储表头。

需要自己手动添加autoreleasepool的情况

  • 编写的不是基于UI框架的程序,例如命令行工具;
  • 通过循环方式创建大量临时对象;
  • 使用非Cocoa程序创建的子线程;

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/63763.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

C语言 ——指针数组与数组指针

目录 一、二维数组 二、指针数组 &#xff08;1&#xff09;概念 &#xff08;2&#xff09;书写方式 &#xff08;3&#xff09;指针数组模拟二维数组 三、数组指针 &#xff08;1&#xff09;概念 &#xff08;2&#xff09;使用数组指针打印一维数组 &#xff08;3&a…

搭建 Python 环境 | Python、PyCharm

计算机 计算机能完成的工作&#xff1a; 算术运算逻辑判断数据存储网络通信…更多的更复杂的任务 以下这些都可以称为 “计算机”&#xff1a; 一台计算机主要由以下这几个重要的组件构成 CPU 中央处理器&#xff1a;大脑&#xff0c;算术运算&#xff0c;逻辑判断 存储器&…

用PointNet分类3D点云

在本教程中&#xff0c;我们将学习如何训练PointNet进行分类。 我们将主要关注数据和训练过程&#xff1b; 展示如何从头开始编码 Point Net 的教程位于此处。 本教程的代码位于这个Github库中&#xff0c;我们将使用的笔记本位于这个Github库中。 一些代码的灵感来自于这个Git…

MyBatis操作数据库常用用法总结1

文章目录 1.单表查询1.1返回所有的表记录1.2根据id查找结果1.3根据名字查找结果 2.单表修改2.1修改密码 3.单表删除3.1根据id删除信息 4.单表增加&#xff08;根据业务情况返回&#xff09;4.1添加返回影响的行数4.2添加返回影响行数和id 5.多表查询&#xff08;多&#xff09;…

[考研机试] KY20 完数VS盈数 清华大学复试上机题 C++实现

描述 一个数如果恰好等于它的各因子(该数本身除外)子和&#xff0c;如&#xff1a;6321。则称其为“完数”&#xff1b;若因子之和大于该数&#xff0c;则称其为“盈数”。 求出2到60之间所有“完数”和“盈数”。 输入描述&#xff1a; 题目没有任何输入。 输出描述&#…

CAS服务端入门使用实践

CAS服务端入门使用实践 一、前言 1.简介 CAS 是一个企业多语言单点登录解决方案&#xff0c;支持大量附加身份验证协议和功能&#xff0c;满足身份验证和授权需求的综合平台。 2.环境 Windows 10JDK 1.8git version 2.41.0.windows.3Tomcat 9.0.78Maven 3.5.3cas-overlay-…

ArcGIS在洪水灾害普查、风险评估及淹没制图中应用教程

详情点击链接&#xff1a;ArcGIS在洪水灾害普查、风险评估及淹没制图中应用教程 一&#xff1a;洪水普查技术规范 1.1 全国水旱灾害风险普查实施方案 1.2 洪水风险区划及防治区划编制技术要求 1.3 山丘区中小河流洪水淹没图编制技术要求 二&#xff1a;ArcGIS及数据管理 …

Java集合知识回顾:从分类到工具类,掌握精髓

文章目录 1. 集合的分类2. Collection 接口3. Map 接口4. 泛型5. Collections 工具类总结 在Java编程世界中&#xff0c;集合是一项极为重要的知识&#xff0c;为我们的程序设计提供了强大的数据结构和处理手段。在本篇文章中&#xff0c;我们将回顾集合的分类以及相关的重要概…

74HC595驱动7x11点阵屏(LED-7X11-JHM)DEMO

起因 由于我之前做了一个点阵时钟 &#xff0c;但是无奈LED点阵屏价格比较贵&#xff0c;所以想找一个价格较为便宜的点阵来做便宜一点的点阵方案&#xff0c;再淘宝上看到有那种五毛钱一个的7x11的LED点阵&#xff0c;所以就想着试试搞一下这种点阵屏&#xff0c;这个由于是7…

阻塞队列的安全实现,定时器的安全实现(面试易考),超详细

一、&#x1f49b; 如何实现一个线程安全的阻塞队列 目前&#xff0c;当前代码是循环队列&#xff08;还没有进行改动&#xff09; head和tail的判空判断满两种方法: 1.浪费一个格子&#xff0c;当前走到head的前一个位置&#xff0c;就认为队列满的 2.单独搞一个变量&#xff…

机器学习基础之《特征工程(4)—特征降维》

一、什么是特征降维 降维是指在某些限定条件下&#xff0c;降低随机变量&#xff08;特征&#xff09;个数&#xff0c;得到一组“不相关”主变量的过程 1、降维 降低维度 ndarry 维数&#xff1a;嵌套的层数 0维&#xff1a;标量&#xff0c;具体的数0 1 2 3... …

AAAI论文阅读

文章目录 Open-Vocabulary Multi-Label Classifcation via Multi-Modal Knowledge Transfer——知识蒸馏的范畴Med-EASi: Finely Annotated Dataset and Models for Controllable Simplifcation of Medical Texts——医学领域数据集构建“Nothing Abnormal”: Disambiguating M…