Osheep

时光不回头,当下最重要。

Cocoa内存管理(一)

什么是内存管理?

内存管理是程序设计中常见的资源管理(resource management)的一部分。每个计算机系统的可供程序使用的资源都是有限的,包括打开文件、网络连接、图片处理等。以图书馆为例,如果每个人都只借不还,那么图书馆最终因将无书可借而倒闭,其他人也无法再使用图书馆。内存管理,即在程序需要的时候分配内存,程序运行结束时释放占用内存。如果只分配不释放就会发生内存泄漏(leak memory):程序的内存占用不断增加,最终耗尽并导致程序崩溃。同时也要注意,不要使用刚释放的内存,避免误读陈旧数据引发的各种错误。在Cocoa框架中,通过引用计数的方式实现内存管理。

什么是引用计数?

Cocoa采用一种叫做引用计数(reference counting)的技术管理内存。每个对象都有一个与之相关联的整数,被称作它的引用计数器。当某段代码需要访问一个对象时,该代码就将该对象的引用计数器值加1,表示“我要访问该对象”。当这段代码结束对象访问时,将对象的引用计数器值减1,表示“我不再访问该对象”。当该对象的引用计数器值为0时,表示“不再有代码访问该对象”。因此,它将被销毁,其占用的内存被系统收回以便重用。

如何使用 Objective-C 进行内存管理?

当使用allocnew方法或者copy消息创建一个对象时,对象的引用计数器值被置为1。需要增加对象的引用计数器值时,可以向对象发送一条retain消息。要减少时,向对象发送一条release消息。当对象的引用计数器值归0时,Objective-C会自动向对象发送dealloc消息。(想要获得当前的引用计数器值,可以向对象发送一条retainCount消息)

等等,这么看的话内存管理也不过如此嘛,有啥难的?那是因为我们还没考虑对象所有权(object ownership),即某个实体持有一个对象时,该实体就要负责对其持有的对象进行释放。

对象所有权

如果一个对象内有指向其他对象的实例变量,则称该对象持有这些对象。例如:Car类中包含一个属性engine,Car对象持有Engine对象。同样如果在一个函数中创建了一个对象,则称这个函数持有该对象。例如:在main()中创建了一个Engine对象,则main()持有该对象。我们已经知道了谁持有谁释放,接下来看一个例子:

int main(int argc, const char * argv[]) {
    Car *car = [Car new];

    Engine *engine = [Engine new];
    [car setEngine:engine];

    return 0;
}

现在哪个实体持有engine对象?main()函数还是car对象?哪个实体负责确保当engine对象不再被使用时能够收到release消息?因为car对象正在使用engine对象,所以不可能是main()函数。同理mian()函数后面可能还会使用engine对象,也不是car对象。

解决办法让engine对象的引用计数器值增加到2。Car类应该在setEngine:方法中保留engine对象,当car释放时在其dealloc方法中释放engine。

setter方法中的保留与释放

- (void)setEngine:(Engine *)newEngine {
    _engine = [newEngine retain];
}

我们知道Car类setter中需要保留newEngine。但是仅仅保留newEngine是不够的,比如下面这种情况:

int main(int argc, const char * argv[]) {
    Car *car = [Car new];

    Engine *engine1 = [Engine new]; // retain count:1
    [car setEngine:engine1]; // retain count:2
    [engine1 release]; // retain count:1

    Engine *engine2 = [Engine new]; // retain count:1
    [car setEngine:engine2]; // retain count:2

    return 0;
}

我们可以看到[engine1 release],即mian()已经释放了engine1对象的引用,Car类也指向新的engine对象,可是engine1对象的引用计数仍然是1。现在engine1已经发生类内存泄漏,engine1会一直空转占用内存。
接下来修该setter如下:

- (void)setEngine:(Engine *) newEngine {
    [newEngine release];
    _engine = [newEngine retain];
}

现在新的setter已经修复了,engine1对象会内存泄漏的问题。可是这样还是不够的。例如下面这种情况:

int main(int argc, const char * argv[]) {

    Engine *engine = [Engine new]; // retain count:1
    Car *car1 = [Car new];
    Car *car2 = [Car new];

    [car1 setEngine:engine]; // retain count:2
    [engine release]; // retain count:1

    [car2 setEngine:[car1 engine]]; // oops!

    return 0;
}

当engine和_engine是同一个对象时,[car1 setEngine:engine]将engine对象的引用计数器值归0,并释放掉engine对象。这时再让car2指向一块已经释放掉的内存就会引发错误。进一步修改后的setter:

