zoukankan      html  css  js  c++  java
  • Objective-C中的引用计数

     导言

    Objective-C语言使用引用计数来管理内存,也就是说,每个对象都有个可以递增或递减的计数器。如果想使某个对象继续存活,那就递增其引用计数;用完了之后,就递减其计数。计数为0,就表示没人关注此对象了,于是,就可以把它销毁。

    从Mac OS X 10.8开始,“垃圾收集器”(garbage collector)已经正式废弃了,以Objective-C代码编写Mac OS X程序时不应再使用它,而iOS则从未支持过垃圾收集。因此,掌握引用计数机制对于学好Objective-C来说十分重要。Mac OS X程序已经不能再依赖垃圾收集器了,而iOS系统不支持此功能,将来也不会支持。

    已经用过ARC的人可能会知道:所有与引用计数有关的方法都无法编译,然而现在先暂时忘掉这件事。那些方法确实无法用在ARC中,不过本文就是要从Objective-C的角度讲解引用计数,而ARC实际上也是一种引用计数机制,所以,还是要谈谈这些在开启ARC功能时不能直接调用的方法。

    工作原理

    在引用计数架构下,对象有个计数器,用以表示当前有多少个事物想令此对象继续存活下去。这在Objective-C中叫做“保留计数”(retain count),不过也可以叫“引用计数”(reference count)。NSObject协议声明了下面三个方法用于操作计数器,以递增或递减其值:

    1)retain 递增保留计数。

    2)release 递减保留计数。

    3)autorelease 待稍后清理“自动释放池”(autorelease pool)时,再递减保留计数。

     

    上图是对象创建及保留计数操作的效果图。

     

    上图对象图中,ObjectB与ObjectC都引用了ObjectA。若ObjectB与ObjectC都不再使用ObjectA,则其保留计数降为0,于是便可摧毁了。还有其他对象想令ObjectB与ObjectC继续存活,而应用程序里又有另外一些对象想令那些对象继续存活。如果按“引用树”回溯,那么最终会发现一个“根对象”(root object)。在Mac OS X应用程序中,此对象是NSApplication对象;而在iOS应用程序中,则是UIApplication对象。两者都是应用程序启动时创建的单例。

    下面这段代码有助于理解这些方法的用法:

    NSMutableArray *array = [[NSMutableArray alloc] init];
    NSNumber *number = [[NSNumber alloc] initWithInt:1337];
    [array addObject:number];
    [number release];
    //do something with 'array'
    
    [array release];

    由于代码中直接调用了release方法,所以在ARC下无法编译。在Objective-C中,调用alloc方法所返回的对象由调用者所拥有。也就是说,调用者已通过alloc方法表达了想令该对象继续存活下去的意愿。不过,这并不是说对象此时的保留计数就是1。在alloc或“initWithInt:”方法的代码实现中,也许还有其他对象也保留了此对象。绝不能说保留计数一定是某个值,只能说你所执行的操作的递增了该计数还是递减了该计数。

    创建完数组后,把number对象加入其中。调用数组的“addObject:”方法时,数组也会在number上调用retain方法,以期继续保留此对象。这时,保留计数至少为2。接下来,代码不再需要number对象了,于是将其释放。现在的保留计数至少为1。这样就不能照常使用number变量了。调用release之后,已经无法保证所指的对象仍然存活。当然,根据本例中的代码,我们显然知道number对象在调用了release之后仍然存活,因为数组还在引用着它。然而绝不应该假设此对象一定存活,也就是说,不要像下面这样子编写代码:

    NSNumber *number = [[NSNumber alloc] initWithInt:1337];
    [array addObject:number];
    [number release];
    NSLog(@"number = %@", number);

    即便上述代码在本例中可以正常执行,也仍然不是个好办法。如果调用release之后,基于某些原因,其保留计数降至为0,那么number对象所占内存也许会回收,这样的话,再调用NSLog可能就将使程序崩溃了。为什么是“可能”,因为对象所占的内存在“解除分配”(deallocated)之后,只是放回“可用内存池”(avaiable pool)。如果执行NSLog时还尚未覆写对象内存,那么该对象仍然有效,这是程序不会崩溃。故,因过早释放对象而导致的bug很难调试

    为避免在不经意间使用了无效对象,一般调用完release之后都会清空指针。这就能保证不会出现可能指向无效对象的指针,这种指针通常称为“悬挂指针”(dangling pointer)。例如,可以这样编写代码来防止此情况发生:

    NSNumber *number = [[NSNumber alloc] initWithInt:1337];
    [array addObject:number];
    [number release];
    number = nil;

    属性存取方法中的内存管理

    如前所述,对象图由相互关联的对象所构成。刚才那个例子中的数组通过在其元素上调用retain方法来保留那些对象。不光数组,其他对象也可以保留别的对象,这一般通过访问“属性”来实现,而访问属性时,会用到相关实例变量的获取方法和设置方法。若属性为“strong关系”(strong relationship),则设置的属性值会保留。比方说,有个名叫foo的属性由名为_foo的实例变量所实现,那么,该属性的设置方法会是这样:

    -(void)setFoo:(id)foo {
             [foo retain];
             [_foo release];
             _foo = foo;
    }

    此方法将保留新值并释放旧值,然后更新实例变量,令其指向新值。顺序很重要。假如还未保留新值就先把旧值释放了,而两个值又指向同一个对象,那么,先执行release操作就可能导致系统将此对象永久回收。而后续的retain操作则无法令这个已经彻底回收的对象复生,于是实例变量就成了悬挂指针。

    自动释放池

    在Objective-C的引用计数架构中,自动释放池是一项重要特性。调用release会立刻递减对象的保留计数(而且还可能令系统回收此对象),然而有时候可以不调用它,改为调用autorelease,此方法会在稍后递减计数,通常是在下一次“事件循环”(event loop)时递减,不过也可能执行得更早些。 

    此特性很有用,尤其是在方法中返回对象时更应该用它。在这种情况下,我们并总是想令方法调用者手工保留其值。比方说,有下面这个方法:

    -(NSString *)stringValue {
             NSString *str = [[NSString alloc] initWithFormat:@"I am this: %@", self];
             return str;
    }

    此时返回的str对象其保留计数比期望值要多1,因为调用者alloc会令保留计数加1,而又没有与之对应的释放操作。保留计数多1,就意味着调用者要负责处理多出来的这一次保留操作。必须设法将其抵消。这并不是说保留计数本身就一定是1,它可能大于1,不过那取决于“initWithFormat:”方法内的实现细节。你要考虑的是如何将多出来的这一次保留操作抵消掉。但是,不能在方法呢你释放str,否则还没等方法返回,系统就把该对象回收了。这里应该用autorelease,它会在稍后释放对象,从而给调用者留下了足够长的时间,使其可以在需要时先保留返回值。换句话说,此方法可以保证对象在跨越“方法调用边界”(method call boundary)后一定存活。实际上,释放操作会在清空最外层的自动释放池时执行,除非你有自己的自动释放池,否则这个时机指的就是当前线程的下一次事件循环。改写stringValue方法,使用autorelease来释放对象:

    -(NSString *)stringValue {
             NSString *str = [[NSString alloc] initWithFormat:@"I am this: %@", self];
             return [str autorelease];
    }

    修改之后,stringValue方法把NSString对象返回给调用者,此对象必然存活。所以我们能够如此使用它:

    NSString *str = [self stringValue];
    NSLog(@"The string is: %@", str);

    由于返回的str对象将于稍后自动释放,所以多出来的那一次保留操作时自然就会抵消,无须再执行内存管理操作。因为自动释放池中的释放操作要等到下一次事件循环时才会执行,所以NSLog语句在使用str对象前不需要手工执行保留操作。但是,假如要持有此对象的话(比如将其设置给实例变量),那就需要保留,并于稍后释放:

    _instanceVariable = [[self stringValue] retain];
    //...
    
    [_instaceVariable release];

    由此可见,autorelease能延长对象生命期,使其在跨越方法调用边界后依然可以存活一段时间。

    保留环

    使用引用计数机制时,经常要注意的一个问题就是“保留环”(retain cycle),也就是呈环状相互引用的多个对象。这将导致内存泄露,因为循环中的对象其保留计数不会降为0。对于循环中的每个对象来说,至少还有另外一个对象引用着它。

     

    如上图,在这个循环里,所以对象的保留计数都是1。在垃圾收集环境中,通常将这种情况认定为“孤岛”(island of isolation)。此时,垃圾收集器会把三个对象全部回收。而在Objective-C的引用计数架构中,则享受不到这一便利。通常采用“弱引用”(weak reference)来解决此问题,或是从外界命令循环中的某个对象不再保留另外一个对象。这两种办法都能打破保留环,从而避免内存泄露。 

    小结

    引用计数机制通过可以递增递减的计数器来管理内存。对象创建好之后,其保留计数至少为1。若保留计数为正,则对象继续存活。当保留计数降为0时,对象就被销毁。

    在对象生命周期中,其余对象通过引用来保留或释放此对象。保留与释放操作分别会递增及递减保留计数。

    参考书籍:《Effective Objective-C 2.0》

  • 相关阅读:
    Discuz X 2.5 点点(伪静态)
    jq 、xml 省市级联动
    php memcache 初级使用(2)
    关于windows虚拟内存管理的页目录自映射
    SharePoint 2010 网络上的开发经验和资源
    SharePoint 2010 Reporting Services 报表服务器正在内置 NT AUTHORITY\SYSTEM 账户下运行 解决方法
    SharePoint 2010 Reporting Services 报表服务器无法解密用于访问报表服务器数据库中的敏感数据或加密数据的对称密钥 解决方法
    Active Directory Rights Management Services (AD RMS)无法检索证书层次结构。 解决方法
    SharePoint 2010 Reporting Services 报表服务器实例没有正确配置 解决方法
    SharePoint 2010 页面引用 Reporting Services 展现 List 报表
  • 原文地址:https://www.cnblogs.com/chars/p/5232109.html
Copyright © 2011-2022 走看看