简书内容都是个人的知识点整理和笔记。
1.Objective-C中的内存管理
应用程序在运行期间通过内存管理完成内存的分配、使用和释放。而在Objective-C中的内存管理则是在有限的内存资源分配的情况下,通过明确管理对象的生命周期和合理的对象释放来管理应用程序的内存。
在OC中我们所要面对的是一群对象的内存管理。如果程序运行时一直给对象分配内存而不及时释放无用的内存,在这样的情况下程序占用的内存越来越大,直至内存消耗殚尽,从而导致内存泄漏应用程序崩溃。
Objective-C提供了两种应用程序内存管理方法。
(1)MRR:手动保留释放(Manual Retain-release),通过跟踪拥有的对象来显式管理内存。在系统运行时通过实现引用计数(reference counting)为环境和对象提供了这样的内存管理方法。
(2)ARC:自动引用计数(Automatic Reference Counting)与MRR一样都实现了引用计数的运用,但系统在编译时会自动插入相应的内存管理方法调用。
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之前也不会被释放。
这种情况如图所示:
为了避免这种问题发生,我们需要使用弱引用。
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,从而延长了其避免在块末尾被释放,以便能在块之外继续调用,并返回给这个方法调用者。