Matrixzk’s Blog

keep moving

OC内存管理那些事儿(3):使用自动释放池块

Nov 13th, 2014

自动释放池块提供了一种机制,通过它我们在放弃一个对象的所有权时,可以避免该对象被立即释放掉(就像从一个方法返回一个对象那样)。通常情况下不需要创建自己的自动释放池的,但有些特殊情况就必须或者说有必要这样做。

关于自动释放池块(Autorelease Pool Blocks)

使用@autoreleasepool来标记一个自动释放池块,例如:

1
2
3
@autoreleasepool {
    // Code that creates autoreleased objects.
}

在自动释放池块的末尾,块内所有接收过autorelease消息的对象都会收到一个release消息,这些对象之前接收过多少次autorelease消息,此时就会接收到相应次数的release消息。

和其他代码块一样,自动释放池块可以嵌套:

1
2
3
4
5
6
7
@autoreleasepool {
    // ...
    @autoreleasepool {
        // ...
    }
    ...
}

但一般不会像上边那么用;通常的做法是,一个源文件中的某自动释放池块内的代码,去调用另一个源文件中包含在某自动释放池块内的代码。对于某给定的autorelease消息,会在发送该autorelease消息所在的自动释放池块的末尾处发送相应的release消息。

Cocoa 总是期望代码都被执行在某个自动释放池块内,否则那些自动释放的对象( autoreleased )就不会接收到release消息,就会导致内存泄漏。(如果在自动释放池块外发送一个autorelease消息,Cocoa 会打印一条相应的错误信息日志。) AppKit 和 UIKit 框架会在一个自动释放池块内执行每个事件循环(event-loop,比如一个鼠标点击时间或者一个tap)。因此通常不需要自己创建自动释放池块,甚至连创建它们的代码都看不到。但是,有三种场景需要创建自己的自动释放池块

  • 所写的程序不是基于 UI 框架,比如一个命令行工具。

  • 所写的循环(loop)中创建了很多临时对象。

    在一个循环进入下次迭代之前,应该使用自动释放池块处理当前循环内自动释放的临时对象。在循环内使用自动释放池块有助于降低应用的内存占用峰值。

  • 开辟了一个二级( secondary )线程

    必须在该线程开始执行时就创建自己的自动释放池块,否则会导致内存泄漏。(详情参考自动释放池块和线程)

使用局部自动释放池块来降低内存峰值

许多程序创建的临时对象是自动释放的( autoreleased )。这些对象在程序运行到自动释放池块的结尾之前都会占据着程序的内存。在当前事件循环结束之前允许临时对象一直累积,在多数情况下不会导致过度的内存开销;但有时,创建大量的临时对象会导致内存占用大幅度升高,这时可以自己创建一个自动释放池块来及时处理下。在块的末尾,这些临时对象会被释放掉,内存占用通常也会因此而降下来。

下边的例子展示了如何在一个for循环中使用局部自动释放池块:

1
2
3
4
5
6
7
8
9
10
NSArray *urls = <# An array of file URLs #>;
for (NSURL *url in urls) {

    @autoreleasepool {
        NSError *error;
        NSString *fileContents = [NSString stringWithContentsOfURL:url
                                         encoding:NSUTF8StringEncoding error:&error];
        /* Process the string, creating and autoreleasing more objects. */
    }
}

上述for循环每次处理一个文件。自动释放池块中每个发出过autorelease消息的对象(比如 fileContents )都会在块的末尾被释放掉。

在自动释放池块之后,该块内的所有自动释放的对象都应该被认为是“已被释放的”。不要再给它发送消息或者将它返回给方法的调用者。如果一定要在自动释放池块外使用某临时产生的对象,可以在块内给它发送一个retain消息,然后在块外给它发送一个autorelease消息,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
 (id)findMatchingObject:(id)anObject {

    id match;
    while (match == nil) {
        @autoreleasepool {

            /* Do a search that creates a lot of temporary objects. */
            match = [self expensiveSearchForObject:anObject];

            if (match != nil) {
                [match retain]; /* Keep match around. */
            }
        }
    }

    return [match autorelease];   /* Let match go and return it. */
}

在自动释放池块内给match发送retain,然后在块外给其发送autorelease来扩展其生命周期,这样match就可以在循环外接收消息了,并可以将其返回给findMatchingObject:方法的调用者。

自动释放池块和线程

Cocoa 应用程序中的每个线程都维持一个它自己的自动释放池块( stack ),如果要写一个 Foundation-only 的程序,或者开辟一个新的线程,这时就需要创建自己的自动释放池块。

如果应用或线程的生命期很长( long-lived ),并且可能生成大量的自动释放的对象( autoreleased objects ),此时就需要使用自动释放池块(就像 AppKit 和 UIKit 在主线程中所做的那样)。否则,自动释放的对象的累积会导致内存占用持续增长。如果所开辟的线程不做涉及 Cocoa 的方法调用,就不需要使用自动释放池块了。


注意: 如果所创建的二级线程使用POSIX线程 APIs 而不是NSThread,此时只有 Cocoa 在多线程模式下时才能使用 Cocoa。Cocoa 只有在开辟了它的第一个NSThread对象之后才进入多线程模式。为了在二级 POSIX 线程中使用 Cocoa,必须先至少开辟一个NSThread对象,它会立即退出。可以使用NSThread类的isMultiThreaded方法来检测 Cocoa 是否处于多线程模式。

返回顶部