iOS 内存管理(一)

简书内容都是个人的知识点整理和笔记。

1.Objective-C中的内存管理

应用程序在运行期间通过内存管理完成内存的分配、使用和释放。而在Objective-C中的内存管理则是在有限的内存资源分配的情况下,通过明确管理对象的生命周期和合理的对象释放来管理应用程序的内存。
在OC中我们所要面对的是一群对象的内存管理。如果程序运行时一直给对象分配内存而不及时释放无用的内存,在这样的情况下程序占用的内存越来越大,直至内存消耗殚尽,从而导致内存泄漏应用程序崩溃。
Objective-C提供了两种应用程序内存管理方法。
(1)MRR:手动保留释放(Manual Retain-release),通过跟踪拥有的对象来显式管理内存。在系统运行时通过实现引用计数(reference counting)为环境和对象提供了这样的内存管理方法。
(2)ARC:自动引用计数(Automatic Reference Counting)与MRR一样都实现了引用计数的运用,但系统在编译时会自动插入相应的内存管理方法调用。


Memory Management

2.内存管理策略

内存管理基本规则

内存管理模型是依据对象持有建立的。一个对象拥有一个或多个持有者,只要一个对象存在持有者,那么这个对象就会存在,反之它将会被销毁。我们可以通过alloc、 new、copy和mutableCopy这些方法创建一个对象。当一个对象被有效地创建并返回给调用者时,有两种情况需要使用retain:
(1)在实现方法或初始化时,将要存储的对象的所有权作为属性值;
(2)防止对象作为某些其他操作的副作用而被无效。
最后我们通过发送释放消息或自动释放消息来放弃对象的所有权,以此来释放对象。
我们通过一个例子来解释这个基本规则:

{
    Person *aPerson = [[Person alloc] init];
    // do something...
    NSString *name = aPerson.fullName;
    // do something...
    [aPerson release];
}

在这个例子中,Person类对象 aPerson通过alloc被创建,在不再使用时又被release方法被释放掉。因为aPerson的fullName属性没有被任何持有方法来存储,所以name不需要被释放。

释放对象

NSObject类定义了一个dealloc方法,当一个对象没有所有者并且它的内存被回收的时候,这个方法被自动地调用。 dealloc方法的作用是释放对象自己的内存,并处理其拥有的任何资源,包括任何对象实例变量的所有权。

@interface Person : NSObject
@property (retain) NSString *firstName;
@property (retain) NSString *lastName;
@property (assign, readonly) NSString *fullName;
@end
 
@implementation Person
// ...
- (void)dealloc
    [_firstName release];
    [_lastName release];
    [super dealloc];
}
@end

在这个代码例子中,当Person类对象被释放时,其属性和实例变量在dealloc方法中被释放处理。

3.内存管理实际应用

我们可以采取一些可行的步骤来更容易地管理内存,帮助我们确保程序保持的可靠和健壮性。

使用方法访问

当我们的类中存在一个作为属性的对象,我们必须确保在使用这个对象的时候没有被释放。因此在设置对象时需要声明对象的所有权,并且确保放弃任何现有价值的所有权。我们可以使用方法访问来减少在对实例变量进行retain和release时产生的内存管理问题。
首先我们定义一个类的属性并声明它的所有权,如下:

@interface Object : NSObject
@property (nonatomic, retain) NSNumber *count;
@end;

在属性声明之后系统便自动生成两个访问方法即“set”和“get”方法。在“get”方法中我们无须对其进行retain或release,仅需返回这个属性的实例变量即可。

- (NSNumber *)count {
    return _count;
}

然而在“set”方法中我们需要假定这个新对象的计数可能会被释放,所以我们需要retain来取得新对象的所有权。

- (void)setCount:(NSNumber *)newCount {
    [newCount retain];//retain newCount获得所有权
    [_count release];
    _count = newCount;
}

如果我们想重新设置这个属性对象,希望通过创建另外一个NSNumber对象来替换,那么我们需要通过释放来平衡对新对象的内存计数。

- (void)resetCount {
    NSNumber *zero = [[NSNumber alloc] initWithInteger:0];
    [self setCount:zero];
    //因为在setCount中已经对zero进行了retain
    //那么我们需要对zero进行release,保证其内存计数为1。
    [zero release];
}

还有另外一种就是在使用构造函数来实例化用于替换的新对象,我们就不用对其进行release。

- (void)resetCount {
    NSNumber *zero = [NSNumber numberWithInteger:0];
    [self setCount:zero];
}

注意:在Cocoa中一些创建指定对象的方法是通过引用返回的。当我们调用这些方法时,并不是创建该对象,所以我们没有它的所有权。最明显的例子便是NSError。

特别需要注意的是,当我们使用KVO时,像这样替换对象从而改变变量的值是不正确的。
那么我们有什么地方在设置实例化变量的时候不需要使用方法访问呢?答案就是在初始化和dealloc时。
我们在Object初始化的时候,直接实例化了一个新的NSNumber对象,并将它作为_count。

- init {
    self = [super init];
    if (self) {
        _count = [[NSNumber alloc] initWithInteger:0];
    }
    return self;
}

最后,像Object类这样存在着count这样的实例化对象变量,我们需要实现dealloc方法。在dealloc中我们通过对count进行release以放弃对其的所有权。

- (void)dealloc {
    [_count release];
    [super dealloc];
}

弱引用避免循环引用

当我们对一个对象进行retain时便会产生对它的强引用。一个对象只有在它所有的强引用被释放时才会被释放。最常见的就是在两个对象互相引用时引发的循环引用。
下面我们就用一个例子来说明这样的问题:
Document 存在一个Page对象,而所有的Page对象拥有一个属性追踪它所属的Document对象。如果Document对Page对象进行了强引用并且Page也如法炮制对Document进行了强引用,这两个对象都不会被释放。因为直到Page对象释放之前Document的引用计数不能为零,而Page对象在Document对象dealloca之前也不会被释放。
这种情况如图所示:


