【Effective Objective - C】—— 读书笔记(五)

【Effective Objective - C】—— 读书笔记(五)

内存管理

29.理解引用计数

Objective-C 语言使用引用计数来管理内存,也就是说,每个对象都有个可以递增或递减的计数器,如果某个对象引用他时就会给其引用计数加1,用完了之后,就递减其计数,直至为0,销毁这个对象。ARC实际上也是一种引用计数机制。

引用计数工作原理

在引用计数架构下,对象有个计数器,用以表示当前有多少个事物想令此对象继续存活下去。NSObject协议声明了下面三个方法用于操作计数器:

  • Retain 递增保留计数
  • release 递减保留计数
  • autorelease 待稍后清理“自动释放池”时,再递减保留计数。

查看保留计数的方法叫做retainCount,此方法不太有用, 即便在调试时也如此,所以笔者(与苹果公司)并不推荐大家使用这个方法。
对象创建出来时,其保留计数至少为1。若想令其继续存活,则调用retain 方法。要是某部分代码不再使用此对象,不想令其继续存活,那就调用release或autorelease方法。最终当保留计数归零时,对象就回收了(deallocated),也就是说,系统会将其占用的内存标记为“可重用”。此时,所有指向该对象的引用也都变得无效了。
在这里插入图片描述
通常我们都使用alloc方法来创建一个对象,给对象一个继续存活下去的意愿,但是使用alloc方法创建的对象不一定其引用计数创建出来就是1,我们只能说明保留计数至少为1。并且我们如果在对象的引用计数为0的情况下,即其“可复用”的情况下使用这个变量的话,程序就会崩溃。但是若是将其放到了“自动释放池”中程序就可能不会崩溃了。所以我们为了避免这种情况的发生,我们通常就将对象释放完后将其置空,这种指针通常称为“悬挂指针”。
在这里插入图片描述

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

如前所述,对象图由互相关联的对象所构成。刚才那个例子中的数组通过在其元素上调 用retain 方法来保留那些对象。不光是数组,其他对象也可以保留别的对象,这 一般通过访 问“ 属性” (参见第6 条)来实现,而访问属性时,会用到相关实例变量的获取方法及设置方法。若属性为“ 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,而又没有与之对应的释放操作,这就会有很大的影响,但是在何处释放就又是问题了,因为其还的返回,返回完后又获取不到这个str,也不能在返回之后释放,所以此时就用到了autorelease,在其返回后保留一段时间再释放。

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

实际上,释放操作会在清空最外层的自动释放池时执行,除非你有自己的自动释放池,否则这个时机指的就是当前线程的下一次事件循环。
通过上述可见,autorelease能延长对象生命期,使其在跨越方法调用边界后依然可以存活一段时间。

保留环

保留环其实就是因为对象之间的互相引用出现的问题,这会导致内存泄漏,因为循环中的对象其保留计数不会降为0。
在这里插入图片描述
我们要解决保留环,通常采用“弱引用”来解决此问题,或者从外界命令循环中的某个对象不在保留另一个对象。

要点

  • 引用计数机制通过可以递增递减的计数器来管理内存。对象创建好之后,其保留计数至少为1。若保留计数为正,则对象继续存活。当保留计数降为0时,对象就被销毁了。
  • 在对象生命期中,其余对象通过引用来保留或释放此对象。保留与释放操作分别会递增及递减保留计数。

30.以ARC简化引用计数

使用ARC时一定要记住,引用计数实际上 还是要执行的,只不过 保 留 与 释 放 操 作 现 在 是由ARC 自动为你添加。稍后将会看到,除了为方法所返回的对象正确运用内存管理语义 之外,ARC 还有更多的功能。 不过,ARC 的那些功能都是基 于核心的内存管理语义而构建的, 这套标准语义贯穿于整 个Objective-C语言。

由于ARC会自动执行retain 、release 、autorelease 等操作,所以直接在ARC下调用这些 内存管理方法是非法的。具体来说,不能调用下列方法:

  • retain
  • release
  • autorelease
  • dealloc

实际上,ARC 在调用这些方法时,并不通过普通的Objective-C 消息派发机制,而是直 接调用其底层C 语言版本。这样做性能更好,因为保留及释放操作需要频繁执行,所以直 接 调 用 底 层 丽 数 能 节省很多CPU周期。比方说,ARC会调用与retain等价的底层丽数 objc_retain。这也是不能覆写retain、release 或autorelease 的缘由,因为这些方法从来不会被直接 调用。笔者在本节后面的文字中将用等价的Objective-C 方法来指代与之相关的底层C语言 版本,这对 于那些 手动管理过引用计数的开发者来说更易理解。

