iOS基础 | 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];

ARC是什么?

现在我们已经掌握了引用计数管理内存的方法,但是日常开发中几乎不需要手动管理内存——手动引用计数 MRC(mannul reference counting),因为苹果为我们提供了更加高效、安全的管理内存方式——自动引用计数 ARC(automatic reference counting)。ARC像是一位内存管家,开启 ARC 后编译器会帮助你插入retainrelease语句。也就是说,ARC 是在编译时进行工作的。

ARC使用条件:
  • 能够确定哪些对象需要内存管理
  • 能够表明如何管理对象
  • 有可行的办法传递对象所有权

桥接转换

日常开发中99%的内存管理工作都交由编译器了,即ARC。我曾调侃内存管理的使用做多的场景是——面试,这既是玩笑话也是实话。那么还有1%的情况,即如何对非OC对象进行内存管理,这就用到了桥接装换(bridge cast)的C语言技术。

总结

Cocoa的内存管理规则:
如果使用new、alloc、或copy获得了一个对象,则该对象的引用计数器值为1;
如果通过其他方法获得一个对象,则假设该对象的引用计数器值为1,而且已经被设置为自动释放;
如果保留了某对象,则必须保持retain方法和release方法的使用次数相等。

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,080评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,422评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,630评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,554评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,662评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,856评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,014评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,752评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,212评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,541评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,687评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,347评论 4 331
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,973评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,777评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,006评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,406评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,576评论 2 349

推荐阅读更多精彩内容