Objective-C中的内存管理机制

2,641 阅读10分钟

从苹果的官方文档来看,OC对应用程序的内存管理提供了2种方法。

第一种即“manual retain-release”(MRR),手动保留释放,也可理解为手动引用计数。

第二种,“Automatic Reference Counting”(ARC),自动引用计数。但是ARC并不等同垃圾回收。在苹果的官方文档有这样一句话,“You are strongly encouraged to use ARC for new projects.”意思是苹果强烈建议在项目中运用ARC机制来管理内存。

内存管理不当的话会出现以下2种问题:

1.过早释放:在某处程序用完某块内存之前,就将该内存还给了“堆”。(这里的堆指的是,ios启动应用时,会为应用保留一部分空闲的RAM,这部分空闲的RAM称为堆。应用程序可随意使用堆,不会影响ios的其他部分,也不会影响其他应用。)

2.内存泄露:不释放已经不使用的内存会导致内存泄露,即使他从来没有被再次使用过。内存泄露会导致你的应用程序的内存使用量日益增加,这反过来有可能会导致系统性能较差或申请内存被终止。

要说明的一点是不管是MRR中的“通过属性机制简化存取方法”(在“存”方法中涉及到了基本的内存管理),还是ARC。本质上都是苹果帮助程序员在开发时减少了代码量,把原来由程序员要完成的工作交给编译器去完成,从而减少软件开发的繁琐程度。

接下来就MRR和ARC进行详细的说明。

在OC中所有的类均继承于基类NSObject,那么所有的类就都有一个类方法alloc和一个实例方法dealloc。当通过向类发送alloc方法来创建类实例时,系统会从堆中分配出相应字节数的内存(注:指针类型的实例变量大小是4个字节,这是保存堆中的对象地址所需的内存空间)。例如:UIView *view = [[UIView alloc] init]; alloc会返回一个指针对象,指向新分配的内存。分配内存后,在类完成其“功能”后,还要将内存还给堆。但是不可以直接向对象发送dealloc方法,即这样写[view dealloc]是不对的,只能由对象自己向自己发送dealloc方法。对于dealloc,苹果官方文档是这么解释的:The NSObject class also defines a method, dealloc, that is invoked automatically when an object is deallocated(NSObject定义了一个方法dealloc,当对象被释放时自动调用)。那么对象何时释放?释放时是否安全呢?这个在OC中是通过引用计数来解决这个问题的。

一.引用计数算法

对象创建后,这个对象就有一个所有者。对象在其生命周期可以有不同的所有者,也可以同时有多个所有者,引用计数既是用来记录所有者的数量。当对象没有所有者时,即引用计数为0时,就会释放自己。作为对象本身不需要知道所有者是谁,只需知道所有者的个数。对象通过retain计数跟踪所有者的数量。这个是通过NSObject定义的协议与标准方法命名约定相结合的方法来实现的。引用计数其实是在进行“责任落实”:谁创建了对象(或保留了已经创建的对象),谁就是该对象的所有者。释放对象即放弃该对象的所有权。谁有对象的所有权,谁就要负责放弃该所有权。在不能再向相应对象发送消息时,即不再拥有指向该对象的指针时,需要放弃该所有权。但此算法无法回收循环引用的存储对象。Cocoa目前采用的就是此种机制。(Cocoa是苹果公司为Mac OS X所创建的原生面向对象的API,是Mac OS X上五大API之一,其它四个是Carbon、POSIX、X11和Java)

manual retain-release(MRR)

MRR可以理解为当对象创建后,会有一个所有者,即新建对象的retain计数是1.当对象得到某个所有者时,retain计数+1,当对象失去某个所有者时,调用release方法,retain计数-1.当对象没有任何所有者时,retain计数为0.对象会自动调用dealloc,将所占用的内存还给堆。用代码来实现就是:

- (id)retain
{
    retainCount++;
    return self;
}
- (void)release
{
    retainCount--;
    if(retainCount == 0){
        [self dealloc];
    } 
}

何为所有者?何为拥有该对象的所有权?就是当你在OC中用“alloc”, “new”, “copy”, or “mutableCopy”方法创建对象后,即是此对象的拥有者。还有就是当你在保留某一对象的值的时候,也是拥有了该对象(属性中的set方法深刻的说明了这一点)。以nane属性为例,它的set方法应该写为:

- (void)setName:(NSString *)str
{
    [str retain];
    [name release];
    name = str;
}

这里必须先保留新对象,再释放当前对象。这是因为name和str有可能指向同一个对象。如果颠倒顺序,就有可能释放掉原本打算作为name保留的对象。在类中,当类拥有其它实例对象的时候,要在dealloc方法中将其release掉。

引用计数的规则:

1.如果用来创建对象的方法,其方法名是以alloc或new开头的,或是包含copy和mutableCopy,那么你已经拥有该对象的所有权。你要负责在不需要该对象的时候将其释放。

2.如果你不拥有某个对象,但是要确保该对象继续存在,那么可以通过向其发送retain消息来获得所有权(retain计数+1)。

3.当你拥有某个对象并且不再需要该对象的时候,要release或autorelea掉。(下面会详细介绍autorelease)

