Autorelease

本文深入探讨了iOS中Autorelease机制的工作原理,包括Autorelease对象的释放时机、AutoreleasePoolPage的作用及其实现细节,以及如何优化autorelease返回值的管理。

我是前言

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

由于这个vc在loadView之后便add到了window层级上,所以viewDidLoad和viewWillAppear是在同一个runloop调用的,因此在viewWillAppear中,这个autorelease的变量依然有值。

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

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

Autorelease原理

AutoreleasePoolPage

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

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

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

AutoreleasePoolPage是一个C++实现的类

46.jpg

  • AutoreleasePool并没有单独的结构,而是由若干个AutoreleasePoolPage以双向链表的形式组合而成(分别对应结构中的parent指针和child指针)。

  • AutoreleasePool是按线程一一对应的(结构中的thread指针指向当前线程)。

  • AutoreleasePoolPage每个对象会开辟4096字节内存(也就是虚拟内存一页的大小),除了上面的实例变量所占空间,剩下的空间全部用来储存autorelease对象的地址。

  • 上面的id *next指针作为游标指向栈顶最新add进来的autorelease对象的下一个位置。

  • 一个AutoreleasePoolPage的空间被占满时,会新建一个AutoreleasePoolPage对象,连接链表,后来的autorelease对象在新的page加入。

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

66.jpg

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

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

释放时刻

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

67.jpg

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

1.根据传入的哨兵对象地址找到哨兵对象所处的page

2.在当前page中,将晚于哨兵对象插入的所有autorelease对象都发送一次- release消息,并向回移动next指针到正确位置

3.补充2:从最新加入的对象一直向前清理,可以向前跨越若干个page,直到哨兵所在的page

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

68.jpg

嵌套的AutoreleasePool

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

【附加内容】

Autorelease返回值的快速释放机制

值得一提的是,ARC下,runtime有一套对autorelease返回值的优化策略。

比如一个工厂方法:

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

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

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

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

黑魔法之Thread Local Storage

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

  1. void* pthread_getspecific(pthread_key_t); 
  2. 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. - (int)foo { 
  2.     NSLog(@"%p", __builtin_return_address(0)); // 根据这个地址能找到下面ret的地址 
  3.     return 1; 
  4. // caller 
  5. int ret = [sark foo]; 

看上去也没啥厉害的,不过要知道,函数的返回值地址,也就对应着调用者结束这次调用的地址(或者相差某个固定的偏移量,根据编译器决定)

也就是说,被调用的函数也有翻身做地主的机会了,可以反过来对主调方干点坏事。

回到上面的问题,如果一个函数返回前知道调用方是ARC还是非ARC,就有机会对于不同情况做不同的处理

黑魔法之反查汇编指令

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

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

而这些汇编指令在内存中的值是固定的,比如movq对应着0x48。

于是乎,就有了下面的这个函数,入参是调用方__builtin_return_address传入值

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

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

其他Autorelease相关知识点

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

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

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

<think>我们在处理模板类中的嵌套类静态成员声明时,可能会遇到需要添加`typename`的情况。错误信息通常为:`need ‘typename’ before ... because ... is a dependent scope`。这是因为在模板中,编译器无法确定嵌套类的类型,除非我们使用`typename`关键字来显式说明这是一个类型。 根据你提供的错误信息:`need ‘typename’ before A<T>::AutoRelease because A<T> is a dependent scope`,我们可以推断,在类外声明静态成员时,没有正确使用`typename`关键字。 下面是一个示例模板类,其中包含一个嵌套类`AutoRelease`,并且我们想要在类外定义`AutoRelease`的一个静态成员。 错误示例: ```cpp template <typename T> class A { public: class AutoRelease; // 嵌套类的声明 static AutoRelease ar; // 静态成员声明 }; // 在类外定义嵌套类 template <typename T> class A<T>::AutoRelease { // ... 成员定义 }; // 错误的静态成员定义:缺少typename template <typename T> A<T>::AutoRelease A<T>::ar; // 这里会报错 ``` 正确的方式是在定义静态成员时,在类型说明符前添加`typename`关键字,因为`A<T>::AutoRelease`是一个依赖作用域的类型名(即依赖于模板参数T),编译器在没有实例化模板之前无法确定它是否是一个类型。因此,我们需要明确告诉编译器这是一个类型。 修正后的定义: ```cpp template <typename T> typename A<T>::AutoRelease A<T>::ar; ``` 这里,`typename`关键字用于告诉编译器`A<T>::AutoRelease`是一个类型。 因此,针对你的问题,在类外声明嵌套类的静态成员时,应该在静态成员的类型前面加上`typename`关键字,以指示该嵌套类是一个类型。 另外,注意静态成员的定义需要在全局作用域(或命名空间作用域)中进行,并且对于模板类,每个静态成员的定义本身也是一个模板定义。 如果静态成员需要在编译时初始化,并且是整型或枚举类型,可以在类内直接初始化(C++11起允许类内初始化非const静态成员,但通常还是需要在类外定义以分配存储空间)。但对于非整型的静态成员,通常需要在类外定义。 总结:在模板类外定义嵌套类的静态成员时,需要: 1. 使用`template <typename T>`开始定义。 2. 在类型名前使用`typename`关键字指明嵌套类是一个类型。 3. 按照`类型 类名<模板参数>::静态成员名`的格式定义。 这样,就可以解决上述错误。 相关问题: 1. 在C++模板中,何时需要使用`typename`关键字? 2. 模板类中的静态成员初始化有什么特殊要求? 3. 嵌套类在模板类中有什么特殊注意事项? 4. 如何理解“dependent scope”在C++模板中的含义?</think>### 解决C++模板类嵌套类静态成员声明错误 在C++中,当在模板类外部声明嵌套类的静态成员时,遇到`need 'typename' before ... because ... is a dependent scope`错误,是因为编译器无法确定嵌套类是否属于类型。以下是解决方案: #### 错误原因分析 1. **依赖作用域(Dependent Scope)** 模板类`A<T>`中的`AutoRelease`是依赖于模板参数`T`的嵌套类型 2. **编译器无法自动推断类型** 在模板实例化前,编译器无法确定`A<T>::AutoRelease`是否表示类型(可能被特化为非类型成员) 3. **缺少`typename`关键字** 必须显式告知编译器这是类型而非静态成员或变量 #### 正确解决方案 在静态成员声明前添加`typename`关键字: ```cpp template<typename T> class A { public: class AutoRelease; // 嵌套类声明 static AutoRelease ar; // 静态成员声明 }; // 嵌套类定义 template<typename T> class A<T>::AutoRelease { // 类实现... }; // ✅ 正确静态成员定义(关键修改) template<typename T> typename A<T>::AutoRelease A<T>::ar; // 添加typename ``` #### 关键说明 1. `typename`的作用: 明确告知编译器`A<T>::AutoRelease`是一个类型而非成员变量 2. 语法要求: - 只能用于模板定义中 - 必须放在完整限定名称前 - 不能用于基础数据类型(如`int`/`double`) 3. 特例情况: 如果嵌套类在基类中定义,可能需要额外添加`template`关键字: ```cpp template<typename T> typename A<T>::template AutoRelease A<T>::ar; ``` #### 错误示例对比 ```cpp // ❌ 错误写法(缺少typename) template<typename T> A<T>::AutoRelease A<T>::ar; // 触发编译错误 ``` > **根本原因**:C++标准规定,在模板的依赖作用域中,编译器默认将嵌套名称视为非类型(除非使用`typename`显式声明)[^1][^2][^3]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值