这篇文章是在研究autoreleasepool时发现自己看法和网上一些文章的看法有出入,因此写下自己的见解,和大家一起讨论讨论。
首先我们来看下面两个代码,大家可以猜猜看两段代码跑起来内存的使用是怎样的:
for (int i = 0; i < 20000000; i++) {
NSString *str = [[NSString alloc] initWithFormat:@"1234567890"];
}
for (int i = 0; i < 20000000; i++) {
NSString *str = [NSString stringWithFormat:@"1234567890"];
}
结果是,第一个用initWithFormat创建的NSString内存几乎没有变化。而第二个用stringWithFormat创建临时变量的循环,会导致内存暴涨。


在这里先说下我的结论:
循环是否会导致内存暴涨,主要取决于临时变量是否会加入循环外层的 autoreleasepool中。
不同的对象创建方法和属性,会导致不同的结果。
这里分为两大情况:
1. 使用alloc/new/copy/mutableCopy/init 创建临时对象
根据ARC的规定(https://clang.llvm.org/docs/AutomaticReferenceCounting.html#method-families), 使用上述方法创建对象,会直接返回被retain对象,不会进入autoreleasepool中。因此该对象在每次循环中都会因为离开作用域,被ARC加入的release释放,所以内存并不会上升。我们可以从编译成的中间语言可以看到里面只有将对象进行storeStrong的处理:

2. 使用其它方法创建的临时对象
这种情况比较复杂。关键点在于autoreleaseReturnValue与retainAutoreleasedReturnValue 两个方法。
autoreleaseReturnValue 是其它方法创建并返回对象时,ARC会帮我们自动添加的函数,而retainAutoreleasedReturnValue是变量指向该创建方法返回的对象时会添加的函数。比如
NSString *str = [NSString stringWithFormat:@"1234567890"];
编译成中间语言时会发现代码中加入了retainAutoreleasedReturnValue:

这两个方法有什么用呢?根据ARC的规定(https://clang.llvm.org/docs/AutomaticReferenceCounting.html#arc-runtime-objc-autoreleasereturnvalue), 如果这两个方法成对出现,则这两个方法会尽可能优化对象,使对象可以直接进行强引用计数,避免进入autoreleasepool;否则,对象创建完返回时会被加入autoreleasepool中,等待autoreleasepool的释放。
那么什么时候会进行优化呢?
其中一个是当指向对象的变量是强引用时会进行。
我们可以加一个Category来试验下:


然后再次在循环中创建临时对象,我们会看到内存并没有上升:

当然也有没被优化的情况,比如创建一个weak变量进行引用:

这里的临时变量就会被加进autoreleasepool中,没有在作用域结束时释放,进而造成内存暴涨。
现在让我们看回NSString的创建方法:
NSString *str = [NSString stringWithFormat:@"1234567890"];
这个会是属于没被优化的情况吗?
答案是并不属于上述任何一种情况。这个其实属于“历史遗留”问题。我找不到NSString的源代码,但从汇编中可以看到,stringWithFormat这个方法返回时直接调用了autorelease。

也就是说,NSString里依然用着MRC,没有经过ARC生成autoreleaseReturnValue与外面的retainAutoreleasedReturnValue对应,对象返回时都会直接进入autoreleasepool。因此,才会出现循环未结束,内存一直在暴涨的情况。
最后,我们还有没有必要在循环中添加@autoreleasepool呢?我的观点,是与其搞清楚复杂的优化和历史问题,不如就直接加@autoreleasepool吧。