4.只要对象还有至少一个所有者,该对象就会继续存在下去,只有在retain计数为0时,才会收到dealloc消息。

使用自动释放池(autorelease)

苹果官网是这样解释的:自动释放池块提供了一种机制,让你可以放弃对象的所有权,但要避免它被立即释放(例如,当您返回一个对象的类方法)的可能性。通常情况下,你并不需要创建自己的autorelease池块。在OC中的类方法,是为“他人”创建对象,“自己”不拥有,也不使用。那类方法中对象的内存怎么管理呢?这里需要某种解决方案,能够暂时不释放对象,但具备释放该对象的权利。通过向对象发送autorelease消息,可以将对象标记为“稍后释放”。当对象收到autorelease后,不会马上释放,而是会加入一个NSAutoreleasePool实例。该NSAutoreleasePool实例会记录所有标记为“稍后释放”的对象。每隔一段时间,这个NSAutoreleasePool实例会被“排干(drain)”,这时它会向其包含的所有对象发送release消息,然后移除这些对象。

标记为autorelease的对象有2种命运:要么走完对象的生命周期,直到被释放,要么被另外一个对象保留。当某一对象保留了标记为autorelease的对象后,那么他的retain count计数会变成2,将来的某个时候,NSAutoreleasePool实例会释放该对象,使其retain count计数为1.何为“将来的某个时候”?ios应用在运行时,存在一个运行循环(run loop)。该运行循环等待事件(event)的发生,例如触摸事件或定时器触发(NSTimer)等等,当事件发生时,应用会跳出运行循环并通过调用某个类方法来处理相应的事件。代码执行完毕后,应用将返回当前的运行循环。每次循环结束,所有标记为autorelease的对象都会收到release消息。

Automatic Reference Counting(ARC)

ARC机制极大的减少了开发过程中常见的程序错误:retain跟release不匹配。ARC并不会消除对retain和release的调用,而是把这项原本大都属于开发者的工作移交给了编译器。ARC并不等同于垃圾回收。retain和release仍然会被调用,所以有一些开销,在release的时候可能还会调用dealloc方法。这段代码与程序员手动调用retain和release的代码在运行结果上是完全一致的。垃圾回收机制是在运行时起作用的,会影响运行效率,而ARC是在编译时插入内存管理代码,不影响运行时效率,因此内存回收比垃圾回收时的效率要高,能够提升系统性能。这种编译器可以自由地以多种方式优化内存管理,而让程序员手动去做这些工作是不现实的。在多数情况下,使用ARC生成内存管理代码的程序比程序员手工添加内存管理代码的对等程序运行更快!

ARC不是垃圾回收,尤其是它不能像Snow Leopard中的垃圾回收机制那样处理循环引用。因此,在ios开发中,必须要做好对强引用(strong reference)的跟踪管理以免出现循环引用。属性关系有两种主要类型:strong和weak。相当于非ARC环境里的retain和assign。只要存在一个强引用,对象就会一直存在,不会被销毁。OC中一直存在循环引用的问题,但在实际应用中很少出现循环引用。对于过去那些使用assign属性的地方,在ARC环境中要使用weak代替。大部分引用循环是由委托(delegate)引起的,所以应该总是把delegate属性声明为weak。当引用的对象被销毁之后,weak引用会被自动设置为nil,与assign相比这是一个巨大的进步,因为assign可以指向被释放掉的内存,导致程序奔溃。

二.可达性分析算法

近现代的垃圾回收实现方法,通过定期对若干根储存对象开始遍历,对整个程序所拥有的储存空间查找与之相关的存储对象和没相关的存储对象进行标记,然后将没相关的存储对象所占物理空间回收。既通过一系列的GC Roots的对象作为起始点,从这些根节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。即是可回收的。此算法可回收循环引用的存储对象。(Java和C#语言采用的机制)


引申阅读:深拷贝和浅拷贝

深拷贝:简单说就是对指针指向的内容进行拷贝,以字符串为例,就是指创建一个新的指针在一个新的地址区域创建一个字符串,这个字符串与原字符串值相同,新的指针指向这个新创建的字符串。而原字符串的引用计数没有+1

浅拷贝:既指针拷贝,例如一个指针指向一个字符串,也就是说这个指针变量的值是这个字符串的地址,那么对这个指针拷贝就是又创建了一个指针变量,这个指针变量的值是这个字符串的地址,也就是这个字符串的引用计数+1

关于深浅拷贝看源码一目了然,以NSString和NSMutableString为例:

- (id)copyWithZone:(NSZone*)zone
{
  if (NSStringClass == Nil) NSStringClass = [NSString class];
  return RETAIN(self)        
}

- (id)mutableCopyWithZone:(NSZone*)zone
{
  return [[NSMutableString allocWithZone:zone] initWithString:self];  
}

看上面代码,当属性设置copy时,实际调用的就是copyWithZone方法,而copyWithZone并没有创建新的对象,而是使指针持有了原来的对象,即浅拷贝。而属性设置mutableCopy时,调用的就是mutableCopyWithZone方法,而这个方法创建了一个新的可变字符串对象,即深拷贝。