ARC下关于autorelease的一些疑问

前言

今天在看黑幕背后的Autorelease这篇博客的时候,发现了一些问题。主要问题在于:

// ARC 情况下
@autoreleasepool {
  id __strong array = [NSMutableArray array];
  NSLog(@"array count is %lu", _objc_rootRetainCount(array));
  _objc_autoreleasePoolPrint();
}

这里的array对象的retainCount到底是1还是2?

按照查找的博客,以及之前看的《Objective-C高级编程 iOS与OS X多线程和内存管理》这本书的结论:这里的retainCount应该是1,因为这个对象没有注册到autoreleasepool中。但是实际运行下来却并非如此。

Stack Overflow

Autorelease

AutoreleasePool 自动释放池

Autorelease就是延迟release的调用时间,将需要延迟release的对象加入autoreleasepool。当pool倾倒时,就开始调用pool中的对象的release方法。每一个线程对应一个自动释放池。

在ARC中,使用@autoreleasepool{}来表示一个自动释放池。将OC代码通过clang的-rewrite-objc,可以生成C++代码。可以看到自动释放池是一个__AtAutoreleasePoolC++对象。然后这个对象只是对AutoreleasePoolPage对象的封装,可以在NSObject.mm文件中找到它的详细定义。

下面这两篇博客对自动释放池有详细的解释:

  1. 黑幕背后的Autorelease
  2. OC源码 —— autoreleasepool

Autorelease返回值的快速释放机制

// ARC下
id array = [NSMutableArray array];

+ (NSMutableArray *)array {
  return [[NSMutableArray alloc] init];
}

因为array类方法是不持有对象的,那么正常情况下调用该方法时,该方法会将返回对象先注册到自动释放池;但是由于ARC下面存在这么一个机制,可以不将对象注册到自动释放池,直接给array持有。

这个机制主要由这两个函数构成:

  1. objc_autoreleaseReturnValue(id);
  2. objc_retainAutoreleasedReturnValue(id);

不过通过我的测试,发现这个说法有点说不通啊。来看看实际的情况

实地测试

测试的时候需要两个关键的函数:

  1. uintptr_t _objc_rootRetainCount(id obj); : 返回对象的retainCount
  2. void _objc_autoreleasePoolPrint(); :打印此时的自动释放池的内容

MacOS Command Line Tools

新建一个MacOS Command Line Tools的项目,总共只有下面这些代码:

#import <Foundation/Foundation.h>

extern uintptr_t _objc_rootRetainCount(id obj);
extern void _objc_autoreleasePoolPrint();

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        id array = [NSMutableArray array];
        NSLog(@"array count is %lu", _objc_rootRetainCount(array));
        _objc_autoreleasePoolPrint();
    }

    return 0;
}

来看看输出结果:

2017-07-13 20:21:52.419722 autoreleaseMac[87916:6040616] array count is 2
objc[87916]: ##############
objc[87916]: AUTORELEASE POOLS for thread 0x10012c3c0
objc[87916]: 2 releases pending.
objc[87916]: [0x102001000]  ................  PAGE  (hot) (cold)
objc[87916]: [0x102001038]  ################  POOL 0x102001038
objc[87916]: [0x102001040]       0x1004032b0  __NSArrayM
objc[87916]: ##############
Program ended with exit code: 0

可以发现:使用array类函数生成的对象不仅被array给强引用,还被注入到了自动释放池;所以他的retainCount为2。这就说明了在Mac下面,貌似这个Autorelease返回值的快速释放机制不起作用

iOS APP

新建一个iOS的项目,来看看iOS的viewDidLoad方法:

- (void)viewDidLoad {
    [super viewDidLoad];

    // id __weak ref = nil;   先暂时注释掉这一行

    id array = [NSMutableArray array];
      NSLog(@"%p", array);
    NSLog(@"array count is %lu", _objc_rootRetainCount(array));
    _objc_autoreleasePoolPrint();
}

来看看打印结果:

2017-07-13 20:31:29.637 autorelease[89067:6068334] array count is 1
objc[89067]: ##############
objc[89067]: AUTORELEASE POOLS for thread 0x112c2b3c0
objc[89067]: 888 releases pending.
...
// 并没有发现array对象加入了自动释放池

在iOS下面这个array对象的retainCount位1,也没有加入到自动释放池。这么看貌似这个快速释放机制起作用了

先别急:让我们取消注释,声明一个__weak变量,再来看看运行结果:

2017-07-13 20:35:39.510 autorelease[89587:6080048] 0x6080000468a0  // 对象地址
2017-07-13 20:35:39.510 autorelease[89587:6080048] array count is 2
objc[89587]: ##############
objc[89587]: AUTORELEASE POOLS for thread 0x1110ce3c0
objc[89587]: 889 releases pending.
...
objc[89587]: [0x7ff4b281ec20]    0x7ff4b4005f80  ViewController
objc[89587]: [0x7ff4b281ec28]    0x7ff4b4005f80  ViewController
objc[89587]: [0x7ff4b281ec30]    0x6080000468a0  __NSArrayM   // 加入到自动释放池中的对象地址,和上面的相同
objc[89587]: ##############

可以看到retainCount变成了2,而且也确实是将array对象加入到了自动释放池中。这时候这个快速释放机制貌似又失效了。。

这里的一个完全无关的__weak变量的声明,似乎是这个快速释放机制的开关;有点玄学=。=

后续操作

clang -S

可以通过clang的-S选项,来看看这些文件生成的汇编代码,或许可以从中看出些端倪。但是我不懂汇编=.=

LinkMap

开启Xcode的Write Link Map File选项,查看可执行文件的构成。

当没有定义__weak变量时:

without

当有定义__weak变量时:

with

可以看到在定义了__weak变量时,多了两个函数的调用:

  1. __Unwind_Resume
  2. _objc_destroyWeak

所以感觉问题就在这两个函数之中

Stack Overflow

我将该问题放在了Stack Overflow上面了。如果哪位大神碰巧看到了这篇文章,并且还耐心的看到了这里,碰巧还知道答案的话,可以到StackOverflow上面帮我解解惑

参考

当前网速较慢或者你使用的浏览器不支持博客特定功能,请尝试刷新或换用Chrome、Firefox等现代浏览器