前文:引用计数
请先看我的这篇文章:
引用计数
内存管理
1.1 alloc/retain/release/dealloc的实现
在这里,我们使用开源软件GNUstep(GNUstep的源代码虽不能说与苹果的Cocoa完全相同,胆识从使用者角度来看,两者的行为和实现方法是一样的。)来说明。
alloc实现
在Objective-C中,对象的创建和初始化通常是由类的工厂方法和初始化方法来完成的。这些方法的实现通常包含了为对象分配内存和初始化对象的代码。
以NSObject类的alloc方法为例,其源代码如下:
+ (instancetype)alloc {
return [self allocWithZone:NULL];
}
这个方法是一个类方法,返回一个新分配的对象。它实际上是调用了类的allocWithZone:方法并传入了一个NULL的参数。
而allocWithZone:方法的源代码如下:
+ (instancetype)allocWithZone:(struct _NSZone *)zone {
return NSAllocateObject(self, 0, zone);
}
这个方法也是一个类方法,返回一个新分配的对象。它实际上是调用了NSAllocateObject函数来为对象分配内存。
关于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后简化了的源代码:
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的实现。
retain
对象的引用计数可用retainCount实例方法获得。
下面是GNUstep源代码。
- (NSUInteger) retainCount
{
return NSExtraRefCount (self) + 1;
}
inline NSUInteger
NSExtraRefCount (id anOnject)
{
return ((struct obj_layout *) anObject)[-1].retained;
}
由地址寻找到对象内存头部,从而访问其中的retained变量。
因为分配时全部置0,所以retained 为0。由NSExtraRefCount(self)+1得出,retainCount为1。可以推测出retain方法使retained变量加1,而release方法使retained变量减1。
[obj retain];
下面看一下像上面那样调用出的retain实例方法。
- (id) retain
{
NSIncermentExtraRefCount(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++;
}
这段代码实现了 Objective-C 对象的引用计数机制中 retain 方法的具体实现。当我们调用一个对象的 retain 方法时,其引用计数值会加1,表示有一个新的对象持有了该对象的引用,从而防止该对象在被其它对象持有的引用都被释放后被系统回收。
具体实现中,retain 方法内部会调用 NSIncrementExtraRefCount 函数来实现引用计数的增加。NSIncrementExtraRefCount 函数会将传入的对象的 retained 域加1,该域存储了当前对象被引用的次数。如果 retained 域已经达到最大值(即 UINT_MAX - 1),则会抛出一个内部不一致的异常。
在这段代码中,通过将传入的对象指针 anObject 转换为结构体指针 ((struct obj_layout *) anObject),然后访问其前一个元素 [-1],即访问对象所在内存块的前一个元素,这个前一个元素就是对象的布局结构体,通过访问这个布局结构体的 retained 域,可以得到当前对象的引用计数值,进而实现对其进行增加的操作。
release
以下为release方法的实现。
- (id) release
{
if (NSDecrementExtraRefCountWasZero(self))
[self dealloc];
}
BOOL
NSDecrementExtraRefCountWasZero(id anObject)
{
if (((struct obj_laout *) anObject)[-1].retained == 0) {
return YES;
} else {
((struct obj_laout *) anObject)[-1].retained--;
return NO;
}
}
这段代码实现了 Objective-C 对象的引用计数机制中 release 方法的具体实现。当retained变量大于0时减1,等于0时调用dealloc实例方法,废弃对象。
具体引用计数的访问与retain方法相同。
dealloc
以下为delloc实例方法的实现。
- (void)dealloc
{
NSDealloocateObject(self);
}
inline void
NSDeallocateObject(id anObject)
{
struct obj_layout *o = &((struct obj_layout *)anObject0[-1];
free(o);
}
上述代码仅废弃由alloc分配的代码块。
1.2 苹果的实现
因为NSObject类的源代码没有公开,此处利用Xcode的调试器和iOS大概追溯出其实现过程。
在NSObject类的alloc类方法上设置断点,追踪程序的执行。以下列出了执行所调用的方法和函数。
+alloc
+allocWithZone:
class_createInstance
calloc
其中:
class_createInstance 是一个 Objective-C Runtime API,用于创建一个指定类的实例对象。它的声明如下:
id class_createInstance(Class cls, size_t extraBytes);
其中,cls 是要创建实例对象的类,extraBytes 是要为这个对象分配的额外字节数,通常情况下这个参数为 0。函数返回一个指向新创建实例对象的指针。
其中调用了calloc函数分配内存块。
同样的方法,看一下retainCout/retain/release实例方法实现。
-retainCount
__CFDoExternRefOperation
CFBasicHashGetCountOfKey
-retain
__CFDoExternRefOperation
CFBasicHashAddValue
-release
__CFDoExternRefOperation
CFBasicHashRemoveValue
(CFBasicHashRemoveValue 返回0时,-release调用dealloc)
其中:
__CFDoExternRefOperation 是 Core Foundation 框架中的一个 C 函数,用于执行外部引用计数操作。在 Core Foundation 中,有一类对象叫做外部引用对象(External References),这些对象的引用计数不由 Core Foundation 管理,而是由外部程序来管理。当 Core Foundation 操作这些对象时,需要调用外部程序提供的函数来对对象的引用计数进行操作,这就是 __CFDoExternRefOperation 函数的作用。
__CFDoExternRefOperation 函数的声明如下:
void __CFDoExternRefOperation(CFTypeRef cf, void (*op)(CFTypeRef cf1, void *context), void *context, Boolean isDeallocating);
该函数接收四个参数:
cf:外部引用对象的指针。
op:指向外部程序提供的引用计数操作函数的指针。这个函数接收两个参数,第一个参数是外部引用对象的指针,第二个参数是上下文信息指针。
context:指向上下文信息的指针,传递给 op 函数。
isDeallocating:表示是否正在释放对象的标志。
在 __CFDoExternRefOperation 函数中,我们首先对传入的参数进行了非空判断,然后根据 isDeallocating 参数的值选择调用外部程序提供的引用计数操作函数 op 的不同版本。如果 isDeallocating 参数为 true,就调用 op 函数的释放版本,否则就调用 op 函数的保留版本。在调用 op 函数之前,我们先对对象进行了锁定操作,防止在操作期间发生对象被释放的情况。
需要注意的是,__CFDoExternRefOperation 函数只是一个内部函数,外部程序一般不会直接调用它。外部程序通常会使用 Core Foundation 提供的一些宏或函数来管理外部引用对象的引用计数,例如 CFMakeExternalRefCounted()、CFRetain() 和 CFRelease() 等函数。
下面是简化了的__CFDoExternRefOperation函数的源代码。
int __CFDoExternRefOperation(uintptr_t op, id obj) {
CFBasicHashRef table = 取得对像对应的散列表(obj);
int count;
switch(op) {
case OPERATTON_retainCount:
count = CFBasicHashGetCountOfKey(table, obj);
return count;
case OPERATTON_retain:
CFBasicHashAddValue(table, obj);
return obj;
case OPERATTON_release:
count = CFBasicHashRemoveValue(table, obj);
return 0 == count;
}
}
__CFDoExternRefOperation函数按retainCount/retain/release操作进行分发,调用不同的函数。NSObject类的retainCount/retain/release实例方法也许如下面代码所示。
- (NSUInteger) retainCount
{
return (NSInteger) __CFDoExternRefOperation(OPERATTON_retainCount, self);
}
- (id) retain
{
return (id) __CFDoExternRefOperation(OPERATTON_retain, self);
}
- (void) release
{
return __CFDoExternRefOperation(OPERATTON_release, self);
}
可以从__CFDoExternRefOperation函数以及由此函数调用的各个函数名来看,苹果的实现大概就是采用散列表(引用计数表)来管理引用计数。如图:
这样实现与GNUstep相比的好处:
通过内存块头部管理引用计数的好处如下:
- 少量代码即可完成
- 能够统一管理引用计数内存块与对象的内存块
通过引用计数表管理引用计数的好处如下:
- 对象用内存块的分配无需考虑内存块头部。
- 引用计数表各记录中存有内存地址,可从各个记录追溯到各对象的内存块。
另外,在利用工具检测内存泄露时,引用计数表的各记录也有助于检测各对象的持有者是否存在。
1.3 autorelease
autorelease方法可以使取得的对象存在,但自己不持有对象。autorelease提供这样的功能,是对象在超出指定的生存范围时能够自动并正确地释放(release方法)。
另外,编程人员可以设定变量的作用域。
autrelease的具体使用方法如下:
- 生成并持有NSAutoreleasePool对象;
- 调用已分配对象的autorrelease实例方法;
- 废弃NSAutoreleasePoll对象。
在Cocoa框架中,相当于程序主循环的NSRunLoop或者在其他程序可运行的地方,对NSAutoreleasePool对象进行生成、持有和废弃处理。
1.4 autorelease实现
autorelease的实现。GNUstep的源代码。
[obj autorelease];
此源代码调用NSObject类的autorelease实例方法。
- (id) autorelease
{
[NSAutoreleasePool addObject:self];
}
autorelease实例方法的本质就是调用NSAutoreleasePool对象的addObject类方法。
下面看NSAutoreleasePool类的实现。由于NSAutoreleasePool类的源代码比较复杂,所以我们假象一个简化的源代码进行说明。
+ (void) addObject:(id)anObj
{
NSAutoreleasePool *pool = 取得正在使用的NSAutoreleasePool对象;
if (pool != nil) {
[pool addObject:anObj];
} else {
NSLog(@"NSAutoreleasePool对象非存在状态下调用autorelease");
}
}
addObject类方法调用正在使用的NSAutoreleasePool对象的addobject实例方法。以下源代码中,被赋予pool变量的即为正在使用的NSAutoreleasePool对象。
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
id obj = [[NSObject alloc] init];
[obj autorelease];
如果嵌套生成或持有的NSAutoreleasePool对象,理所当然会使用最内侧的对象。下例中,pool2为正在使用的NSAutoreleasePool对象。
NSAutoreleasePool *pool0 = [[NSAutoreleasePool alloc] init];
NSAutoreleasePool *pool1 = [[NSAutoreleasePool alloc] init];
NSAutoreleasePool *pool2 = [[NSAutoreleasePool alloc] init];
id obj = [[NSObject alloc] init];
[obj autorelease];
[pool2 drain];
[pool1 drain];
[pool0 drain];
下面看一下addObject实例方法的实现。
- (void) add object:(id) anObj
{
[array addObject:anObj];
}
实际的GNUstep实现使用的是连接列表,这同在NSMutableArray对象中追加对象参数是一样的。
如果调用NSObject类的autorelease实例方法,该对象将被追加到正在使用的NSAutoreleasePool对象的数组里。
[pool drain];
以下为通过drain实例方法废弃正在使用的NSAutoreleasePool对象的过程。
- (void) drain
{
[self dealloc];
}
- (void) dealloc
{
[self emptyPool];
[array release];
}
- (void) emtyPool
{
for (id obj in array) {
[obj release];
}
}