什么是内存管理?
内存管理是程序设计中常见的资源管理(resource management)的一部分。每个计算机系统的可供程序使用的资源都是有限的,包括打开文件、网络连接、图片处理等。以图书馆为例,如果每个人都只借不还,那么图书馆最终因将无书可借而倒闭,其他人也无法再使用图书馆。内存管理,即在程序需要的时候分配内存,程序运行结束时释放占用内存。如果只分配不释放就会发生内存泄漏(leak memory):程序的内存占用不断增加,最终耗尽并导致程序崩溃。同时也要注意,不要使用刚释放的内存,避免误读陈旧数据引发的各种错误。在Cocoa框架中,通过引用计数的方式实现内存管理。
什么是引用计数?
Cocoa采用一种叫做引用计数(reference counting)的技术管理内存。每个对象都有一个与之相关联的整数,被称作它的引用计数器。当某段代码需要访问一个对象时,该代码就将该对象的引用计数器值加1,表示“我要访问该对象”。当这段代码结束对象访问时,将对象的引用计数器值减1,表示“我不再访问该对象”。当该对象的引用计数器值为0时,表示“不再有代码访问该对象”。因此,它将被销毁,其占用的内存被系统收回以便重用。
如何使用 Objective-C 进行内存管理?
当使用alloc
、new
方法或者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
后编译器会帮助你插入retain
和release
语句。也就是说,ARC
是在编译时进行工作的。
ARC使用条件:
- 能够确定哪些对象需要内存管理
- 能够表明如何管理对象
- 有可行的办法传递对象所有权
桥接转换
日常开发中99%的内存管理工作都交由编译器了,即ARC
。我曾调侃内存管理的使用做多的场景是——面试,这既是玩笑话也是实话。那么还有1%的情况,即如何对非OC对象进行内存管理,这就用到了桥接装换
(bridge cast)的C语言技术。
总结
Cocoa的内存管理规则:
如果使用new、alloc、或copy获得了一个对象,则该对象的引用计数器值为1;
如果通过其他方法获得一个对象,则假设该对象的引用计数器值为1,而且已经被设置为自动释放;
如果保留了某对象,则必须保持retain方法和release方法的使用次数相等。
引用:《Objective-C 基础教程》