使用ARC时必须遵守的方法命名规则

若方法名以下列词语开头,则其返回的对象归调用者所有:

  • alloc
  • new
  • copy
  • mutableCopy

意思就是调用这四种方法的那段代码要负责释放方法所返回的对象。也就是说,这些对象的保留计数是正值,而调用了这四种方法的那段代码要将其中一次保留操作抵消。若方法名不以上述四个词语开头,则表示其所返回的对象并不归调用者所有。这种情况下,返回的对象会自动释放,也就是使用了autorelease。

ARC除了自动调用“保留”和“释放”方法外,其也可以优化操作,比如两个在一起的保留和释放,它就会将这一对直接移除,不执行这两个代码。

在ARC环境下编译代码时,必须考虑“向后兼容性”,以兼容那些不使用ARC的代码,其实ARC的简化操作是因为其调用的特殊函数,它会把autorelease方法改为调用objc_autoreleaseReturnValue函数,把retain方法改为objc_retainAutoreleaseReturnValue函数。
下列代码演示了ARC的用法∶

+ (EOCPerson *)newPerson {
    
    
	EOCPerson *person = [[EOCPerson alloc] init];
	return person;
}

+ (EOCPerson *)somePerson {
    
    
	EOCPerson *person = [[EOCPerson alloc] init];
	return person;
}

- (void)doSomething {
    
    
	EOCPerson *personOne = [EOCPerson newPerson];
	EOCPerson *personTwo = [EOCPerson somePerson];
}

ARC通过命名约定将内存管理规则标准化,初学此语言的人通常觉得这有些奇怪,其他编程语言很少像 Objective-C这样强调命名。但是,想成为优秀的 Objective-C程序员就必须适应这套理念。在编码过程中,ARC 能帮程序员做许多事情。
除了会自动调用"保留"与"释放"方法外,使用ARC还有其他好处,它可以执行一些手工操作很难甚至无法完成的优化。 例如,在编译期,ARC会把能够互相抵消的 retain、release、autorelease 操作约简。如果发现在同一个对象上执行了多次"保留"与"释放"操作,那么 ARC有时可以成对地移除这两个操作。

变量的内存管理语义

ARC也会处理局部变量与实例变量的内存管理。通常情况下,每个变量都是指向对象的强引用。

就用set方法来说,不使用ARC而自己设置set方法就需要这样写:

- (void)setObject:(id)object {
    
    
    [_object release];
    _object = [object retain];
}

但是这样写会出现问题,如果新值和实例变量已有的值相同了,它再执行release就会将其释放其保留计数若降为0,后来再进行retain保留操作,程序就会报错,而使用ARC仅仅需要这样就够了:

- (void)setObject:(id)object {
    
    
    _object = object;
}

ARC会用一种安全的方式来设置:先保留新值,再释放旧值,最后设置实例变量。
我们通常会给局部变量加上修饰符,用以打破由“块”所引入的“保留环”。块会自动保留其所捕获的全部对象,而如果这其中有某个对象又保留了块本身,那么就可能导致“保留环”。

在应用程序中,可用以下修饰符来改变局部变量与实例变量的语义:

  • __strong: 默认语义,保留此值。
  • __unsafe_unretained:不保留此值,这么做可能不安全,因为等到再次使用变量时,其对象可能己经回收了。
  • __weak:不保留此值,但是变量可以安全使用,因为如果系统把这个对象回收了, 那么变量也会自动清空。
  • __autoreleasing:把对象“按引用传递”给方法时,使用这个特殊 的修饰符 。此值在方法返回时自动释放。

ARC如何清理实例变量

要管理其内存,ARC就必须在“回收分配给对象的内存”是生产必要的清理代码。ARC环境下,dealloc方法可以这样来写:

- (void)dealloc {
    
    
	CFRelease ( _coreFoundationObject);
	free ( _heapAllocatedMemoryBlob);
}

因为ARC会自动生成回收对象时所执行的代码,所以通常无需再编写dealloc方法。这能减少项目源代码的大小,而且可以省去其中一些样板代码。

要点:

  • 有ARC之后,程序员就无须担心内存管理问题了。使用ARC来编程,可省去类中的许多“样板代码”。
  • ARC管理对象生命期的办法基本上就是:在合适的地方插入“保留”及“释放”操作。在ARC环境下,变量的内存管理语义可以通过修饰符指明,而原来则需要手工执行“保留”及“释放”操作。
  • 由方法所返回的对象,其内存管理语义总是通过方法名来体现。ARC将此确定为开发者必须遵守的规则。
  • ARC只负责管理OC对象的内存。尤其要注意CoreFoundation对象不归ARC管理,开发者必须适时调用CFRetain/CFRelease。

