关于Objective-C内存管理及其原理

Posted 江云流

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了关于Objective-C内存管理及其原理相关的知识,希望对你有一定的参考价值。

OC内存管理

看到“引用计数”这个名称,不自觉就会将重点放在计数上。但其实,更加客观、正确的思考方式是:

自己生成的对象,自己所持有。

非自己生成的对象,自己也能持有

不再需要自己持有的对象时释放。

非自己持有的对象无法释放。

引用计数式内存管理的思考方式仅此而已。按照这个思路,完全不必考虑引用计数。

上文出现了“生成”、“持有”、“释放”三个词。而在Objective-C 内存管理中还要加上“废弃”一词。


这些有关Objective-C 内存管理的方法,实际上不包括在该语言中,而是包含在Cocoa 框架中用于OS Xios 应用开发。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 类对象的差异。用这些方法生成的对象,虽然是对象的副本,但同allocnew方法一样,在“自己生成并持有对象”这点上没有改变。

另外,根据上述“使用以下名称开头的方法名”,下列名称也意味着自己生成并持有对象。

● 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 方法进行释放。

如果要用某个方法生成对象,并将其返还给该方法的调用方,那么它的源代码又是怎样的呢?


- idallocObject

{

/*

* 自己生成并持有对象

*/

id obj = [[NSObject alloc] init];

/*

* 自己持有对象

*/

return obj;

}

如上例所示,原封不动地返回用alloc 方法生成并持有的对象,就能让调用方也持有该对象。请注意allocObject 这个名称是符合前文命名规则的。


/*

* 取得非自己生成并持有的对象

*/

id obj1 = [obj0 allocObject];

/*

* 自己持有对象

*/


allocObject 名称符合前文的命名规则,因此它与用alloc 方法生成并持有对象的情况完全相同,所以使用allocObject 方法也就意味着“自己生成并持有对象”。

那么,调用[NSMutableArray array] 方法使取得的对象存在,但自己不持有对象,又是如何实现的呢?根据上文命名规则,不能使用以alloc/new/copy/mutableCopy 开头的方法名,因此要使用object 这个方法名。


- idobject

{

id obj = [[NSObject alloc] init];

/*

* 自己持有对象

*/

[obj autorelease];

/*

* 取得的对象存在,但自己不持有对象

*/

return obj;

}


上例中,我们使用了autorelease 方法。用该方法,可以使取得的对象存在,但自己不持有对象。 autorelease 提供这样的功能,使对象在超出指定的生存范围时能够自动并正确地释放(调用release 方法)。


在后面,对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];

/*

* 释放了非自己持有的对象!

* 这肯定会导致应用程序崩溃!

*/


如这些例子所示,释放非自己持有的对象会造成程序崩溃。因此绝对不要去释放非自己持有的对象。

以上四项内容,就是“引用计数式内存管理”的思考方式。


alloc/retain/release/dealloc 实现


接下来,以Objective-c 内存管理中使用的alloc/retain/release/dealloc 方法为基础,通过实际操作来理解内存管理。

OS XiOS 中的大部分作为开源软件公开在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);


通过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 = NSZoneMalloczone,size);

memsetnew, 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 = sizeofstruct obj_layout + 对象大小;

struct obj_layout *p = struct obj_layout *calloc1,size);

return id)(p+1);

}

alloc 类方法用struct obj_layout 中的retained 整数来保存引用计数,并将其写入对象内存头部,该对象内存块全部置0 后返回。


对象的引用计数可通过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 NSExtraRefCountself + 1;

}

inline NSUInteger

NSExtraRefCountid anObject

{

return ((struct obj_layout *anObject)[-1].retained;

}


由对象寻址找到对象内存头部,从而访问其中的retained 变量。


因为分配时全部置0,所以retained 0。由NSExtraRefCountself + 1 得出,retainCount 1

可以推测出,retain 方法使retained 变量加1,而release 方法使retained 变量减1

[obj retain];

下面来看一下像上面那样调用出的retain 实例方法。

▼▼GNUstep/modules/core/base/Source/NSObject.m retain

- id retain

{

NSIncrementExtraRefCountself);

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 NSDecrementExtraRefCountWasZeroself))

[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];

freeo);

}


上述代码仅废弃由alloc 分配的内存块。

以上就是alloc/retain/release/dealloc GNUstep 中的实现。具体总结如下:


Objective-C的对象中存有引用计数这一整数值。

调用 alloc或是 retain方法后,引用计数值加 1

调用 release后,引用计数值减 1

引用计数值为 0 时,调用 dealloc方法废弃对象。


苹果的实现


在看了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 __CFDoExternRefOperationuintptr_t op, id obj {

CFBasicHashRef table = 取得对象对应的散列表(obj);

int count;

switch op {

case OPERATION_retainCount:

count = CFBasicHashGetCountOfKeytable, obj);

return count;

case OPERATION_retain:

CFBasicHashAddValuetable, obj);

return obj;

case OPERATION_release:

count = CFBasicHashRemoveValuetable, obj);

return 0 == count;

}

}


__CFDoExternRefOperation 函数按retainCount/retain/release 操作进行分发,调用不同的函数。NSObject 类的retainCount/retain/release 实例方法也许如下面代码所示:


- NSUInteger retainCount

{

return NSUInteger__CFDoExternRefOperationOPERATION_retainCount, self);

}

- id retain

{

return id__CFDoExternRefOperationOPERATION_retain, self);

}

- void release

{

return __CFDoExternRefOperationOPERATION_release, self);

}

可以从__CFDoExternRefOperation 函数以及由此函数调用的各个函数名看出,苹果的实现大概就是采用散列表(引用计数表)来管理引用计数。


GNUstep 将引用计数保存在对象占用内存块头部的变量中,而苹果的实现,则是保存在引用计数表的记录中。GNUstep 的实现看起来既简单又高效,而苹果如此实现必然有它的好处。下面我们来讨论一下。


通过内存块头部管理引用计数的好处如下:

少量代码即可完成。

能够统一管理引用计数用内存块与对象用内存块。


通过引用计数表管理引用计数的好处如下:

对象用内存块的分配无需考虑内存块头部。

这里特别要说的是,第二条这一特性在调试时有着举足轻重的作用。即使出现故障导致对象占用的内存块损坏,但只要引用计数表没有被破坏,就能够确认各内存块的位置。


另外,在利用工具检测内存泄漏时,引用计数表的各记录也有助于检测各对象的持有者是否存在。


以上是关于关于Objective-C内存管理及其原理的主要内容,如果未能解决你的问题,请参考以下文章

Objective-c的内存管理MRC与ARC

更具体的通用 Objective-C 内存管理

[精通Objective-C]内存管理

Objective-C的内存管理

Objective-C内存管理之MRC

Objective-C内存管理之MRC