- (void)setEngine:(Engine *) newEngine {
    [_engine retain];
    [newEngine release];
    _engine = newEngine;
}

现在我们已经知道setter中应该先保留新值,再释放旧值,然后进行赋值。

自动释放

通过上一篇文章,我们已经知道了谁持有谁释放。如果一个对象由函数持有就函数释放,由某个类持有就让类来释放。看下面这种情况:

- (NSString *)description {

    NSString *description = [[NSString alloc] initWithFormat:@"hello world"];
    return description;

}

看上去desctiption方法持有NSString对象description,那么description方法应该负责释放description对象,但是description一旦释放就无法返回。这样就引出了下一个概念:自动释放池。

自动释放池

Cocoa中有一个自动释放池(autorelease pool)的概念。我们在程序的入口mian()函数中都看过关键字@autoreleasepool。为了理解自动释放池的工作,首先要用到NSObject类提供的autorelease方法:

- (id)autorelease;

该方法的作用是,预先设定会在未来某个时间想对象发送一条release消息,其返回值是接接收这条消息的对象。当给一个对象发送autorelease消息时,实际上是将该对象添加到了自动释放池中。当自动释放池呗销毁时,会想池中所有对象发送release消息。改写后的代码如下:

- (NSString *)description {

    NSString *description = [[NSString alloc] initWithFormat:@"hello world"];
    return [description autorelease];

}

那么我们怎么知道自动释放池什么时候被销毁呢?

自动释放池销毁时间

自动释放池什么时候销毁,并向其包含所有对象发送release消息?既然是销毁,那么创建是在什么时候,如何创建?创建自动释放池有两种方法:

  • 通过@autoreleasepool关键字
  • 通过NSAutoreleasePool对象

1.使用@autoreleasepool{}时,所有花括号里的代码都会放入新池子里。但是要注意,任何在花括号里定义的变量在括号外就无法使用了。
2.既然NSAutoreleasePool对象也是NSObject对象,同样遵守引用计数内存管理方式。如下:

NSAutoreleasePool *pool = [NSAutoreleasePool new];
// 创建对象...
[pool release];

两种方法推荐使用:@autoreleasepool关键字,因为Objective-C语言创建和释放内存的能力远在我们之上。下面看一下使用示例:

int main (int argc, const char * argv[])
{
    NSAutoreleasePool *pool;
    pool = [[NSAutoreleasePool alloc] init];

    RetainTracker *tracker;
    tracker = [RetainTracker new]; // count: 1

    [tracker retain]; // count: 2
    [tracker autorelease]; // count: still 2
    [tracker release]; // count: 1

    NSLog (@"releasing pool");
    [pool release]; 
    // gets nuked, sends release to tracker

    @autoreleasepool
    {
        RetainTracker *tracker2;
        tracker2 = [RetainTracker new]; // count: 1

        [tracker2 retain]; // count: 2
        [tracker2 autorelease]; // count: still 2
        [tracker2 release]; // count: 1

        NSLog (@"auto releasing pool");
    }

    return (0);
}

注意: [tracker autorelease],向tracker对象发送autorelease消息后,tracker对象的引用计数器值并没有立即减1,而是保持不变,依旧为2。当自动释放池销毁时,将向tracker对象发送release消息。运行程序,控制台输出结果为:

init: Retain count of 1.
releasing pool
dealloc called. Bye Bye.
init: Retain count of 1.
auto releasing pool
dealloc called. Bye Bye.

打印结果验证了自动释放池的释放时间先于其包含的对象。

请记住,自动释放池被销毁的时间是确定的:要么是在代码中你自己手动销毁,要么是使用APPKiti时在事件循环结束时销毁。

有时即使我们使用了自动释放池,程序的内存却仍然增长。如下面这种情况:

    int i;
    for (i = 0; i < 1000000; i++) {
        id obj = [someArray objectAtIndex:i];
        NSString *desc = [obj description];
    }

该程序执行了一个循环,这个循环创建了100w个desc字符串对象,直到循环结束自动释放池才能释放。因为自动释放池的销毁时间是确定的,循环执行过程中不会被销毁。解决这一问题的方法是在循环中创建自己的自动释放池。优化代码如下:

    NSAutoreleasePool *pool = [NSAutoreleasePool new];
    int i;
    for (i = 0; i < 1000000; i++) {
        id obj = [someArray objectAtIndex:i];
        NSString *desc = [obj description];
        if (i % 1000 == 0) {
            [pool release];
            pool = [NSAutoreleasePool new];
        }
    }
    [pool release];

引用:《Objective-C 基础教程》

点赞