`
啸笑天
  • 浏览: 3465648 次
  • 性别: Icon_minigender_1
  • 来自: China
社区版块
存档分类
最新评论

黑幕背后的Autorelease

    博客分类:
  • ios
 
阅读更多

我是前言

Autorelease机制是iOS开发者管理对象内存的好伙伴,MRC中,调用[obj autorelease]来延迟内存的释放是一件简单自然的事,ARC下,我们甚至可以完全不知道Autorelease就能管理好内存。而在这背后,objc和编译器都帮我们做了哪些事呢,它们是如何协作来正确管理内存的呢?刨根问底,一起来探究下黑幕背后的Autorelease机制。

Autorelease对象什么时候释放?

这个问题拿来做面试题,问过很多人,没有几个能答对的。很多答案都是“当前作用域大括号结束时释放”,显然木有正确理解Autorelease机制。
在没有手加Autorelease Pool的情况下,Autorelease对象是在当前的runloop迭代结束时释放的,而它能够释放的原因是系统在每个runloop迭代中都加入了自动释放池Push和Pop

小实验

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
__weak id reference = nil;
- (void)viewDidLoad {
    [super viewDidLoad];
    NSString *str = [NSString stringWithFormat:@"sunnyxx"];
    // str是一个autorelease对象,设置一个weak的引用来观察它
    reference = str;
}
- (void)viewWillAppear:(BOOL)animated {
    [super viewWillAppear:animated];
    NSLog(@"%@", reference); // Console: sunnyxx
}
- (void)viewDidAppear:(BOOL)animated {
    [super viewDidAppear:animated];
    NSLog(@"%@", reference); // Console: (null)
}

这个实验同时也证明了viewDidLoadviewWillAppear是在同一个runloop调用的,而viewDidAppear是在之后的某个runloop调用的。
由于这个vc在loadView之后便add到了window层级上,所以viewDidLoadviewWillAppear是在同一个runloop调用的,因此在viewWillAppear中,这个autorelease的变量依然有值。

当然,我们也可以手动干预Autorelease对象的释放时机:

1
2
3
4
5
6
7
8
- (void)viewDidLoad
{
    [super viewDidLoad];
    @autoreleasepool {
        NSString *str = [NSString stringWithFormat:@"sunnyxx"];
    }
    NSLog(@"%@", str); // Console: (null)
}

Autorelease原理

AutoreleasePoolPage

ARC下,我们使用@autoreleasepool{}来使用一个AutoreleasePool,随后编译器将其改写成下面的样子:

1
2
3
void *context = objc_autoreleasePoolPush();
// {}中的代码
objc_autoreleasePoolPop(context);

而这两个函数都是对AutoreleasePoolPage的简单封装,所以自动释放机制的核心就在于这个类。

AutoreleasePoolPage是一个C++实现的类



 

  • AutoreleasePool并没有单独的结构,而是由若干个AutoreleasePoolPage以双向链表的形式组合而成(分别对应结构中的parent指针和child指针)
  • AutoreleasePool是按线程一一对应的(结构中的thread指针指向当前线程)
  • AutoreleasePoolPage每个对象会开辟4096字节内存(也就是虚拟内存一页的大小),除了上面的实例变量所占空间,剩下的空间全部用来储存autorelease对象的地址
  • 上面的id *next指针作为游标指向栈顶最新add进来的autorelease对象的下一个位置
  • 一个AutoreleasePoolPage的空间被占满时,会新建一个AutoreleasePoolPage对象,连接链表,后来的autorelease对象在新的page加入

所以,若当前线程中只有一个AutoreleasePoolPage对象,并记录了很多autorelease对象地址时内存如下图:



 

图中的情况,这一页再加入一个autorelease对象就要满了(也就是next指针马上指向栈顶),这时就要执行上面说的操作,建立下一页page对象,与这一页链表连接完成后,新page的next指针被初始化在栈底(begin的位置),然后继续向栈顶添加新对象。

所以,向一个对象发送- autorelease消息,就是将这个对象加入到当前AutoreleasePoolPage的栈顶next指针指向的位置

释放时刻

每当进行一次objc_autoreleasePoolPush调用时,runtime向当前的AutoreleasePoolPage中add进一个哨兵对象,值为0(也就是个nil),那么这一个page就变成了下面的样子:



 

objc_autoreleasePoolPush的返回值正是这个哨兵对象的地址,被objc_autoreleasePoolPop(哨兵对象)作为入参,于是:

  1. 根据传入的哨兵对象地址找到哨兵对象所处的page
  2. 在当前page中,将晚于哨兵对象插入的所有autorelease对象都发送一次- release消息,并向回移动next指针到正确位置
  3. 补充2:从最新加入的对象一直向前清理,可以向前跨越若干个page,直到哨兵所在的page

刚才的objc_autoreleasePoolPop执行后,最终变成了下面的样子:



 

嵌套的AutoreleasePool

知道了上面的原理,嵌套的AutoreleasePool就非常简单了,pop的时候总会释放到上次push的位置为止,多层的pool就是多个哨兵对象而已,就像剥洋葱一样,每次一层,互不影响。


【附加内容】

Autorelease返回值的快速释放机制

值得一提的是,ARC下,runtime有一套对autorelease返回值的优化策略。
比如一个工厂方法:

1
2
3
4
5
+ (instancetype)createSark {
    return [self new]; 
}
// caller
Sark *sark = [Sark createSark];

秉着谁创建谁释放的原则,返回值需要是一个autorelease对象才能配合调用方正确管理内存,于是乎编译器改写成了形如下面的代码:

1
2
3
4
5
6
7
8
+ (instancetype)createSark {
    id tmp = [self new];
    return objc_autoreleaseReturnValue(tmp); // 代替我们调用autorelease
}
// caller
id tmp = objc_retainAutoreleasedReturnValue([Sark createSark]) // 代替我们调用retain
Sark *sark = tmp;
objc_storeStrong(&sark, nil); // 相当于代替我们调用了release

一切看上去都很好,不过既然编译器知道了这么多信息,干嘛还要劳烦autorelease这个开销不小的机制呢?于是乎,runtime使用了一些黑魔法将这个问题解决了。

黑魔法之Thread Local Storage

Thread Local Storage(TLS)线程局部存储,目的很简单,将一块内存作为某个线程专有的存储,以key-value的形式进行读写,比如在非arm架构下,使用pthread提供的方法实现:

1
2
void* pthread_getspecific(pthread_key_t);
int pthread_setspecific(pthread_key_t , const void *);

说它是黑魔法可能被懂pthread的笑话- -

在返回值身上调用objc_autoreleaseReturnValue方法时,runtime将这个返回值object储存在TLS中,然后直接返回这个object(不调用autorelease);同时,在外部接收这个返回值的objc_retainAutoreleasedReturnValue里,发现TLS中正好存了这个对象,那么直接返回这个object(不调用retain)。
于是乎,调用方和被调方利用TLS做中转,很有默契的免去了对返回值的内存管理。

于是问题又来了,假如被调方和主调方只有一边是ARC环境编译的该咋办?(比如我们在ARC环境下用了非ARC编译的第三方库,或者反之)
只能动用更高级的黑魔法。

黑魔法之__builtin_return_address

这个内建函数原型是char *__builtin_return_address(int level),作用是得到函数的返回地址,参数表示层数,如__builtin_return_address(0)表示当前函数体返回地址,传1是调用这个函数的外层函数的返回值地址,以此类推。

1
2
3
4
5
6
- (int)foo {
    NSLog(@"%p", __builtin_return_address(0)); // 根据这个地址能找到下面ret的地址
    return 1;
}
// caller
int ret = [sark foo];

看上去也没啥厉害的,不过要知道,函数的返回值地址,也就对应着调用者结束这次调用的地址(或者相差某个固定的偏移量,根据编译器决定)
也就是说,被调用的函数也有翻身做地主的机会了,可以反过来对主调方干点坏事。
回到上面的问题,如果一个函数返回前知道调用方是ARC还是非ARC,就有机会对于不同情况做不同的处理

黑魔法之反查汇编指令

通过上面的__builtin_return_address加某些偏移量,被调方可以定位到主调方在返回值后面的汇编指令

1
2
3
4
5
// caller 
int ret = [sark foo];
// 内存中接下来的汇编指令(x86,我不懂汇编,瞎写的)
movq ??? ???
callq ???

而这些汇编指令在内存中的值是固定的,比如movq对应着0x48。
于是乎,就有了下面的这个函数,入参是调用方__builtin_return_address传入值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
static bool callerAcceptsFastAutorelease(const void * const ra0) {
    const uint8_t *ra1 = (const uint8_t *)ra0;
    const uint16_t *ra2;
    const uint32_t *ra4 = (const uint32_t *)ra1;
    const void **sym;
    // 48 89 c7    movq  %rax,%rdi
    // e8          callq symbol
    if (*ra4 != 0xe8c78948) {
        return false;
    }
    ra1 += (long)*(const int32_t *)(ra1 + 4) + 8l;
    ra2 = (const uint16_t *)ra1;
    // ff 25       jmpq *symbol@DYLDMAGIC(%rip)
    if (*ra2 != 0x25ff) {
        return false;
    }
    ra1 += 6l + (long)*(const int32_t *)(ra1 + 2);
    sym = (const void **)ra1;
    if (*sym != objc_retainAutoreleasedReturnValue)
    {
        return false;
    }
    return true;
}

它检验了主调方在返回值之后是否紧接着调用了objc_retainAutoreleasedReturnValue,如果是,就知道了外部是ARC环境,反之就走没被优化的老逻辑。

其他Autorelease相关知识点

使用容器的block版本的枚举器时,内部会自动添加一个AutoreleasePool:

1
2
3
[array enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
    // 这里被一个局部@autoreleasepool包围着
}];

当然,在普通for循环和for in循环中没有,所以,还是新版的block版本枚举器更加方便。for循环中遍历产生大量autorelease变量时,就需要手加局部AutoreleasePool咯。 

 

感谢:http://blog.sunnyxx.com/2014/10/15/behind-autorelease/

  • 大小: 49.1 KB
  • 大小: 42.2 KB
  • 大小: 44 KB
  • 大小: 34.4 KB
分享到:
评论

相关推荐

    Objective-C中的Autorelease Pool揭秘:深入理解自动释放机制

    ### Objective-C 中的 Autorelease Pool 揭秘:深入理解自动释放机制 Objective-C 作为一种面向对象的编程语言,因其在 macOS 和 iOS 开发中的广泛应用而备受关注。它不仅支持面向对象的基本特性,如封装、继承和...

    前端开源库-node-circleci-autorelease

    **前端开源库-node-circleci-autorelease** 在前端开发领域,开源库扮演着至关重要的角色,它们为开发者提供了可复用的代码模块,提高了开发效率。`node-circleci-autorelease` 是一个专门针对CircleCI的开源工具,...

    在非ARC环境下的内存管理

    ARC是苹果在2011年引入的一种机制,它自动处理对象的引用计数,使得开发者不再需要手动调用`retain`, `release`, 和`autorelease`方法。但在iOS 5之前的版本或者某些特定场景下,我们可能需要在没有ARC的环境下进行...

    Node.js-mysql-autoRelease:node.js mysql事务自动释放连接

    "Node.js-mysql-autoRelease" 是一个针对Node.js的MySQL连接管理模块,其核心目标是实现MySQL事务处理时自动释放数据库连接,以确保资源的有效管理和系统的稳定运行。以下将详细介绍该模块的工作原理、相关技术点...

    autorelease-github:通过 GitHub API 为您的构建管道自动发布

    autorelease-github 通过 GitHub API 为您的构建管道自动发布 这是一个非常简单的项目,它需要 N 个文件并使用 bash、curl 和 jq 通过上传它们。 它旨在成为构建管道中的嵌入式发布构建。 输入是一堆文件和一个放...

    IOS高级内存管理编程指南.pdf

    本文将详细介绍iOS内存管理的知识点,包括内存管理的基本概念、策略、最佳实践和实战技巧,以及Autorelease池的使用等。 ### 1. 内存管理概述 内存管理是指在程序运行时对内存进行分配、使用和释放的过程。在...

    AutoReleaseTool:自动部署桌面应用程序的CICD工具

    通过在完整的CI / CD管道配置中使用AutoRelease,为您的桌面应用程序创建新版本并将其部署到其用户所需的全部过程很简单: 将您的更改推送到定义的github版本分支触发一个webhook,该webhook将在 WM中启动构建过程...

    有时侯我们需要延迟一个对象的引用计算减一操作

    在本文中,我们将深入探讨一种特定的内存管理技术,即`autorelease`机制,以及它如何与`NSAutoreleasePool`相关联。 标题所提到的“有时侯我们需要延迟一个对象的引用计算减一操作”,指的是在某些情况下,我们不...

    ios高级内存管理编程指南

    - **autorelease**: 将对象放入autorelease池中,稍后由系统自动释放。 **2. 延时release—使用autorelease** 在某些情况下,为了减少内存压力,可以使用`autorelease`。这会将对象放入一个autorelease池中,待...

    Objective-C内存管理课件.doc

    在Objective-C 2.0之前,程序员需要手动管理内存,主要通过四个关键的方法:`alloc`、`release`、`retain`和`autorelease`。理解这些概念对于防止内存泄漏和避免程序崩溃至关重要。 1. `alloc`:当你创建一个新的...

    Auto Release Sh*t-crx插件

    "Auto Release Sh*t-crx插件"是一款专为中文用户设计的自动化工具,主要用于简化发布申请流程。这个插件的名称直译为“自动发布*”,它的主要功能是自动生成发版申请,大大提升了工作效率,尤其对于那些频繁需要进行...

    iOS内存暴增问题追查与使用陷阱.docx编程资料

    本文旨在深入探讨iOS平台下的内存管理机制、 autorelease机制以及内存暴增的追查方法和解决方案。 #### 二、iOS平台内存管理介绍 **2.1 对象所有权与销毁原则** - **2.1.1 谁创建,谁释放** - 如果通过`alloc`、...

    OC(完整)内存管理文档(中文)

    - **计数器原理**:每个对象背后都有一个引用计数器,用于追踪对象被引用的数量。当计数器的值降为零时,对象会被自动销毁。 - **计数器操作**: - **增加计数**:通过`alloc`、`copy`等创建对象的方法会增加...

    弹出框的运用

    PoPoverViewController * popover = [[[PoPoverViewController alloc]initWithNibName:@"PoPoverViewController" bundle:nil] autorelease]; popover.mainViewController = self; UIPopoverController * pcr = [...

    IOS IPhone 内存管理

    IOS IPhone 内存管理 IOS IPhone 内存管理是指在 ...IOS IPhone 内存管理机制需要开发者手动管理内存,使用 retain 和 release 机制来避免无效指针,遵守一些规则以使用 autorelease pool,以避免内存泄露和崩溃。

    ios面试总结

    - 在非主线程中使用`autorelease`时,必须创建并管理自己的`NSAutoreleasePool`,以避免内存泄漏或异常。通常在线程开始时创建`NSAutoreleasePool`,在适当的时候(如任务完成后)进行drain,以释放自动释放的对象...

    Cocos2d-X面试题

    以下是 Cocos2d-X 面试题汇总,涵盖 autorelease 和 release 的区别、图形渲染机制、cache 机制原理、场景切换的内存处理过程、动作回调函数的原理、减少内存开销的方法、图片压缩方法、处理、存储、显示中文字符串...

    iPhone Mac Objective-C内存管理教程和原理剖析

    5. 引入 autorelease:当对象不再需要立即释放,但也不确定何时不再需要时,可以使用 autorelease。autorelease 会在稍后的某个时间点(通常是下次事件循环)调用 release。ClassA *obj1 = [[ClassA alloc] init]; /...

    ARC完全指南

    - **深入探讨**:虽然ARC通常自动处理 autorelease 和 autorelease pool 的使用,但在某些情况下可能需要手动管理。 - **手动管理**:例如,在长时间运行的任务中,为了避免内存消耗过大,可以显式地创建和释放 ...

    详细讲述在采用引用计数的内存回收方式的工作原理[定义].pdf

    引用计数是一种常见的...总之,引用计数、`alloc`、`release`、`retain`、`autorelease`以及`NSAutoreleasePool`是Objective-C内存管理的关键组成部分,理解并正确使用这些概念对于避免内存泄漏和程序崩溃至关重要。

Global site tag (gtag.js) - Google Analytics