31.在dealloc方法中只释放引用并解除监听

对象在经历其生命期后,最终会为系统所回收,这时就要执行dealloc 方法了。在每个 对象的生命期内,此方法仅执行一次,也就是当保留计数降为。的时候。然而具体何时执 行,则无法保证。也可以理解成:我们能够通过人工观察保留操作与释放操作的位置,来预 估此方法何时即将执行。但实际 上,程序库会以开发者察觉不到的方式操作对象,从而使回 收对象的真正时机和预期的不同。你决不应该自己调用dealloc 方法。运行期系统会在适当 的时候调用它。而且, 一旦调用过dealloc之后,对象就不再有效了,后续方法调用均是无效的。

那么,应该在dealloc 方法中做些什么呢?主要就是释放对象所拥有的引用,也就是把 所 有 Objective-C对象都释放掉,ARC会通过自动生成的.cxx_destruct方法 (参见第30条), 在dealloc 中为你自动添加这些释放代码。对象所拥有的其他非Objective-C 对象也要释放。 比如CoreFoundation 对象就必领手工释放,因为它们是由纯C的API 所生成的。

在dealloc 方法中,通常还要做一件事,那就是把原来配置过的观测行为(observation behavior )都清理掉。如果用NSNotificationCenter 给此对象订阅(register)过某种通知,那么 一般应该在这里注销(unregister ),这样的话,通知系统就不再把通知发给回收后的对象了, 若是还向其发送通知,则必然会令应用程序崩溃。

dealloc 方法可以这样来写:

- (void)dealloc {
    
    
	CFRelease(coreFoundationObject);
	[[NSNotificationCenter defaultCenter] removeObserver:self];
}

要点:

  • 在dealloc方法里,应该做的事情就是释放指向其他对象的引用,并取消原来订阅的“键值观测”或“NSNotificationCenter”等通知,不要做其他事情。
  • 如果对象持有文件描述符等系统资源,那么应该专门编写一个方法来释放此种资源。这样的类要和其使用者约定:用完资源后必须调用close方法。
  • 执行异步任务的方法不应在dealloc里调用;只能在正常状态下执行的那些方法也不应在dealloc里调用,因为此时对象已处于正在回收的状态了。

32.编写“异常安全代码”时留意内存管理问题

许多时下流行的编程语言都提供了"异常"(exception)这一特性。纯C中没有异常,而C++与Objective-C都支持异常。实际上,在当前的运行期系统中,C++与Objective-C的异常相互兼容,也就是说,从其中一门语言里抛出的异常能用另外一门语言所编的"异常处理程序"(exception handler)来捕获。
Objective-C的错误模型表明,异常只应在发生严重错误后抛出(参见第 21条),虽说如此,不过有时仍然需要编写代码来捕获并处理异常。比如使用Objective-C++来编码时,或是编码中用到了第三方程序库而此程序库所抛出的异常又不受你控制时,就需要捕获及处理异常了。此外,有些系统库也会用到异常,这使我们想起从前那个频繁使用异常的年代。比如,在使用"键值观测"(KVO)功能时,若想注销一个尚未注册的"观察者",便会抛出异常。
发生异常时应该如何管理内存是个值得研究的问题。在 try 块中,如果先保留了某个对象,然后在释放它之前又抛出了异常,那么,除非 catch块能处理此问题,否则对象所占内存就将泄漏。C++的析构函数(destructor)由 Objective-C 的异常处理例程(exception-handle routine)来运行。这对于C++对象很重要,由于抛出异常会缩短其生命期,所以发生异常时必须析构,不然就会泄漏,而文件句柄(file handle)等系统资源因为没有正确清理,所以就更容易因此而泄漏了。
异常处理例程将自动销毁对象,然而在手动管理弓用计数时。销毁工作有些麻烦。以下面这段使用手工引用计数的 Objective-C代码为例∶

@try {
    
    
	EOCSomeClass *object = [ [EOCSomeClass alloc] init];
	[object doSomethingThatMayThrow];
	[object release];
}
@catch(...) {
    
    
	NSLog(@"whoops,there was an error. Oh well...");
}

