1.创建和释放时机问题

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

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

第二个 Observer 监视了两个事件: BeforeWaiting(准备进入休眠) 时调用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 释放旧的池并创建新池;Exit(即将退出Loop) 时调用 _objc_autoreleasePoolPop() 来释放自动释放池。这个 Observer 的 order 是 2147483647(2^31-1
),优先级最低,保证其释放池子发生在其他所有回调之后。

在主线程执行的代码,通常是写在诸如事件回调、Timer回调内的。这些回调会被 RunLoop 创建好的 AutoreleasePool 环绕着,所以不会出现内存泄漏,开发者也不必显示创建 Pool 了。

2.AutorealeasePool内部结构

是一条双向链表,每个节点都是一个AutoreleasePoolPage

3.AutoreleasePoolPage的结构

AutoreleasePoolPage 是一个 C++ 中的类:

class AutoreleasePoolPage {
magic_t const magic; //用于对当前 AutoreleasePoolPage 完整性的校验
id *next;  // 下一个可插入对象的可用地址,如果 next 指向的地址加入一个 object,它就会如下图所示移动到下一个为空的内存地址中
pthread_t const thread; // 当前的线程
AutoreleasePoolPage * const parent; // 父指针
AutoreleasePoolPage *child; // 子指针
uint32_t const depth;
uint32_t hiwat;
};

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

4.POOL_BOUNDARY(哨兵对象)

也有人称作 POOL_SENTINEL

POOL_BOUNDARY 只是 nil 的别名,

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

5.objc_autoreleasePoolPush 方法

void *objc_autoreleasePoolPush(void) {
return AutoreleasePoolPage::push();
} static inline void *push() {
return autoreleaseFast(objc);
}

在这里会进入一个比较关键的方法 autoreleaseFast,第一次传入哨兵对象 POOL_SENTINEL

static inline id *autoreleaseFast(id obj)
{
AutoreleasePoolPage *page = hotPage();
if (page && !page->full()) {//page没有满,就把obj对象加到page
return page->add(obj);

} else if (page) {
return autoreleaseFullPage(obj, page);
} else {
return autoreleaseNoPage(obj);
}
}
//有 hotPage 并且当前 page 已满
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);
}

