探究是否需要@autoreleasepool优化循环

2,822 阅读3分钟

这篇文章是在研究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. 使用其它方法创建的临时对象

这种情况比较复杂。关键点在于autoreleaseReturnValueretainAutoreleasedReturnValue 两个方法。

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吧。