乍一看似乎没问题,但如果 doSomethingThatMayThrow抛出异常了呢?由于异常会令执行过程终止并跳至 catch 块,因而其后的那行 release 代码不会运行。在这种情况下,如果代码抛出异常,那么对象就泄漏了。这么做不好。解决办法是使用@finally 块,无论是否抛出异堂其中的代码都保证会运行,日 只运行一次。H方说,刚才那段代码可改写如下

EOCSomeClass *object;
@try {
    
    
	object = [ [EOCSomeClass alloc] init];
	[object doSomethingThatMayThrow] ;
}	
@catch (...){
    
    
	NSLog (@"whoops,there was an error.Oh well...");
}
@finally {
    
    
	[object release];
}

注意,由于@finally 块也要引用object 对象,所以必须把它从 @try 块里移到外面去。要是所有对象都得如此释放,那这样做就会非常乏味。而且,假如 @try 块中的逻辑更为复杂,含有多条语句,那么很容易就会因为忘记某个对象而导致泄漏。若泄漏的对象是文件描述符或数据库连接等稀缺资源(或是这些稀缺资源的管理者),则可能引发大问题,因为这将导致应用程序把所有系统资源都抓在自己手里而不及时释放。
在 ARC 环境下,问题会更严重。下面这段使用ARC 的代码与修改前的那段代码等效∶

@try {
    
    
	EOCSomeClass *object = [[EOCSomeClass alloc] init];
	[object doSomethingThatMayThrow ];
}
@catch (...) {
    
    
	NSLog(@"whoops,there was an error. Oh well...");
}	

现在问题更大了∶由于不能调用release,所以无法像手动管理引用计数时那样把释放操作移到 @finally 块中。你可能认为这种状况 ARC自然会处理的。但实际上 ARC不会自动处理,因为这样做需要加入大量样板代码,以便跟踪待清理的对象,从而在抛出异常时将其释放。可是,这段代码会严重影响运行期的性能,即便在不抛异常时也如此。而且,添加进来的额外代码还会明显增加应用程序的大小。这些副作用都不甚理想。

要点

  • 捕获异常时,一定要注意将 try 块内所创立的对象清理干净。
  • 在默认情况下,ARC不生成安全处理异常所需的清理代码。开启编译器标志后,可生成这种代码,不过会导致应用程序变大,而且会降低运行效率。

33.以弱引用避免保留环

先来看看保留环的概念:就是几个对象都以某种方式互相引用,从而形成"环"(cycle)。
由于 Objective-C 内存管理模型使用引用计数架构,所以这种情况通常会泄漏内存,因为最后没有别的东西会引用环中的对象。这样的话,环里的对象就无法为外界所访问了,但对象之间尚有引用,这些引用使得它们都能继续存活下去,而不会为系统所回收。
最简单的保留环由两个对象构成,它们互相引用对方。下图举例说明了这种情况。这种保留环的产生原因不难理解,且很容易就能通过查看代码而侦测出来∶
举个例子:

#import<Foundation/Foundation.h>
@class EOCClassA;
@class EOCClassB;

@interface EOCClassA : NSObject
@pzoperty(nonatomic,strong) EOCClassB *other;
@end

@interface EOCClassB : NSObject
@property(nonatomic,strong) EOCClassA *other;
@end

在这里插入图片描述

避免保留环的最佳方式就是弱引用。这种引用经常用来表示"非拥有关系"(nonowning relationship)。将属性声明为 unsafe_unretained 即可。修改刚才那段范例代码,将其属性声明如下∶

#import <Foundation/Foundation.h>

@class EOCClassA;
@class EOCClassB;

@interface EOCClassA : NSObject
@property(nonatomic,strong)EOCClassB *other;
@end

@interface EOCClassB: NSObject
@property(nonatomic,unsafe unretained) EOCClassA *other;
@end

修改之后,EOCClassB 实例就不再通过 other 属性来拥有 EOCClassA实例了。属性特质(attribute)中的 unsafe unretained一词表明,属性值可能不安全,而且不归此实例所拥有。如果系统已经把属性所指的那个对象回收了,那么在其上调用方法可能会使应用程序崩溃。由于本对象并不保留属性对象,因此其有可能为系统所回收。
用 unsafe unretained修饰的属性特质,其语义同 assign特质等价(参见第6条)。然而,assign通常只用于"整体类型"(int、float、结构体等),unsafe unretained 则多用于对象类型。这个词本身就表明其所修饰的属性可能无法安全使用(unsafe)。
Objective-C中还有一项与ARC 相伴的运行期特性,可以令开发者安全使用弱引用∶这就是weak属性特质,它与unsafe unretained 的作用完全相同。然而,只要系统把属性回收,属性值就会自动设为 nil。在刚才那段代码中,EOCClassB的 other属性可修改如下∶

