内存管理2(主讲MRR)
内存管理2
我们讨论过properties 后,所有的内存管理系统都是通过控制所有对象的生命周期来减少内存的占用。iOS和OS X应用程序完成这些是通过对象拥有者来实现的,它保证了只要对象使用就会存在,但是不长。
这种对象拥有者的模式来自于引用计数系统,它会记录对象现在被多少对象拥有,当你生命一个对象的拥有者,你要增加它的计数,而当你不用这个对象的时候,你需要减少这个计数。只要它的引用计数大于0,对象就一定会存在。但是一旦计数为0,操作系统就会被允许释放它。
在过去,开发者通常通过调用一个被NSObject protocol定义的特殊的内存管理方法来控制对象的引用计数。这个方法叫做Manual Retain Release(MRR),也就是手动保持释放。然而,到了Xcode4.2之后介绍了Automatic Reference Countin(ARC),就是自动引用释放。ARC自动地插入了所有他们的方法。
如今的应用程序应该总是使用ARC,
因为它更加可靠而且使你专注于你的App特性而不是内存管理。
该文章主要解释引用计数概念里面的MRR,然后讨论一些特别需要关注的关于ARC的一些知识。
Manual Retain Release
在手动保持释放环境中,持有和放弃每个对象的所有权是我们的工作。你实现这些需要调用一些特殊的内存相关方法,下面就是用到的方法以及简短描述
方法 | 行为 |
---|---|
alloc | 创建一个对象并且声明它的所有权 |
retian | 声明一个存在对象的所有权 |
copy | 复制一个对象,然后声明它的所有权 |
release | 放弃一个对象的所有权,然后立刻销毁它 |
autorelease | 放弃对象的所有权,但是延迟销毁。(最终也是销毁) |
手动的控制一个对象的所有权貌似看起来是一件令人害怕的任务,但是其实它非常容易。你要做的就是声明任何你需要对象的所有权,当你不再使用的时候记得放弃所有权就行了。从实用的角度来看,意味着你必须平衡alloc,retain和copy的调用使用release或者autorelease再相同的对象上。
如果你忘记了release一个对象,那么它的潜在的内存将不会被释放,这样就会造成内存泄露。如果泄露严重就会导致程序崩溃。相反如果你调用release太多次数的话,会产生野指针(悬挂指针)。当你试图访问悬挂指针的时候,你就会请求一个无效的内存地址,这样你的程序很有可能崩溃。
下面来解释一样如何通过合理的使用上面提到的方法来避免内存泄露和悬挂指针。
允许MRR
在内存管理(1)中我们介绍过了,就不在介绍了。
alloc方法
我们已经知道使用alloc创建一个对象。但是,它不仅仅是给对象分配了内存,它也设置它的引用计数为1.这也合理,因为我们如果不想持有这个对象(持有一小会儿也算)那么我们就没有必要去创建对象。
#import <Foundation/Foundation.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSMutableArray *mutableArray = [[NSMutableArray alloc] init];
[mutableArray addObject:@"Scott"];
NSLog(@"%@",mutableArray);
}
return 0;
}
上述带么我们肯定很熟悉,就是实例化一个可变数组,添加一个value,然后打印它的内容。从内存管理的角度来看,我们现在有一个mutableArr对象,这就意味着后面我们必须在某个地方要release它。
但是,在代码中我们没有释放它,我们的代码现在就有一个内存泄露。我们可以通过Product-->Analyze测试出来。结果如下:
这就有一个问题,它可以在上图中看到(main.m)。
这是一个很小的对象,因此泄露不太致命。然而,如果他一次又一次地发生(例如在一个循环中或者用户一直点击一个按钮),那么这个程序就会最终用完内存,然后崩溃。
release方法
该方法通过放弃对象的所有权来减少引用计数。因此,我们可以解决内存泄露问题通过在NSLog()后面添加下面的代码:
[mutableArray release];
现在我们的alloc和release就平衡了,静态分析就不会报出任何错误。典型的,你将会想放弃对象的所有权在同样的方法之后。
过早释放会造成悬挂指针。例如,在NSLog()前面去调用release方法。因为release会立刻释放占用的内存,mutableArray变量在NSLog()里面就指向了一块不可用的内存,然后你的程序就会崩溃:EXC_BAD_ACCESS error code 当你想运行的时候。(最新的Xcode现在直接打印出来空,而没有提示错误。)
因此,不要在还在使用一个对象的时候而去释放它。
retain方法
我们现在新建一个对象CarStore。
CarStore.h
#import <Foundation/Foundation.h>
@interface CarStore : NSObject
- (NSMutableArray *)inventArr;
- (void)setInventArr:(NSMutableArray *)newInventArr;
@end
CarStore.m
#import "CarStore.h"
@implementation CarStore
{
NSMutableArray *_inventArr;
}
- (NSMutableArray *)inventArr {
return _inventArr;
}
- (void)setInventArr:(NSMutableArray *)newInventArr {
_inventArr = newInventArr;
}
@end
然后我们在main.m中进行如下操作:
#import <Foundation/Foundation.h>
#import "CarStore.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSMutableArray *mutableArray = [[NSMutableArray alloc] init];
[mutableArray addObject:@"Scott"];
CarStore *superStore = [[CarStore alloc] init];
[superStore setInventArr:mutableArray];
[mutableArray release];
NSLog(@"%@",[superStore inventArr]);
}
return 0;
}
此时我们会发现里面是没有数据的。因为此时inventArr就是一个悬挂指针,因为对象已经被released了在main.m中。现在,superstore对象有个弱引用array.为了实现强引用,CarStore需要声明数组所有权:
- (void)setInventArr:(NSMutableArray *)newInventArr {
_inventArr = [newInventArr retain];
}
这样就确保了inventArr没有被释放当superstore正在使用他的时候。可以看一下:retain方法返回是对象的本身,这就是我们执行retain和指派任务在一行。
然而这又造成了另一个问题:retain调用没有平衡release,因此会产生另一个内存泄露。当我们传递过数组之后,我们不能访问老得数值,这就意味着我们将不会使用它,为了解决这个问题,我们需要调用release去释放老值: //不太理解,依然会报内存泄露
- (void)setInventArr:(NSMutableArray *)newInventArr {
if (_inventArr==newInventArr) {
return;
}
NSMutableArray *oldValue = _inventArr;
_inventArr = [newInventArr retain];
[oldValue release];
}
这就是tetain和strong属性做的事情,使用property将会更方便。
由此可知alloc和retain必须和release平衡,确保内存最终被释放。
copy方法
另一种保留是复制的方法,它创建了一个新实列对象和增加了引用计数,保留最初的影响。因此,如果你想复制mutalbeArr,而不是指向可变的,你可以修改setInventory方法如下:
- (void)setInventArr:(NSMutableArray *)newInventArr {
if (_inventArr==newInventArr) {
return;
}
NSMutableArray *oldValue = _inventArr;
_inventArr = [newInventArr copy];
[oldValue release];
}
一些类支持多重复制方法(例如多重init方法)。任何copy的对象具有相同的行为。
autorelease方法
就像release一样,autorelease方法放弃对象的所有权,但是不是立刻销毁对象,它延迟释放内存。这样允许你当你应该释放对象的时候释放。
例如,考虑一下一个简单的工厂方法:
+ (CarStore *)carStore;
从技术上讲,是carStore方法对对象的释放负责,因为调用者不知道该方法拥有返回对象。因此,它应该实现返回一个自动释放的对象,就像下边:
+ (CarStore *)carStore {
CarStore *newStore = [[CarStore alloc] init];
return [newStore autorelease];
}
那么这个对象就放弃了所有权,延迟释放,直到在最近的@autoreleasepool{}块中,然后会调用正常的release方法。这就是为什么main()函数被@autoreleasepool包围着:
它确保当程序结束运行的时候,自动释放对象被销毁。
在ARC之前,它是一个很方便的方式,因为它让你创建对象但是不用担心在后面什么时候哪里去调用release。
如果你改变superstore的创建方式,使用下边的:
// CarStore *superStore = [[CarStore alloc] init];
CarStore *superStore = [CarStore carStore];
实际上,你不允许释放这个superstore实例了现在,因为你不再拥有它--而是carStore工程方法拥有它。一定不要去release一个autorelease对象,否则你会产生悬挂指针,甚至使程序崩溃。
dealloc方法
该方法和init方法相同。它被合理的使用在对象销毁之前。给你个机会来清理任何内部的对象们。这个方法通过runtime自动调用------
你不应该去自己调用dealloc。
在MRR环境中,最常见的你需要做的就是使用dealloc方法去释放一个实例变量存储的对象。想想我们刚才的CarStore当一个实例dealloc时发生了什么:它的_inventArr(被setter保持着的)从来没有机会被释放。这是另一种形式的内存泄露,为了解决这些,我们要做的就是添加一个dealloc方法到CarStore.m中:
- (void)dealloc {
[_inventArr release];
[super dealloc];
}
你必须要调用super dealloc去保证父类中的实例变量在合适的时间去释放。为了让dealloc简单,我们不应该在dealloc里面去进行逻辑处理。
MRR总结:
总之,关键是处理好alloc,retain和copay 和release活着autorelease之间的平衡,否则你就会遇到悬挂指针活着内存泄露在你的程序中。
在真实地路上,上面好多代码都是废弃的。但是通过上面可以让你更好的理解ARC编译原理。
Automatic Reference Counting(自动引用计数,ARC)
现在,你已经了解了MMR,现在你可以忘记他们了。ARC和MRR的工作方式一样,但是它自动为你插入合适的内存管理方法。这对于OC开发者是很好地处理。因为我们可以把精力放在应用程序需要做什么而不是如何做。
ARC中人为错误的内存管理几乎不存在,所以使用他的唯一理由可能就是使用过去的第三方代码库。(然而,ARC大部分情况下向后兼容MRR程序)。下面就是介绍MRR和ARC之间的切换。
允许ARC
Project--->Build Settings--->搜索garbage,找到Objective-C Automatic Reference Counting设置为YES即可。
没有更多的内存方法
ARC通过分析代码每个对象的理想的生存时间应该是多来工作,然后自动的插入必要的retain和release方法。该方式需要完全控制整个程序中对象的所有权,
这就意味着你不允许调用retain,release或者autorelease
唯一你可能见到的在ARC中的内存相关方法就是alloc和copy。
新的属性
ARC介绍了新的@property属性,你应该使用strong来代替retain,使用weak来代替assign。这些已经在properties 讨论过了。
ARC中的dealloc方法
dealloc在ARC中有一点不同,你不要release实例变量在dealloc方法中---ARC已经为你实现了。另外,父类的dealloc是自动调用,你也不需要[super dealloc]
。
但是有一种例外,如果你使用的低层次的内存构造函数,就像malloc(),那样的话,你仍然需要调用free在dealloc中去避免内存泄露。
总结
ARC中你唯一要关系的就是循环引用。
你应该明白了所有你应该知道的关于OC的内存管理。