第一次翻译文档,翻译文档来自:《Advanced Memory Management Proramming Guide——Practical Memory Management 》,英文能力一般,也是初学者,可能译的不好,只是在这记录下自己的学习过程,有不对的地方还请指正!
虽然在内存管理政策章节里阐述的基础概念很明确,但这里还有一些实际的方法可以让你更容易管理内存,可以帮助你确保你的程序保持可靠性和鲁棒性,同时可以最小化资源需求。
使用存取器方法是内存管理更容易
如果你的类的属性是对象,你必须确保任意设置为属性值的对象 在使用时不被释放,所以,当你用对象来设置属性时,你一定要申请该对象的所有权,你也必须确保以后放弃目前拥有的的对象所有权。
有时这些东西看起来比较乏味和迂腐,但如果你坚持使用存储方法,可能有问题的内存管理将大幅度减少,如果你通篇地对实例变量使用retain和release,你几乎一定做错了。
考虑一个你想要设定他计数的计数对象
@interface Counter : NSObject |
@property (nonatomic, retain) NSNumber *count; |
@end; |
这个属性声明了两个存储方法。你应该让编译器合成这两个方法,然而,这是指导去看他们如何被实现。在“get”存取器里,你仅仅返回合成的实例变量,所以这里无需retain或release。
- (NSNumber *)count { |
return _count; |
} |
在“set”存取器方法里,如果所有人都遵守同一个规则,你必须假定一个可以在任意时间设定的新额计数器,所以你必须拥有对象的所有权——通过发送retain消息,以确保他不会被释放。你同样需要通过发送release消息放弃旧对象的所有权(在OC,给nil发送消息是允许的,所以即使count还没被赋值,这个实现依然可行),你必须在【newCount retain】之后发送此消息,以防止他们俩是同一个对象。你不想不经意间引起他被释放。
- (void)setCount:(NSNumber *)newCount { |
[newCount retain]; |
[_count release]; |
// Make the new assignment. |
_count = newCount; |
} |
使用存取器方法设置属性值
假如你想实现一个重新设置计数器的方法,你有两种选择,第一种实现是用alloc去创建一个NSNumber实例,所以你要用release去平衡实例的内存计数。
- (void)reset { |
NSNumber *zero = [[NSNumber alloc] initWithInteger:0]; |
[self setCount:zero]; |
[zero release]; |
} |
第二种实现是使用一个方便的构造函数去创建一个新的NSNumber对象,这里无需使用发送etain or release消息。
- (void)reset { |
NSNumber *zero = [NSNumber numberWithInteger:0]; |
[self setCount:zero]; |
} |
注意上述两种方法都使用set存取器方法。
对于简单的案例,接下来的实现将几乎一定可以正确运行,但会绕开存取器方法,在某些阶段,这样做将几乎一定导致错误(例如,当你忘记retain 或release时,或如果实例变量的内存管理的语意发生改变时)
- (void)reset { |
NSNumber *zero = [[NSNumber alloc] initWithInteger:0]; |
[_count release]; |
_count = zero; |
} |
同样注意,如果你正在使用KVO,那么用这种方法改变变量并不兼容KVO。
不要在初始化方法和析构方法里使用存取器方法
唯一不能使用存取器方法去设置实例变量的地方是初始化方法和析构方法。用一个代表数值0的对象实例化一个计数对象,你应该如下实现一个init方法。
- init { |
self = [super init]; |
if (self) { |
_count = [[NSNumber alloc] initWithInteger:0]; |
} |
return self; |
} |
为了使一个connter可以用一个非零的计数去初始化,你应该如下实现一个initWithCount: 方法
- initWithCount:(NSNumber *)startingCount { |
self = [super init]; |
if (self) { |
_count = [startingCount copy]; |
} |
return self; |
} |
由于这个计数器类有一个对象的实例变量,你也一定要实现dealloc方法。它应该通过给他们发送release消息放弃任意实例变量的所有权,最终还应该调用父类的dealloc。
- (void)dealloc { |
[_count release]; |
[super dealloc]; |
} |
使用弱引用去避免保持环(Retain Cycles)
保持一个对象就创建了一个强引用。一个对象只有在它所有的强引用被释放时才能被销毁。一个被熟知的问题——保持环(retain cycle),如果两个对象有循环性引用,就是,他们各自保持着对 对方的强引用(无论是直接的,还是通过一个强引用链),这个问题就因此产生。
图一展示的对象关系解析一个潜在的retain cycle。Document对象有一个Page对象,Document每一页都是一个Page对象,每一个Page对象都有一个属性去追踪它位于哪个document,如果Document 对象有一个Page对象的强引用,而且Page对象也有一个Document对象的强引用,两者都不会被销毁。Document对象的引用计数直到Page对象释放了才为0,Page对象直到Document对象销毁了才能被释放。
解决retain cycle的办法是使用weak reference。weak reference是一个非拥有关系,源对象不保持它所引用的对象。
为了保持完整的对象图,但是,必须要有强引用的地方(如果只有weak reference,那么pages和paragraphs可能不会有任何拥有者,所以他们将会被销毁)。Cocoa建立了一个约定,父对象应该保持对子对象的强引用,而子对象应该保持对父对象的weak reference。
所以,在图一中,document对象有一个page对象的强引用,而page对象有document对象的weak reference。
在Cocoa弱引用的例子包括,但不限于,表数据源,outline view items,notification observers ,miscellaneous targets and delegates.
你给一个 你只有它的weak referen 的对象发送消息时需要注意。如果你在一个对象销毁后给它发送消息,你的程序会崩溃。你必须有明确的条件,什么时候对象是有效的(You must have well-defined conditions for when the object is valid)。weak reference对象知道其他对象weak reference它,就像循环引用的情况,当weak reference对象销毁时它会通知其他对象(自己的理解:对该weak reference对象保持weak reference的对象)。例如,当你在notification center里注册一个对象时,notification center会保持该对象的weak reference,当合适的的通知被posted时,还会给对象发送消息。当对象销毁时,你需要取消注册,以防notification center继续给该对象发送消息。同样的,当一个delegate对象被销毁时,你需要通过给其他对象发送带nil参数的setDelegate:
消息删除这个delegate link,这些消息通常在对象的dealloc方法里发送。
避免释放你正在使用的对象
Cocoa的拥有权政策指定接收对象应该在调用函数的整个作用域内保持有效,它也应该可以从当前作用域返回一个接收对象而不用担心它会被释放。一个对象的getter方法返回一个缓存实例变量还是一个计算值跟你的程序无关,重要的是在你需要它的时候对象保持有效。
这条规则也有个别例外,主要落入两个类别之一
1. 当一个对象从一个基础集合类删除时
heisenObject = [array objectAtIndex:n]; |
[array removeObjectAtIndex:n]; |
// heisenObject could now be invalid. |
当一个对象从一个集合类中删除时,他会发送release(而不是autorelease)消息,当集合是这个删除对象的唯一拥有者时,这个删除的对象(例子中的heisenObject)就会马上被销毁
2. 当一个父对象被销毁时
-
- id parent = <#create a parent object#>;
// ...
heisenObject = [parent child] ;
[parent release]; // Or, for example: self.parent = nil;
// heisenObject could now be invalid.
在一些情形你从另外对象得到对象,然后直接或者间接release父对象。如果release父对象引起它销毁,而且父对象是子对象的唯一所有者,那么子对象(例子中的heisenObject)将会在同一时间被销毁(假设在父对象的dealloc方法里给它发送的是release而不是autorelease消息)
为了防止这些情况,你在接收时保持heisenObject,当你使用完时,release它。例如:
heisenObject = [[array objectAtIndex:n] retain];
[array removeObjectAtIndex:n];
// Use heisenObject...
[heisenObject release];
不要使用dealloc去管理稀缺资源
你不应该在dealloc方法里管理诸如文件描述符,网络连接,buffers或缓存这些稀缺资源。特别的,你不应该设计类以便当你认为dealloc将会被调用时它就将会被调用(you should not design classes so that
dealloc
will be invoked when you think it will be invoked),dealloc的调用可能会被延迟或者被回避,因为bug或者程序的拆毁(tear-down)相反,如果你有一个管理稀缺资源的类,你应该设计你的程序,你知道当不再需要这些资源时,你可以告诉这个实例去清理,然后你通常会release这个实例,接着会调用dealloc,但如果不是这样,你也不会遭遇更多的问题。(You would typically then release the instance, and
dealloc
would follow, but you will not suffer additional problems if it does not.)如果你在dealloc上尝试背驮式资源管理,问题可能会出现。例如:
1. 对象图的顺序依赖撕裂
2. 对象图的撕裂机制本质上是非顺序的,即使你通常可能期望和得到一个特殊的顺序,这时你正在引入脆弱性,如果一个对象被意外的autorelease,而不是release,例如,
撕裂的顺序会改变,这可能会导致意外的结果。
3. 稀缺资源的不可再生
4. 内存泄露的bug应该被修复,但他们通常不是致命的错误,当你希望稀缺资源被release而实际上没有时,你将陷入更大的困境。如果你的程序耗尽文件描述符时,用户可能
会无法保存数据。
5. 清理逻辑在错误的线程上执行
6. 如果一个对象在一个意料不到的时间上autorelease,它将会被销毁,无论它在哪个autorelease pool,在只能在一个线程接触的资源上 很容易致命的(If an object is autoreleased at an unexpected time, it will be deallocated on whatever thread’s autorelease pool block it happens to be in. This can easily be fatal for resources that should only be touched from one thread.)
集合类拥有它所包含的对象
当你将一个对象加入到集合(如array,dictionary,set),集合会拥有对象的所有权,当一个对象从集合里移走时,或当集合本身release时,集合会放弃对象的所有权,所以,例如,如果你想创建一个数值数组,你可以按照以下方式去做:
NSMutableArray *array = <#Get a mutable array#>;
NSUInteger i;
// ...
for (i = 0; i < 10; i++) {
NSNumber *convenienceNumber = [NSNumber numberWithInteger:i];
[array addObject:convenienceNumber];
}
这种情况,你没有调用alloc,所以你无需调用release,无需保持新的数值(convenienceNumber),因为数组会自动保持。
NSMutableArray *array = <#Get a mutable array#>;
NSUInteger i;
// ...
for (i = 0; i < 10; i++) {
NSNumber *allocedNumber = [[NSNumber alloc] initWithInteger:i];
[array addObject:allocedNumber];
[allocedNumber release];
}
在这种情况,在for 循环里,无需给已经分配内存的数值对象发送release消息,以平衡其引用计数,因为在其加入数组时,数组会自动retain数值对象。当它在数组里时,它将不会被销毁
为了理解这点,把你自己当做是实现集合类的人,你想确保你查找的任何对象都不会消息,所以在他们传进来时,你给他们发送retain消息,如果他们被删除了,你给他们发送release消息以平衡引用技术,在你本身(自加注解:集合)的dealloc方法里应该给剩下的其他对象发送release消息。
所有权政策是使用引用计数实现的
所有权政策是通过引用计数实现的,在调用retain方法后,通常叫“retain count“,每个对象都有一个retain count。
当你创建一个对象,他计数为1
当你给对象发送retain消息时,他的计数增加1
当你给对象发送release消息时,他的计数减1
当你给对象发送autorelease消息时,在他当前的autorelease pool block结束时它的计数会减1
如果对象的计数为0,他会被销毁
重点:显式请求对象的计数是不合理的(参考 retainCount),结果通常是误导人的,因为你不知道哪个框架对象保留了你感兴趣的对象,在调试内存管理的问题上,你应该关注和确保你的代码遵循ownership rules。