@property (nonatomic,weak) EOCClassA *other;

下图演示了 unsafe unretained与 weak 属性的区别:
在这里插入图片描述

当指向 EOCClassA 实例的引用移除后,unsafe unretained属性仍然指向那个已经回收的实例,而weak 属性则指向 nil。
但是,使用 weak属性并不是偷懒的借口。在刚才那个例子中,如果在 EOCClassA 对象已经回收之后,引用它的 EOCClassB实例仍然存活,那么就是编程错误。发生这种情况,就是 bug。开发者应确保程序中不出现此类问题。然而,使用 weak 而非 unsafe unretained引用可以令代码更安全。应用程序也许会显示出错误的数据,但不会直接崩溃。这么做显然比令终端用户直接看到程序退出要好。不过无论如何,只要在所指对象已经彻底销毁后还继续使用弱引用,那就依然是个bug。比方说,用户界面中的某个元素会把数据源设置给某个属性,并通过它来查询将要显示的数据。这种属性通常是弱引用(参见第 23条)。假如还未等界面元素查询完数据源对象就已经回收。那么。继续使用弱弓|用虽不致程序崩溃。但却无法再查到数据了。
一般来说,如果不拥有某对象,那就不要保留它。这条规则对 collection 例外,collection 虽然并不直接拥有其内容,但是它要代表自己所属的那个对象来保留这些元素。有时,对象中的引用会指向另外一个并不归自己所拥有的对象,比如 Delegate 模式就是这样(参见第 23条)。

要点:

  • 将某些引用设为 weak,可避免出现"保留环"。
  • weak 引用可以自动清空,也可以不自动清空。自动清空(autonilling)是随着 ARC 而引入的新特性,由运行期系统来实现。在具备自动清空功能的弱引用上,可以随意读取其数据,因为这种引用不会指向已经回收过的对象。

34.以"自动释放池块"降低内存峰值

Objective-C对象的生命期取决于其引用计数(参见第29条)。在 Objective-C的引用计数架构中,有一项特性叫做"自动释放池"(autorelease pool)。释放对象有两种方式;一种是调用release 方法,使其保留计数立即递减;另一种是调用 autorelease 方法,将其加入"自动释放池"中。自动释放池用于存放那些需要在稍后某个时刻释放的对象。清空(drain)自动释放池时,系统会向其中的对象发送 release 消息。
创建自动释放池所用语法如下∶

@autoreleasepool {
    
    
	// ...
}