RetainCycles

为了避免这种问题发生,我们需要使用弱引用。

A weak reference is a non-owning relationship where the source object does not retain the object to which it has a reference.
弱引用是一个非拥有关系,源对象不保留它所引用的对象。

然而为了保证界面和图像的完整性,必须有强引用的存在。
在Cocoa中存在着这样一个惯例:一个父对象应该对其子对象保持强引用,而子对象对其父对象是弱引用。
所以在上图中,Document对象对Page对象是强引用,而Page对象对Document对象应该是弱引用关系。

Cocoa中弱引用使用的例子比比皆是,不仅仅是存在于表格数据源、视图,通知观察者以及代理委托。

我们需要谨慎想弱引用对象发送消息。如果我们对一个已经被delloac的对象发送消息,那么程序将会崩溃。我们必须确保这个对象任然存在有效。弱引用的对象通常知道其他对象对它的弱引用,就像循环引用一样,负责在释放对象时通知另一个对象。例如,当我们向通知中心注册一个对象为观察者时,通知中心将存储对该对象的弱引用,并在发布适当的通知时向其发送消息。当对象被释放时,我们需要将其注销到通知中心,以防止通知中心将任何进一步的消息发送到不再存在的对象。同样,当一个委托对象被释放时,我们需要通过向另一个对象发送一个带有nil参数的setDelegate:消息来移除委托。这些通常是在对象dealloc方法中进行。

避免取消对象的分配

Cocoa所有权政策定义接收的对象通常应该在调用方法的方法体内保持有效,在从当前方法体返回一个接收的对象也不会被释放。我们需要注意的不是一个对象的getter方法中返回的缓存的实例变量或计算值,而是使用这些变量或计算值时是否有效。
但在方法体内也有例外的情况:
(1)当一个对象从基本集合类中被移除

someObject = [array objectAtIndex:n];
[array removeObjectAtIndex:n];
// someObject 已经失效了

(2)当父对象被dealloc

someObject = [parent child] ;
[parent release]; 
// someObject 已经失效了

我们从另一个对象中检索对象,然后直接或间接释放父对象。如果释放父项导致它被解除分配,并且父项是该子项的唯一所有者,那么将同时释放子项(假设它是在父类dealloc中release而不是自动释放)。
为了防止在方法体内发生如下例外情况,我们需要对someObject进行retain。

someObject = [[array objectAtIndex:n] retain];
[array removeObjectAtIndex:n];
// someObject 继续存在并处理其他事宜
[heisenObject release];

稀缺资源的管理

我们通常不应该在dealloc方法中管理稀缺资源,例如文件描述符,网络连接以及缓冲区或缓存。特别是,我们不应该设计当我们认为dealloc会被调用就会被调用的类。因为dealloc方法的调用可能由于错误或因为应用程序销毁而被延迟或回避。
相反,如果我们有一个实例管理稀缺资源的类,我们应该设计应用程序,在我们知道什么时候不再需要这些资源时告诉实例去进行“清理”。我们通常会释放这个实例,然后会释放dealloc,但不会引发其他的问题。

它们包含的对象的集合

当我们添加一个对象到一个集合(数组、字典或set)里时,这个集合就会获取到这个对象的所有权。当对象从集合中被删除或集合本身被释放时,集合将放弃该对象的所有权。所以我们可以这样:

NSMutableArray *array = [NSMutableArray array];
NSUInteger i;
// ...
for (i = 0; i < 10; i++) {
    NSNumber *convenienceNumber = [NSNumber numberWithInteger:i];
    [array addObject:convenienceNumber];
}

在这段代码中,我们没有使用alloc分配内存,所以也无需调用release。但我们把这段代码改成下面这样,就需要使用release了。

NSMutableArray *array = [NSMutableArray array];
NSUInteger i;
// ...
for (i = 0; i < 10; i++) {
    NSNumber *allocedNumber = [[NSNumber alloc] initWithInteger:i];
    [array addObject:allocedNumber];
    [allocedNumber release];
}

4.自动释放池块

自动释放池块提供了一种机制,我们可以放弃对象的所有权,但也不用担心它立即被释放掉的可能。通常情况下,我们不需要创建自己的自动释放池。

关于自动释放池块

一个自动释放池快通过@autoreleasepool被标记。

@autoreleasepool {
}

在自动释放池块的末尾,在块内收到的自动释放消息的对象同样也收到了释放的消息。
自动释放池块也可以嵌套

@autoreleasepool {
    // do something...
    @autoreleasepool {
        // do something...
    }
}

对于一个自动释放的消息,其对应的释放消息在已发送了自动释放消息的自动释放池块末尾被发送。(>_<)好绕口!

使用本地自动释放池块来减少峰值内存占用

许多程序都会创建会被自动释放的临时对象。这些对象将添加到程序的内存占用空间直到块的结束。在许多情况下,允许临时对象累积到当前事件循环迭代结束是不会导致过多的开销的;但是,在某些情况下,我们可能会创建大量占用相当多内存占用空间的临时对象,并且希望更快地进行处理。这种情况下,我们可以创建我们自己的自动释放池块。在块的末尾,通常为了减少程序的内存占用而使临时对象被释放。
以下这个例子将会给我们展示如何在For循环中使用本地自动释放池块:

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池块之外的临时对象,则可以通过向块中的对象retain,然后在块之后将其自动释放。

– (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,从而延长了其避免在块末尾被释放,以便能在块之外继续调用,并返回给这个方法调用者。

参考资料

Advanced Memory Management Programming Guide

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

推荐阅读更多精彩内容