//无 hotPage
static id *autoreleaseNoPage(id obj) {
AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
setHotPage(page); if (obj != POOL_SENTINEL) {
page->add(POOL_SENTINEL);
} return page->add(obj);

上述方法分三种情况选择不同的代码执行:

  • 有 hotPage 并且当前 page 不满

    • 调用 page->add(obj) 方法将对象添加至 AutoreleasePoolPage 的栈中
  • 有 hotPage 并且当前 page 已满
    • 1.它会从传入的 page 开始遍历整个双向链表,查找到一个未满的 AutoreleasePoolPage,
    • 2.调用 autoreleaseFullPage 初始化一个新的页
    • 调用 page->add(obj) 方法将对象添加至 AutoreleasePoolPage 的栈中
  • 无 hotPage
    • 调用 autoreleaseNoPage 创建一个 hotPage

    • 如果obj == POOL_SENTINEL,则直接page->add(obj) 将对象添加至 AutoreleasePoolPage 的栈中

    • 如果obj != POOL_SENTINEL,则先加入POOL_SENTINEL哨兵对象,再调用 page->add(obj) 方法将对象添加至 AutoreleasePoolPage 的栈中

最后的都会调用 page->add(obj) 将对象添加到自动释放池中。

hotPage 可以理解为当前正在使用的 AutoreleasePoolPage

6.objc_autoreleasePoolPop 方法

static inline void pop(void *token) {
AutoreleasePoolPage *page = pageForPointer(token);
id *stop = (id *)token; page->releaseUntil(stop); 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. 调用 child 的 kill 方法

- pageForPointer 获取 AutoreleasePoolPage

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

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

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

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

- releaseUntil 释放对象

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

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

-  kill() 方法

函数releaseUntil ,它在释放的时候其实会一直顺着parent往前释放,只到找到参数stop传入的地址,也就是说可能一次性释放好几个page

  1. 当前page为空,直接kill掉当前page,然后把parent设置为hotpage
  2. 当前page为空,而且没有parent,kill掉当前pagehotpage置为空;
  3. 当前page不为空,但是有child,如果当前page的空间占用不到一半,释放child,如果当前page的空间占用超过一半,且child还有child,直接释放这个孙子辈的page。(对于第三步注释中的解释是:keep one empty child if page is more than half fully)

注意:

  1. releaseUntil 做的工作是擦除AutoreleasePoolPage所占用的内存区域的内容(通过memset),但是并没有置空child或者parent指针

  2. kill做的工作是置空已经被releaseUntil是放过的page的child的,也就是释放child所指向的AutoreleasePoolPage占用空间

if (page->lessThanHalfFull()) {   

      page->child->kill();
}
 
当前page的空间占用少于一半,释放掉child page占用的空间
else if (page->child->child) {
page->child->child->kill();
}
当前page的空间占用超过一半的,因为很可能会满了,就不释放child page的占用空间了,为了预留一个child page,满了的话我们就不用创建一个page再置为自己的child了,但是只要预留一页就够了,所以释放掉child的child,也就是只保留page和page+1页

补充:
系统 C函数: 使用extern 可以调用其他类的c函数

extern void _objc_autoreleasePoolPrint(void);

可以使用,用于打印autoreleasepool 存放的对象


参考博客:https://draveness.me/autoreleasepool/#AutoreleasePoolPage

AutoreleasePool 的总结的更多相关文章

  1. @autoreleasepool在MRC和ARC中的区别

    对于@autoreleasepool {} (1)在ARC中会销毁所有在里面创建的对象,即使你用外面的Strong指针指向他 (2)在MRC中如果有外部的强指针指向,不会销毁对象,retainCoun ...

  2. autoreleasepool自动释放池

     示例: @autoreleasepool { ; i[largeNumber; i++) { (因识别问题,该行代码中尖括号改为方括号代替) Person *per = [[Person alloc ...

  3. AutoReleasePool 和 ARC 以及Garbage Collection

    AutoReleasePool autoreleasepool并不是总是被auto 创建,然后自动维护应用创建的对象. 自动创建的情况如下: 1. 使用NSThread的detachNewThread ...

  4. 深入剖析AutoreleasePool

    [深入剖析AutoreleasePool] Objc的AutoreleasePool是一个首尾相连的内存链接,每块大小为1页(32位机上为4kb). 上面可以看到,parent指向父Pool,chil ...

  5. Objc中2维指针作为输出参数时由ARC及@autoreleasepool引发的血案

    先看下面一个例子 #import <UIKit/UIKit.h> #import "AppDelegate.h" @interface Something : NSOb ...

  6. iOS 非ARC基本内存管理系列 4-autorelease方法和@autoreleasepool

    1.autorelease 基本用法 对象执行autorelease方法时会将对象添加到自动释放池中 当自动释放池销毁时自动释放池中所有对象作release操作 对象执行autorelease方法后自 ...

  7. autoreleasepool的笔记

    1.autoreleasepool总是会被问到,放在自动释放池中的对象合适被释放?理解不正确的答案:{}出了大括号.出了作用域等等.个人认为参考答案是,1.在不是手动添加的AutoreleasePoo ...

  8. iOS基本内存管理:autorelease和autoreleasepool

    1.autorelease 基本用法 对象执行autorelease方法时会将对象添加到自动释放池中 当自动释放池销毁时自动释放池中所有对象作release操作 对象执行autorelease方法后自 ...

  9. cocos2D-x 3.5 引擎解析之--引用计数(Ref),自己主动释放池(PoolManager),自己主动释放池管理器( AutoreleasePool)

    #include <CCRef.h> Ref is used for reference count manangement. If a classinherits from Ref. C ...

  10. Runloop与autoreleasePool联系

    autoreleasePool自动释放池,ARC模式下,苹果会自动进行内存管理,不需要我们手动去管理内存.这对于苹果开发者来说,省去了很多事情,不用再每天为了内存管理浪费掉宝贵的开发时间.大家都知道, ...

随机推荐

  1. Rust中的into函数和from函数

    1.Rust中的into函数和from函数是做什么用的? into函数是Rust语言中的一个转换函数,它属于Into trait.它可以将一个类型转换为另一个类型.实现了From trait的类型会自 ...

  2. Qt5.9 UI设计(七)——统一样式设计

    前言 前面已经将UI设计部分实现,各页面也做了最简单的设计,本章介绍一下qss样式的使用.样式设计最终的显示效果如下图: 操作步骤 将stylesheet.qss 样式文件添加进工程 styleshe ...

  3. Kubernetes入门实践(ConfigMap/Secret)

    Kubernetes中用于管理配置信息的两种对象: ConfigMap和Secret,可使用它们来灵活地配置和定制应用.应用程序有很多类别的配置信息,从数据安全的角度看可分为明文配置和机密配置,明文配 ...

  4. BAT 基础语法

    命令 //功能 echo //标准输出命令 在CMD窗口中 显示echo 后的内容 @ //关闭当前行的 回显    回显:源代码在 CMD 窗口中再次显示 pasue     // 暂停程序 的执行 ...

  5. 【FAQ】关于华为推送服务因营销消息频次管控导致服务通讯类消息下发失败的解决方案

    一. 问题描述 使用华为推送服务下发IM消息时,下发消息请求成功且code码为80000000,但是手机总是收不到消息: 在华为推送自助分析(Beta)平台查看发现,消息发送触发了频控. 二. 问题原 ...

  6. DRF的Serializer组件(源码分析)

    DRF的Serializer组件(源码分析) 1. 数据校验 drf中为我们提供了Serializer,他主要有两大功能: 对请求数据校验(底层调用Django的Form和ModelForm) 对数据 ...

  7. 第138篇:了解HTTP协议(TCP/IP协议,DNS域名解析,浏览器缓存)

    好家伙,发现自己的网络知识十分匮乏,赶紧补一下   这里先举个我生活中的例子 欸,作业不会写了,上网搜一下 用edge浏览器上bing必应搜一下(百度广告太多了,真不想用百度举例子)   假设这是我们 ...

  8. Prism Sample 12-UsingCompositeCommands

    本例中,主页是一个按钮,绑定了一个复合命令,然后下面一个TabControl <Grid> <Grid.RowDefinitions> <RowDefinition He ...

  9. 2022-06-03:a -> b,代表a在食物链中被b捕食, 给定一个有向无环图,返回这个图中从最初级动物到最顶级捕食者的食物链有几条。 来自理想汽车。

    2022-06-03:a -> b,代表a在食物链中被b捕食, 给定一个有向无环图,返回这个图中从最初级动物到最顶级捕食者的食物链有几条. 来自理想汽车. 答案2022-06-03: 拓扑排序. ...

  10. 【GiraKoo】CMake提示could not find any instance of Visual Studio

    CMake提示could not find any instance of Visual Studio. 原因 此种情况是由于默认的CMake工具不是Visual Studio提供的版本导致的. 解决 ...