然而,一般情况下无须担心自动释放池的创建问题。Mac OS X与 iOS 应用程序分别运行于Cocoa 及 Cocoa Touch 环境中。系统会自动创建一些线程,比如说主线程或是"大中枢派发"(Grand Central Dispatch,GCD)③机制中的线程,这些线程默认都有自动释放池,每次执行"事件循环"(event loop)时,就会将其清空。因此,不需要自己来创建"自动释放池块"。通常只有一个地方需要创建自动释放池,那就是在 main 函数里,我们用自动释放池来包裹应用程序的主入口点((main application entry point)。比方说,iOS程序的 main 函数经常这样写∶

int main(int argc,char *argv[]) {
    
    
	@autoreleasepool {
    
    
		return UIApplicationMain (argc, argV, ni1, @"EOCAppDelegate");
	}
}	

从技术角度看,不是非得有个"自动释放池块"才行。因为块的末尾恰好就是应用程序的终止处,而此时操作系统会把程序所占的全部内存都释放掉。虽说如此,但是如果不写这个块的话,那么由 UIApplicationMain 函数所自动释放的那些对象,就没有自动释放池可以容纳了,于是系统会发出警告信息来表明这一情况。所以说,这个池可以理解成最外围捕捉全部自动释放对象所用的池。
下面这段代码中的花括号定义了自动释放池的范围。自动释放池于左花括号处创建,并干对应的右花括号处自动清空。位于自动释放池范围内的对象。将在此范围末尾处收到release消息。自动释放池可以嵌套。系统在自动释放对象时,会把它放到最内层的池里。比方说∶

@autoreleasepool {
    
    
	NSString *string = [NSString stringwithFormat:@"1 = %i",1];
	@autoreleasepool {
    
    
		NSNumber *number = [NSNumber numberWithInt:1];
	}	
}

本例中有两个对象,它们都由类的工厂方法所创建,这样创建出来的对象会自动释放(参见第30条)。NSString 对象放在外围的自动释放池中,而 NSNumber 对象则放在里层的自动释放池中。将自动释放池嵌套用的好处是,可以借此控制应用程序的内存峰值,使其不致过高。

for (int i = 0; i < 100000; i++) {
    
    
	[self doSomethingWithInt:i];
} 

如果"doSomethingWithInt∶"方法要创建临时对象,那么这些对象很可能会放在自动释放池里。比方说,它们可能是一些临时字符串。但是,即便这些对象在调用完方法之后就不再使用了,它们也依然处于存活状态,因为目前还在自动释放池里,等待系统稍后将其释放并回收。然而,自动释放池要等线程执行下一次事件循环时才会清空。这就意味着在执行for 循环时。会持续有新对象创建出来。并加入自动释放池中。所有这种对象都要等 for 循环执行完才会释放。这样一来,在执行 for 循环时,应用程序所占内存量就会持续上涨,而等到所有临时对象都释放后,内存用量又会突然下降。
这种情况不甚理想,尤其当循环长度无法预知,必须取决于用户输入时更是如此。比方说,要从数据库中读出许多对象。代码可能会这么写∶

NSArray*databaseRecords =/* ...*/;
NSMutableArray *people = [NSMutableArray new];
for (NSDictionary *record in databaseRecords) {
    
    
	EOCPerson *person = [ [EOCPerson alloc] initWithRecord: record];
	[people addObject:person];
}

EOCPerson的初始化函数也许会像上例那样,再创建出一些临时对象。若记录有很多条,则内存中也会有很多不必要的临时对象,它们本来应该提早回收的。增加一个自动释放池即可解决此问题。如果把循环内的代码包裹在"自动释放池块"中,那么在循环中自动释放的对象就会放在这个池,而不是线程的主池里面。例如∶

NSArray *databaseRecords = /*...*/;
NSMutableArray *people = [NSMutableArray new];
for(NSDictionary *record in databaseRecords)[
	@autoreleasepool {
    
    
		EOCPerson *person = [[EOCPerson alloc] initWithRecord:record];
		[people addObject:person];
	}	
}

加上这个自动释放池之后,应用程序在执行循环时的内存峰值就会降低,不再像原来那么高了。内存峰值(high-memory waterline)是指应用程序在某个特定时段内的最大内存用量(highest memory footprint)。新增的自动释放池块可以减少这个峰值,因为系统会在块的末尾把某些对象回收掉。而刚才提到的那种临时对象,就在回收之列。
自动释放池机制就像"栈"(stack)一样。系统创建好自动释放池之后,就将其推入栈中,而清空自动释放池,则相当于将其从栈中弹出。在对象上执行自动释放操作,就等于将其放入栈顶的那个池里。
是否应该用池来优化效率,完全取决于具体的应用程序。首先得监控内存用量,判断其中有没有需要解决的问题,如果没完成这一步。那就别急着优化。尽管自动释放池块的开销不太大,但毕竟还是有的,所以尽量不要建立额外的自动释放池。
如果在ARC 出现之前就写过Obiective-C程序.那么可能还记得有种老式写法。就是使用NSAutoreleasePool对象。这个特殊的对象与普通对象不同,它专门用来表示自动释放池,就像新语法中的自动释放池块一样。但是这种写法并不会在每次执行 for循环时都清空池,此对象更为"重量级"(heayyweight),通常用来创建那种偶尔需要清空的池。比方说∶

NSArray *databaseRecords = /* ... */;
NSMutableArray *people =[NSMutableArray new];
int i = 0;
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
for (NSDictionary*record in databaseRecords) {
    
     
	EOCPerson *person = [[EOCPerson alloc] initWithRecord: record];
	[people addObject:person];
	// Drain the pool only every 10 cycles 
	if (++i == 10){
    
    
		[pool drain];
		i= 0;
	}	
}
[pool drain];

现在不需要再这样写代码了。采用随着 ARC 所引入的新语法,可以创建出更为"轻量级"(lightweight)的自动释放池。原来所写的代码可能会每执行n 次循环清空一次自动释放池,现在可以改用自动释放池块把 for 循环中的语句包起来,这样的话,每次执行循环时都会建立并清空自动释放池。
@autoreleasepool语法还有个好处;每个自动释放池均有其范围,可以避免无意间误用了那些在清空池后已为系统所回收的对象。比方说,考虑下面这段采用旧式写法的代码∶

NSAutoreleasePool *pool =[ [NSAutoreleasePool alloc] init];
id object = [self createObject];
[pool drain];
[self useObject:object];

要点

  • 自动释放池排布在栈中,对象收到 autorelease 消息后,系统将其放入最顶端的池里。
  • 合理运用自动释放池,可降低应用程序的内存峰值。
  • @autoreleasepool 这种新式写法能创建出更为轻便的自动释放池。

35.用"僵尸对象"调试内存管理问题

调试内存管理问题很令人头疼。大家都知道,向业已回收的对象发送消息是不安全的。这么做有时可以,有时不行。具体可行与否,完全取决于对象所占内存有没有为其他内容所覆写。而这块内存有没有移作他用,又无法确定,因此,应用程序只是偶尔崩溃。在没有崩溃的情况下,那块内存可能只复用了其中一部分,所以对象中的某些二进制数据依然有效。还有一种可能,就是那块内存恰好为另外一个有效且存活的对象所占据。在这种情况下,运行期系统会把消息发到新对象那里。而此对象也许能应答。也许不能。如果能。那程序就不崩溃,可你会觉得奇怪∶为什么收到消息的对象不是预想的那个呢?若新对象无法响应选择子,则程序依然会崩溃。
所幸 Cocoa提供了"僵尸对象"(Zombie Object)这个非常方便的功能。启用这项调试功能之后,运行期系统会把所有已经回收的实例转化成特殊的"僵尸对象"、而不会真正回收它们。这种对象所在的核心内存无法重用,因此不可能遭到覆写。僵尸对象收到消息后,会抛出异常,其中准确说明了发送过来的消息,并描述了回收之前的那个对象。僵尸对象是调试内存管理问题的最佳方式。
将NSZombieEnabled环境变量设为 YES,即可开启此功能。比方说,在 Mac OS X系统中用 bash运行应用程序时,可以这么做∶

export NSZombieEnabled = "YES"
./app

也可以在xcode打开:
请添加图片描述
请添加图片描述

僵尸对象的工作原理:

系统在即将回收对象时,如果发现通过环境变量启用了僵尸对象功能,那么还将执行一个附加步骤。这一步就是把对象转化为僵尸对象,而不彻底回收。

僵尸类是怎么产生的:

他其实是在运行期生成的,当首次碰到一个类的对象要变成僵尸对象时,它就会创建这么一个类。僵尸类是从名为_NSZombie_的模版里复制出来的,这些僵尸类没有多少事情可做,只是一个标记,又因为它将原类的方法都拷贝了,所以它会响应原类的方法,不过它会报错提醒程序员。

要点:

  • 系统在回收对象时,可以不将其真的回收,而是把它转化为僵尸对象。通过环境变量NSZombieEnabled 可开启此功能。
  • 系统会修改对象的 isa 指针,令其指向特殊的僵尸类,从而使该对象变为僵尸对象。僵尸类能够响应所有的选择子,响应方式为∶打印一条包含消息内容及其接收者的消息,然后终止应用程序。

36.不要使用retainCount

Objective-C通过引用计数来管理内存(参见第 29条)。每个对象都有一个计数器,其值表明还有多少个其他对象想令此对象继续存活。对象创建好之后,其保留计数大于0。保留与释放操作分别会使该计数递增及递减。当计数变为0时,对象就为系统所回收并摧毁了。
NSObject 协议中定义了下列方法,用于查询对象当前的保留计数∶

-(NSUInteger) retainCount

然而 ARC已经将此方法废弃了。实际上,如果在 ARC中调用,编译器就会报错,这和在 ARC中调用retain、release、autorelease 方法时的情况一样。虽然此方法已经正式废弃了,但还是经常有人误解它,其实这个方法根本就不应该调用。若在不启用ARC的环境下编程(说真的,还是在ARC下编程比较好),那么仍可调用此方法,而编译器不会报错。所以,还是必须讲清楚为何不应使用此方法。
这个方法看上去似乎挺合理、挺有用的。它毕竟返回了保留计数,而此值对每个对象来说显然都很重要。但问题在于,保留计数的绝对数值一般都与开发者所应留意的事情完全无关。即便只在调试时才调用此方法,通常也还是无所助益的。
此方法之所以无用。其首要原因在干∶它所返回的保留计数只是某个给定时间点上的值。该方法并未考虑到系统会稍后把自动释放池清空(参见第 34条).因而不会将后续的释放操作从返回值里减去,这样的话,此值就未必能真实反映实际的保留计数了。因此,下面这种写法非常糟糕∶

while([object retainCount]){
    
    
	[object release];
}

这种写法的第一个错误是∶它没考虑到后续的自动释放操作,只是不停地通过释放操作来降低保留计数,直至对象为系统所回收。假如此对象也在自动释放池里,那么稍后系统清空池子时还要把它再释放一次,而这将导致程序崩溃。
第二个错误在于retainCount 可能永远不返回0,因为有时系统会优化对象的释放行为,在保留计数还是1的时候就把它回收了。只有在系统不打算这么优化时,计数值才会递减至0。因此,保留计数可能永远都不会完全归零。所以说,这段代码就算有时能正常运行,也多半是凭运气,而非理性判断。对象回收之后,如果 while 循环仍在运行,那么目前的运行期系统一般会直接令应用程序崩溃。
从来都不需要编写这种代码。这段代码所要实现的操作,应该通过内存管理来解决。开发者在期望系统于某处回收对象时,应该确保没有尚未抵消的保留操作,也就是不要令保留计数大于期望值。在这种情况下,如果发现某对象的内存泄漏了,那么应该检查还有谁仍然保留这个对象,并查明其为何没有释放此对象。
读者可能还是想看一看保留计数的具体值,然而看过之后你就会觉得奇怪了;它的值为何那么大呢?比方说,有下面这段代码∶

NSString *string = @"Some string";
NSLog(@"string retainCount = %lu",[string retainCount]);
NSNumber *numberI = @1;
NSLog (@"numberI retainCount = %lu",[numberI retainCount]);
NSNumber *numberF = @3.141f;
NSLog (@"numberF retainCount = 8lu",[numberF retainCount]);

在64 位 Mac OS X10.8.2系统中,用Clang 4.1编译后,这段代码输出的消息如下∶

string retainCount = 18446744073709551615 
numberI retainCount = 9223372036854775807 
numberF retainCount = 1

第一个对象的保留计数是2°-1,第二个对象的保留计数是283-1。由于二者皆为"单例对象"(singleton object),所以其保留计数都很大。系统会尽可能把 NSString 实现成单例对象。如果字符串像本例所举的这样,是个编译期常量(compile-time constant),那么就可以这样来实现了。在这种情况下,编译器会把 NSString 对象所表示的数据放到应用程序的二进制文件里,这样的话,运行程序时就可以直接用了,无须再创建 NSString 对象。NSNumber 也类似,它使用了一种叫做"标签指针"(tagged pointer)的概念来标注特定类型的数值。这种做法不使用NSNumber 对象。而是把与数值有关的全部消息都放在指针值里面。运行期系统会在消息派发(参见第11条)期间检测到这种标签指针,并对它执行相应操作,使其行为看上去和真正的 NSNumber 对象一样。这种优化只在某些场合使用,比如范例中的浮点数对象就没有优化,所以其保留计数就是1。
另外,像刚才所说的那种单例对象,其保留计数绝对不会变。这种对象的保留及释放操作都是"空操作"(no-op)。可以看到,即便两个单例对象之间,其保留计数也各不相同,系统对其保留计数的这种处理方式再-一次表明∶我们不应该总是依赖保留计数的具体值来编码。
假如你根据 NSNumber对象的具体保留计数来增减其值。而系统却以标签指针来实现此对象,那么编出来的代码就错了。
那么,只为了调试而使用 retainCount 方法行不行呢?即便只为调试,此方法也不是很有用。由于对象可能处在自动释放池中,所以其保留计数未必如想象般精确。而且其他程序库也有可能自行保留或释放对象,这都会扰乱保留计数的具体取值。看了具体的计数值之后,你可能还误以为是自己的代码修改了它,殊不知其实是由深埋在另外一个程序库中的某段代码所改的。以下列代码为例∶

id object =[self createObject];
[opaqueObject doSomethingWithObject:object];
NSLog (@"retainCount = 8lu",[object retainCount]);

object 的保留计数是多少呢?这个计数可以是任意值。"doSomethingWithObject∶"方法也许会将对象加到多个collection中,而这些 collection均会保留此对象。这个方法还可能会多次保留并自动释放此对象,而其中某些自动释放操作要留待系统稍后清空自动释放池时才执行。因此,保留计数的实际值就不是那么有用了。
那到底何时才应该用retainCount呢?最佳答案是∶绝对不要用,尤其考虑到苹果公司在引入 ARC之后已正式将其废弃,就更不应该用了。

要点:

  • 对象的保留计数看似有用,实则不然,因为任何给定时间点上的"绝对保留计数"都无法反映对象生命期的全貌。
  • 引入 ARC之后,retainCount方法就正式废止了,在 ARC下调用该方法会导致编译器报错。

猜你喜欢

转载自blog.csdn.net/m0_62386635/article/details/128530066
今日推荐