Objective-C高级编程1.2 内存管理/ 引用计数_Objective-C高级编程1.2 内存管理/ 引用计数试读-查字典图书网
查字典图书网
当前位置: 查字典 > 图书网 > 编程 > Objective-C高级编程 > 1.2 内存管理/ 引用计数

Objective-C高级编程——1.2 内存管理/ 引用计数

1.2.1 概要 Objective-C 中的内存管理,也就是引用计数。可以用开关房间的灯为例来说明引用计数的机制,如图1-1 所示。 解决这一问题的办法是使办公室在还有至少1 人的情况下保持开灯状态,而在无人时保持关灯状态。 (1)最早进入办公室的人开灯。 (2)之后进入办公室的人,需要照明。 (3)下班离开办公室的人,不需要照明。 (4)最后离开办公室的人关灯(此时已无人需要照明)。 为判断是否还有人在办公室里,这里导入计数功能来计算“需要照明的人数”。下面让我们来看看这一功能是如何运作的吧。 (1)第一个人进入办公室,“需要照明的人数”加1。计数值从0 变成了1,因此要开灯。 (2)之后每当有人进入办公室,“需要照明的人数”就加1。如计数值从1 变成2。 (3)每当有人下班离开办公室,“需要照明的人数”就减1。如计数值从2 变成1。 (4)最后一个人下班离开办公室时,“需要照明的人数”减1。计数值从1 变成了0,因此要关灯。 这样就能在不需要照明的时候保持关灯状态。办公室中仅有的照明设备得到了很好的管理,如图1-3 所示。 在Objective-C 中,“对象”相当于办公室的照明设备。在现实世界中办公室的照明设备只有一个,但在Objective-C 的世界里,虽然计算机资源有限,但一台计算机可以同时处理好几个对象。 此外,“对象的使用环境”相当于上班进入办公室的人。虽然这里的“环境”有时也指在运行中的程序代码、变量、变量作用域、对象等,但在概念上就是使用对象的环境。上班进入办公室的人对办公室照明设备发出的动作,与Objective-C 中的对应关系则如表1-1 所示。 使用计数功能计算需要照明的人数,使办公室的照明得到了很好的管理。同样,使用引用计数功能,对象也就能够得到很好的管理,这就是Objective-C 的内存管理。如图1-4 所示。 现在对Objective-C 的内存管理多少理解一些了吧。下面,我们将学习“引用计数式内存管理”的思考方式,并在此基于实现进一步加深理解。 1.2.2 内存管理的思考方式 首先来学习引用计数式内存管理的思考方式。看到“引用计数”这个名称,我们便会不自觉地联想到“某处有某物多少多少”而将注意力放到计数上。但其实,更加客观、正确的思考方式是: ● 自己生成的对象,自己所持有。 ● 非自己生成的对象,自己也能持有 ● 不再需要自己持有的对象时释放。 ● 非自己持有的对象无法释放。 引用计数式内存管理的思考方式仅此而已。按照这个思路,完全不必考虑引用计数。 上文出现了“生成”、“持有”、“释放”三个词。而在Objective-C 内存管理中还要加上“废弃”一词,这四个词将在本书中频繁出现。各个词表示的Objective-C 方法如表1-2。 这些有关Objective-C 内存管理的方法,实际上不包括在该语言中,而是包含在Cocoa 框架中用于OS X、iOS 应用开发。Cocoa 框架中Foundation 框架类库的NSObject 类担负内存管理的职责。Objective-C 内存管理中的alloc/retain/release/dealloc 方法分别指代NSObject 类的alloc 类方法、retain 实例方法、release 实例方法和dealloc 实例方法。 接着来详细了解“内存管理的思考方式”中出现的各个项目。 自己生成的对象,自己所持有使用以下名称开头的方法名意味着自己生成的对象只有自己持有: ● alloc ● new ● copy ● mutableCopy 上文出现了很多“自己”一词。本书所说的“自己” 固然对应前文提到的“对象的使用环境”,但将之理解为编程人员“自身”也是没错的。下面写出了自己生成并持有对象的源代码。为生成并持有对象,我们使用alloc 方法。 /* * 自己生成并持有对象 */ id obj = [[NSObject alloc] init]; /* * 自己持有对象 */ 使用NSObject 类的alloc 类方法就能自己生成并持有对象。指向生成并持有对象的指针被赋给变量obj。另外,使用如下new 类方法也能生成并持有对象。[NSObject new] 与[[NSObjectalloc] init] 是完全一致的。 /* * 自己生成并持有对象 */ id obj = [NSObject new]; /* * 自己持有对象 */ copy 方法利用基于NSCopying 方法约定,由各类实现的copyWithZone :方法生成并持有对象的副本。与copy 方法类似,mutableCopy 方法利用基于NSMutableCopying 方法约定,由各类实现的mutableCopyWithZone :方法生成并持有对象的副本。两者的区别在于,copy 方法生成不可变更的对象,而mutableCopy 方法生成可变更的对象。这类似于NSArray 类对象与 NSMutableArray 类对象的差异。用这些方法生成的对象,虽然是对象的副本,但同alloc、new方法一样,在“自己生成并持有对象”这点上没有改变。 另外,根据上述“使用以下名称开头的方法名”,下列名称也意味着自己生成并持有对象。 ● allocMyObject ● newThatObject ● copyThis ● mutableCopyYourObject 但是对于以下名称,即使用alloc/new/copy/mutableCopy 名称开头,并不属于同一类别的方法。 ● allocate ● newer ● copying ● mutableCopyed 这里用驼峰拼写法(CamelCase①)来命名。 非自己生成的对象,自己也能持有用上述项目之外的方法取得的对象,即用alloc/new/copy/mutableCopy 以外的方法取得的对象,因为非自己生成并持有,所以自己不是该对象的持有者。我们来使用alloc/new/copy/mutableCopy 以外的方法看看。这里试用一下NSMutableArray 类的array 类方法。 /* * 取得非自己生成并持有的对象 */ id obj = [NSMutableArray array]; /* * 取得的对象存在,但自己不持有对象 */ 源代码中,NSMutableArray 类对象被赋给变量obj,但变量obj 自己并不持有该对象。使用retain 方法可以持有对象。 /* * 取得非自己生成并持有的对象 */ id obj = [NSMutableArray array]; /* * 取得的对象存在,但自己不持有对象 */ [obj retain]; /* * 自己持有对象 */ 通过retain 方法,非自己生成的对象跟用alloc/new/copy/mutableCopy 方法生成并持有的对象一样,成为了自己所持有的。 不再需要自己持有的对象时释放自己持有的对象,一旦不再需要,持有者有义务释放该对象。释放使用release 方法。 /* * 自己生成并持有对象 */ id obj = [[NSObject alloc] init]; /* * 自己持有对象 */ [obj release]; /* * 释放对象 * * 指向对象的指针仍然被保留在变量obj 中,貌似能够访问, * 但对象一经释放绝对不可访问。 */ ................................................................................................ ①a 驼峰拼写法是将第一个词后每个词的首字母大写来拼写复合词的记法。例如CamelCase 等。 ................................................................................................ 如此,用alloc 方法由自己生成并持有的对象就通过release 方法释放了。自己生成而非自己所持有的对象,若用retain 方法变为自己持有,也同样可以用release 方法释放。 /* * 取得非自己生成并持有的对象 */ id obj = [NSMutableArray array]; /* * 取得的对象存在,但自己不持有对象 */ [obj retain]; /* * 自己持有对象 */ [obj release]; /* * 释放对象 * 对象不可再被访问 */ 用alloc/new/copy/mutableCopy 方法生成并持有的对象,或者用retain 方法持有的对象,一旦不再需要,务必要用release 方法进行释放。 如果要用某个方法生成对象,并将其返还给该方法的调用方,那么它的源代码又是怎样的呢? - (id)allocObject { /* * 自己生成并持有对象 */ id obj = [[NSObject alloc] init]; /* * 自己持有对象 */ return obj; } 如上例所示,原封不动地返回用alloc 方法生成并持有的对象,就能让调用方也持有该对象。请注意allocObject 这个名称是符合前文命名规则的。 /* * 取得非自己生成并持有的对象 */ id obj1 = [obj0 allocObject]; /* * 自己持有对象 */ allocObject 名称符合前文的命名规则,因此它与用alloc 方法生成并持有对象的情况完全相同,所以使用allocObject 方法也就意味着“自己生成并持有对象”。 那么,调用[NSMutableArray array] 方法使取得的对象存在,但自己不持有对象,又是如何实现的呢?根据上文命名规则,不能使用以alloc/new/copy/mutableCopy 开头的方法名,因此要使用object 这个方法名。 - (id)object { id obj = [[NSObject alloc] init]; /* * 自己持有对象 */ [obj autorelease]; /* * 取得的对象存在,但自己不持有对象 */ return obj; } 上例中,我们使用了autorelease 方法。用该方法,可以使取得的对象存在,但自己不持有对象。 autorelease 提供这样的功能,使对象在超出指定的生存范围时能够自动并正确地释放(调用release 方法)。如图1-6 所示。 在后面,对autorelease 做了更为详细的解说,具体可参看1.2.5 节。使用NSMutableArray类的array 类方法等可以取得谁都不持有的对象,这些方法都是通过autorelease 而实现的。此外,根据上文的命名规则,这些用来取得谁都不持有的对象的方法名不能以alloc/new/copy/mutableCopy 开头,这点需要注意。 id obj1 = [obj0 object]; /* * 取得的对象存在,但自己不持有对象 */ 当然,也能够通过retain 方法将调用autorelease 方法取得的对象变为自己持有。 id obj1 = [obj0 object]; /* * 取得的对象存在,但自己不持有对象 */ [obj1 retain]; /* * 自己持有对象 */ 无法释放非自己持有的对象对于用alloc/new/copy/mutableCopy 方法生成并持有的对象,或是用retain 方法持有的对象,由于持有者是自己,所以在不需要该对象时需要将其释放。而由此以外所得到的对象绝对不能释放。倘若在应用程序中释放了非自己所持有的对象就会造成崩溃。例如自己生成并持有对象后,在释放完不再需要的对象之后再次释放。 /* * 自己生成并持有对象 */ id obj = [[NSObject alloc] init]; /* * 自己持有对象 */ [obj release]; /* * 对象已释放 */ [obj release]; /* * 释放之后再次释放已非自己持有的对象! * 应用程序崩溃! * * 崩溃情况: * 再度废弃已经废弃了的对象时崩溃 * 访问已经废弃的对象时崩溃 */ 或者在“取得的对象存在,但自己不持有对象”时释放。 id obj1 = [obj0 object]; /* * 取得的对象存在,但自己不持有对象 */ [obj1 release]; /* * 释放了非自己持有的对象! * 这肯定会导致应用程序崩溃! */ 如这些例子所示,释放非自己持有的对象会造成程序崩溃。因此绝对不要去释放非自己持有的对象。 以上四项内容,就是“引用计数式内存管理”的思考方式。 1.2.3 alloc/retain/release/dealloc 实现 接下来,以Objective-c 内存管理中使用的alloc/retain/release/dealloc 方法为基础,通过实际操作来理解内存管理。 OS X、iOS 中的大部分作为开源软件公开在Apple Open Source① 上。虽然想让大家参考NSObject 类的源代码,但是很遗憾,包含NSObject 类的Foundation 框架并没有公开。不过,Foundation 框架使用的Core Foundation 框架的源代码,以及通过调用NSObject 类进行内存管理部分的源代码是公开的。但是,没有NSObject 类的源代码,就很难了解NSObject 类的内部实现细节。为此,我们首先使用开源软件GNUstep② 来说明。 GNUstep 是Cocoa 框架的互换框架。也就是说,GNUstep 的源代码虽不能说与苹果的Cocoa实现完全相同,但是从使用者角度来看,两者的行为和实现方式是一样的,或者说非常相似。理解了GNUstep 源代码也就相当于理解了苹果的Cocoa 实现。 我们来看看GNUstep 源代码中NSObject 类的alloc 类方法。为明确重点,有的地方对引用的源代码进行了摘录或在不改变意思的范围内进行了修改。 id obj = [NSObject alloc]; 上述调用NSObject 类的alloc 类方法在NSObject.m 源代码中的实现如下。 ▼▼GNUstep/modules/core/base/Source/NSObject.m alloc + (id) alloc { return [self allocWithZone: NSDefaultMallocZone()]; } + (id) allocWithZone: (NSZone*)z { return NSAllocateObject (self, 0, z); .................................................................................................. ① Apple Open Source http://opensource.apple.com/。 ②GNUstep http://gnustep.org/。 .................................................................................................. 通过allocWithZone :类方法调用NSAllocateObject 函数分配了对象。下面我们来看看NSAllocateObject 函数。 ▼▼GNUstep/modules/core/base/Source/NSObject.m NSAllocateObject struct obj_layout { NSUInteger retained; }; inline id NSAllocateObject (Class aClass, NSUInteger extraBytes, NSZone *zone) { int size = 计算容纳对象所需内存大小; id new = NSZoneMalloc(zone,size); memset(new, 0, size); new = (id)&((struct obj_layout *)new)[1]; } NSAllocateObject 函数通过调用NSZoneMalloc 函数来分配存放对象所需的内存空间,之后将该内存空间置0,最后返回作为对象而使用的指针。 以下是去掉NSZone 后简化了的源代码: ▼▼GNUstep/modules/core/base/Source/NSObject.m alloc简化版 struct obj_layout { NSUInteger retained; }; + (id) alloc { int size = sizeof(struct obj_layout) + 对象大小; struct obj_layout *p = (struct obj_layout *)calloc(1,size); return (id)(p+1); } alloc 类方法用struct obj_layout 中的retained 整数来保存引用计数,并将其写入对象内存头部,该对象内存块全部置0 后返回。以下用图示来展示有关GNUstep 的实现,alloc 类方法返回的对象。如图1-8 所示。 对象的引用计数可通过retainCount 实例方法取得。 id obj = [[NSObject alloc] init]; NSLog(@"retainCount=%d", [obj retainCount]); /* * 显示retainCount=1 */ 执行alloc 后对象的retainCount 是“1”。下面通过GNUstep 的源代码来确认。 ▼▼GNUstep/modules/core/base/Source/NSObject.m retainCount - (NSUInteger) retainCount { return NSExtraRefCount(self) + 1; } inline NSUInteger NSExtraRefCount(id anObject) { return ((struct obj_layout *)anObject)[-1].retained; } 由对象寻址找到对象内存头部,从而访问其中的retained 变量。如图1-9 所示。 因为分配时全部置0,所以retained 为0。由NSExtraRefCount(self) + 1 得出,retainCount 为1。 可以推测出,retain 方法使retained 变量加1,而release 方法使retained 变量减1。 [obj retain]; 下面来看一下像上面那样调用出的retain 实例方法。 ▼▼GNUstep/modules/core/base/Source/NSObject.m retain - (id) retain { NSIncrementExtraRefCount(self); return self; } inline void NSIncrementExtraRefCount (id anObject) { if (((struct obj_layout *)anObject)[-1].retained == UINT_MAX – 1) [NSException raise: NSInternalInconsistencyException format: @"NSIncrementExtraRefCount() asked to increment too far"); ((struct obj_layout *)anObject)[-1].retained++; } 虽然写入了当retained 变量超出最大值时发生异常的代码,但实际上只运行了使retained 变量加1 的retained++ 代码。同样地,release 实例方法进行retained-- 并在该引用计数变量为0 时做出处理。下面通过源代码来确认。 [obj release]; 以下为此release 实例方法的实现。 ▼▼GNUstep/modules/core/base/Source/NSObject.m release - (void) release { if (NSDecrementExtraRefCountWasZero(self)) [self dealloc]; } BOOL NSDecrementExtraRefCountWasZero (id anObject) { if (((struct obj_layout *)anObject)[-1].retained == 0) { return YES; } else { ((struct obj_layout *)anObject)[-1].retained--; return NO; } } 同预想的一样,当retained 变量大于0 时减1,等于0 时调用dealloc 实例方法,废弃对象。 以下是废弃对象时所调用的dealloc 实例方法的实现。 ▼▼GNUstep/modules/core/base/Source/NSObject.m dealloc - (void) dealloc { NSDeallocateObject (self); } inline void NSDeallocateObject (id anObject) { struct obj_layout *o = &((struct obj_layout *)anObject)[-1]; free(o); } 上述代码仅废弃由alloc 分配的内存块。 以上就是alloc/retain/release/dealloc 在GNUstep 中的实现。具体总结如下: ● 在 Objective-C的对象中存有引用计数这一整数值。 ● 调用 alloc或是 retain方法后,引用计数值加 1。 ● 调用 release后,引用计数值减 1。 ● 引用计数值为 0 时,调用 dealloc方法废弃对象。 1.2.4 苹果的实现 在看了GNUstep 中的内存管理和引用计数的实现后, 我们来看看苹果的实现。因为NSObject 类的源代码没有公开,此处利用Xcode 的调试器(lldb)和iOS 大概追溯出其实现过程。 在NSObject 类的alloc 类方法上设置断点,追踪程序的执行。以下列出了执行所调用的方法和函数。 +alloc +allocWithZone: class_createInstance calloc alloc 类方法首先调用allocWithZone: 类方法,这和GNUstep 的实现相同,然后调用class_createInstance 函数①,该函数在Objective-C 运行时参考中也有说明,最后通过调用calloc 来分配 内存块。这和前面讲述的GNUstep 的实现并无多大差异。class_createInstance 函数的源代码可以 通过objc4 库②中的runtime/objc-runtime-new.mm 进行确认。 retainCount/retain/release 实例方法又是怎样实现的呢?同刚才的方法一样,下面列出各个方法分别调用的方法和函数。 -retainCount __CFDoExternRefOperation CFBasicHashGetCountOfKey -retain __CFDoExternRefOperation CFBasicHashAddValue -release __CFDoExternRefOperation CFBasicHashRemoveValue (CFBasicHashRemoveValue 返回0 时,-release 调用dealloc) 各个方法都通过同一个调用了__CFDoExternRefOperation 函数, 调用了一系列名称相似的函数。如这些函数名的前缀“CF”所示, 它们包含于Core Foundation 框架源代码中,即是CFRuntime.c 的__CFDoExternRefOperation 函数。为了理解其实现,下面简化了__ CFDoExternRefOperation 函数后的源代码。 ▼▼CF/CFRuntime.c __CFDoExternRefOperation int __CFDoExternRefOperation(uintptr_t op, id obj) { CFBasicHashRef table = 取得对象对应的散列表(obj); int count; switch (op) { case OPERATION_retainCount: ................................................................................................ ①http://developer.apple.com/library/ios/#documentation/Cocoa/Reference/ObjCRuntimeRef/reference.html。 ②http://www.opensource.apple.com/source/objc4/。 ................................................................................................ count = CFBasicHashGetCountOfKey(table, obj); return count; case OPERATION_retain: CFBasicHashAddValue(table, obj); return obj; case OPERATION_release: count = CFBasicHashRemoveValue(table, obj); return 0 == count; } } __CFDoExternRefOperation 函数按retainCount/retain/release 操作进行分发,调用不同的函数。NSObject 类的retainCount/retain/release 实例方法也许如下面代码所示: - (NSUInteger) retainCount { return (NSUInteger)__CFDoExternRefOperation(OPERATION_retainCount, self); } - (id) retain { return (id)__CFDoExternRefOperation(OPERATION_retain, self); } - (void) release { return __CFDoExternRefOperation(OPERATION_release, self); } 可以从__CFDoExternRefOperation 函数以及由此函数调用的各个函数名看出,苹果的实现大概就是采用散列表(引用计数表)来管理引用计数。如图1-10 所示。 GNUstep 将引用计数保存在对象占用内存块头部的变量中,而苹果的实现,则是保存在引用计数表的记录中。GNUstep 的实现看起来既简单又高效,而苹果如此实现必然有它的好处。下面我们来讨论一下。 通过内存块头部管理引用计数的好处如下: ● 少量代码即可完成。 ● 能够统一管理引用计数用内存块与对象用内存块。 通过引用计数表管理引用计数的好处如下: ● 对象用内存块的分配无需考虑内存块头部。 ● 引用计数表各记录中存有内存块地址,可从各个记录追溯到各对象的内存块。 这里特别要说的是,第二条这一特性在调试时有着举足轻重的作用。即使出现故障导致对象占用的内存块损坏,但只要引用计数表没有被破坏,就能够确认各内存块的位置。如图1-11 所示。 另外,在利用工具检测内存泄漏时,引用计数表的各记录也有助于检测各对象的持有者是否存在。 通过以上解说即可理解苹果的实现。

展开全文

推荐文章

猜你喜欢

附近的人在看

推荐阅读

拓展阅读

《Objective-C高级编程》其他试读目录

• 1.1 什么是自动引用计数
• 1.2 内存管理/ 引用计数 [当前]