第05章 基于引用计数的内存管理 上
自动引用计数(ARC,Automatic Reference Counting)是苹果公司推荐使用的内存管理方法。启动ARC后,编译器会在适当的地方自动加入retain、release、autorelease等语句,来简化OC编程在内存管理方面的工作量。
5.1 动态内存管理
5.1.1 内存管理的必要性
C语言中需要手动利用malloc()和free()对内存进行管理。当程序运行结束时,操作系统会释放为其分配的内存。对于长时间运行的程序,程序员必须释放不再使用的内存,否则程序就会崩溃。
程序未能释放已经不再使用的内存叫做内存泄漏(memory leak)。
在指针所指向的对象已经被释放或回收的情况下,该指针就称为悬垂指针(dangling pointer(dangle |ˈdæŋgl| v 悬荡))或也指针。继续使用这种指针会造成程序崩溃。
5.1.2 应用计数哦、自动引用计数和自动垃圾回收
Cocoa环境的OC提供了一种动态的内存管理方式,称为引用计数(reference counter)。这种方式会跟踪每个对象被引用的次数,当对象的引用次数为0时,系统就会释放这个对象所占用的内存。本书把这种内存管理方式成为基于引用计数的内存管理。
比起引用计数内存管理更高级一点的就是自动引用计数(Automatic Reference Counting,简写为ARC)的内存管理,相对的手动引用计数内存管理称为MRC(Manual Reference Counting(manual |ˈmænjʊəl| adj 体力的 ))。
5.2 手动引用计数内存管理
5.2.1 引用计数
每收到一个release消息,对象的引用计数值就会减一。当对象的引用计数值到达0的时候,系统就知道这个对象不再需要了。这时,OC会自动向对象发送一条dealloc消息来释放内存。通常不允许直接调用dealloc方法。
生成对象或通过给对象放松retain消息来保持对象这种状态,都可以说是拥有这个对象的所有权(ownership(|ˈəʊnəʃɪp| n 所有权))。拥有实例所有权的对象叫做所有者(owner)。
通用引用计数能够表现出一个对象有几个所有者。只要某个对象的引用计数大于0就表示这个对象有所有者。引用计数为0的时候,就说明这个对象没有所有者,就会被释放。
5.2.2 测试引用计数的例子
MRCTest *test = [MRCTest alloc];
NSLog(@"%u",[test retainCount]);
//2016-06-02 14:54:36.497 Test[793:28060] 1 //可以看出alloc后引用计数已经为1;
test = [test init];
NSLog(@"%u",[test retainCount]);
//2016-06-02 14:54:38.454 Test[793:28060] 1
[test retain];
NSLog(@"%u",[test retainCount]);
//2016-06-02 14:56:25.318 Test[833:29659] 2
[test release];
NSLog(@"%u",[test retainCount]);
//2016-06-02 14:56:27.067 Test[833:29659] 1
5.2.3 释放对象的方法
释放一个类的实例对象时,为了彻底释放该实例对象所保持的所有对象的所有权,需要为该类重写dealloc方法,在其中释放已经分配的资源,放弃实例变量的所有权。因为最终释放内存的是dealloc方法,所以不能重写release方法,如下例所示。
- (void) dealloc // 重写的是dealloc方法而不是release方法
{
/*
这类通过release方法放弃子类中所有实例变量的所有权。其他用于释放前的善后操作也能写在这里。
*/
[super dealloc];
}
销毁对象的时候,不允许直接用dealloc,而是使用release。release会让引用计数减1,只有当引用计数等于0时,系统才会调用dealloc真正销毁这个对象。
5.2.4 访问方法和对象所有权
在通过访问方法等改变拥有实例变量所有权的对象时,必须注意实例变量引用计数的变化,合理安排release和retain的先后顺序。
如下setMyValue这个方法。
- (void)setMyValue:(id)obj
{
[myValue release];
myValue = [obj retain];
}
绝大多数情况下这个方法是没有问题的,只有在参数obj和myValue是同一个对象的时候才会出现错误。因为最先进行的release操作可能导致对象被释放。
安全的setter方法的写法:如下
- (void)setMyValue:(id)obj
{
[obj retain];
[myValue release];
myValue = obj;
}
或则
- (void)setMyValue:(id)obj
{
if(myValue != obj)
{
[myValue release];
myValue = [obj retain];
}
}
如果不考虑对象所有权,只是单纯的赋值,则不需要保持和释放。不考虑所有权的setter方法如下所示。
-(void)setMyValue:(id)obj
{
myValue = obj;
}
专栏
C语言中可以定义一个整数或结构题,允许编译器在栈上为变量分配内存。
那么,Objective-C的变量能否不通过alloc来动态分配内存,而在栈上分配内存呢?
#import "MRCTest.h"
static NSArray testArry;//Interface type cannot be statically allocted.
@implementation MRCTest
@end
实践证明是不允许的。
5.2.5 自动释放
在实际编程中,会遇到很多只是用了一次就不再使用的对象。如果这种对象也要逐个释放, 就会很麻烦。Cocoa环境的Objective-C提供了一种对象自动释放(autorelease)的机制。这种机制的基本思想是把所有需要发送release消息的对象都记录下来,等到需要释放这些对象时,会给这些对象一起发送release消息。其中NSAutoreleasePool(自动释放池)就起到了记录的作用。
本书中把自动释放池中登录的实际上相当于放弃了所有权的对象称为临时对象。
自动释放池的典型用法如下。
id pool = [[NSAutoreleasePool alloc] init];
/*
在此进行一些操作。
给临时对象发送autorelease消息。
*/
[pool release];/*销毁自动释放池,自动释放池中所有对象引用计数减1*/
/*
在一个garbage collected环境里,release不做任何操作。 NSAutoreleasePool因此提供了一个 drain 方法,它在reference-counted环境中的行为和调用release一样, 但是在一个garbage collected环境中则触发
garbage collection动作 (if the memory allocated since the last collection is greater than the current threshold)。 因此通常你应该使用drain而不是release去释放一个autorelease pool。
*/
5.2.6 使用自动释放池时需要注意的地方
autorelease 虽然是NSObject的方法,但必须和NSAutoreleasePool一起使用。
可以给实例对象多次发送retain消息,相应的也可以给实例对象发送多次autorelease消息。这种情况就相当于同一个对象被重复登录到自动释放池中,只要autorelease和retain被成对发即可。
5.2.7 临时对象的生成
Objective-C中提供了很多类似于+(id) stringWithUTF8String:(const char*)bytes;的方法,这种类方法的命名规则是,不以init开头,而以要生成的对象的类型作为开头。
这种生成临时对象的类方法,在Objective-C中称为便利构造器(convenience constructor)或便利构造器。
convenience |kənˈviːnɪəns| noun 便利
5.2.8 运行回路和自动释放池
典型的图形界面应用程序(GUI程序)往往会在休眠中等待用户操作。在用户发出动作之前,程序将一直处于空闲状态。
当点击事件发生后,程序会被唤醒并开始工作,执行某些必要的操作以相应这一事件。处理完这一事件后,程序又会返回到休眠状态,并等待下一个事件的到来。这个过程叫作运行回路(runloop)。
Cocoa在程序开始事件处理之前会隐式创建一个自动释放池,并在事件处理结束后销毁该自动释放池。所以,程序员在进行Cocoa的GUI编程时,就算不手动创建自动释放池,也可以使用临时对象。
5.2.9 常量对象
有些时候,我们可能会需要某个类生成一个实例,程序中访问到这个类的对象时使用的都是同一个实例对象。在设计模式中这种情况叫做单例模式(singleton)。这些类通常以shared开头的方法返回唯一的实例对象。
singleton |ˈsɪŋgltən| noun 单个物件