玩转 @autoreleasepool

「这是我参与11月更文挑战的第20天,活动详情查看:2021最后一次更文挑战」。

Foundation 的NSAutoreleasePool类型,后来抽象为@autoreleasepool块,在 iOS 开发中是一个非常古老的概念。在 iOS 的 ObC 时代,这种类型的使用对于防止您的应用程序的内存在特定情况下爆炸很重要。随着 ARC 和 Swift 的出现和发展,很少有人仍然需要手动管理内存,这使得看到它变得罕见。

@autoreleasepool(OC)

在手动存储器管理的预ARC的OC的时候,retain()并且release()不得不被用来控制一个iOS应用的存储器流。由于 iOS 的内存管理基于对象的保留计数工作,因此用户可以使用这些方法来指示对象被引用的次数,以便在该值达到零时可以安全地释放它。但是,请考虑以下情况,其中我们有一种getCoolLabel方法可供某人使用以获得非常酷的标签:

-(NSString *)getCoolLabel {
     NSString *label = [[NSString alloc] initWithString:@"SwiftRocks"];
     return label;
 }
复制代码

NSString alloc自动调用retain()以确保label能够存在(保留计数 1),然后将标签返回给想要引用它的其他人,再次保留它(保留计数 2)直到没有人再使用它。但是这里有一个很大的问题。在调用getCoolLabel调用release()以发出不再需要它的信号的堆栈之后,保留计数不会是 0,而是 1。NSString *label被创建用来保持的内部也保留它,并且它也需要被释放,如果我们希望 NSString 本身解除分配。问题是,由于label在此方法之外无法访问,我们无法释放它:

-(NSString *)getCoolLabel {

     NSString *label = [[NSString alloc] initWithString:@"SwiftRocks"];
     [label release]; // Oopsie, nope!
     return label;
     [label release]; // Oopsie, nope!
 }
复制代码

如果release()在 before 调用return,NSString 会在可以使用之前解除分配,这会导致应用程序崩溃,而在 after 调用return意味着它永远不会被执行,从而导致内存泄漏。 这种边缘情况的解决方案是使用一种称为 的简洁方法autorelease():

-(NSString *)getCoolLabel {
     NSString *label = [[NSString alloc] initWithString:@"SwiftRocks"];
     return [label autorelease];
 }
复制代码

不是立即减少对象的保留计数,而是将对象autorelease()添加到需要在未来某个时间释放的对象池中,但不是现在。默认情况下,池会在正在执行的线程的运行循环结束时释放这些对象,这足以覆盖所有使用而getCoolLabel()不会导致内存泄漏。很棒,对吧?

嗯,有点。这确实会在 99% 的情况下解决您的问题,但请考虑以下这个问题:

-(void)emojifyAllFiles {
     int numberOfFiles = 1000000;
     for(i=0;i<numberOfFiles;i++) {
         NSString *contents = [self getFileContents:files[i]];
         NSString *emojified = [contents emojified];
         [self writeContents:contents toFile:files[i]];
     }
 }
复制代码

假设getFileContents和emojified返回自动释放的情况下,应用程序将举行200万个NSString的实例一次,即使个别属性可以各自循环后安全获释!  因为autorelease推迟释放这些对象,它们只会在运行循环结束后才会被释放——也就是在emojifyAllFiles. 如果这些文件的内容很大,如果不完全崩溃应用程序,这将导致严重的问题。

防止这种情况的解决方案是@autoreleasepool块;使用时,其中定义的每个 autoreleased 属性都将在块的末尾完全释放:

-(void)emojifyAllFiles {
     int numberOfFiles = 1000000;
     for(i=0;i<numberOfFiles;i++) {
         @autoreleasepool {
             NSString *contents = [self getFileContents:files[i]];
             NSString *emojified = [contents emojified];
             [self writeContents:contents toFile:files[i]];
         }
     }
 }
复制代码

两个 NSStrings 现在不是等到线程的运行循环结束,而是在被调用后立即收到一条release消息,从而保持内存使用稳定。 writeContents事实上,“在线程的运行循环之后释放”并不是编译器魔法,这仅仅是因为线程本身被@autoreleasepools! 您可以在任何 OC项目的 main.m 中部分地看到这一点。

int main(int argc, char * argv[]) {
     @autoreleasepool {
         return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
     }
 }
复制代码

@autoreleasepool(Swift)

Swift 的 ARC 优化在过去几年有了很大的发展,据我测试,ARC for Swift 现在似乎足够聪明,可以从不调用autorelease,编辑代码以便对象release多次调用。该语言本身似乎甚至没有针对autorelease OC 桥接之外的定义——我们可以在 Swift 中使用的实际上是来自 OC 的定义。这意味着对于纯 Swift 对象来说,@autoreleasepool似乎是无用的,因为什么都不会autoreleased。即使循环数百万次,以下代码仍保持稳定的内存级别。

Swift 代码调用 Foundation / Legacy OC 代码

但是,如果您的代码处理遗留的 OC 代码,特别是 iOS 中的旧 Foundation 类,则情况就不同了。考虑以下加载大图像的代码:

func run() 
     guard let file = Bundle.main.path(forResource: "bigImage", ofType: "png") else {
         return
     }
     for i in 0..<1000000 {
         let url = URL(fileURLWithPath: file)
         let imageData = try! Data(contentsOf: url)
     }
 }
复制代码

即使我们在 Swift 中,这也会导致 Obj-C 示例中显示的同样荒谬的内存峰值!这是因为Data init 是原始 Obj-C 的桥梁[NSData dataWithContentsOfURL]——不幸的是autorelease,它仍然在它内部的某个地方调用。就像在 OC 中一样,您可以使用 Swift 版本@autoreleasepool来解决这个问题。

autoreleasepool {
     let url = URL(fileURLWithPath: file)
     let imageData = try! Data(contentsOf: url)
 }
复制代码

内存使用量现在将再次处于稳定的低水平。 简而言之,它autoreleasepool在 OC/Swift 开发中仍然很有用,因为 UIKit 和 Foundation 中仍有遗留的 OC 类调用autorelease,但是由于 ARC 的优化,您在处理 Swift 类时可能不需要担心它。

猜你喜欢

转载自juejin.im/post/7034906566000115720
今日推荐