自动引用计数
- 前言
- alloc/retain/release/dealloc实现
- 苹果的实现
- autorelease
- autorelease实现
- 苹果的实现
- 总结
前言
此前,写过一遍对自动引用计数的简单学习,因此掠过其中相同的部分:引用计数初步学习
alloc/retain/release/dealloc实现
由于NSObject类的源码不公开,我们通过开源软件GNUstep来学习相关内容。
GNUstep是Cocoa框架的互换框架,虽然并不是与苹果Cocoa的实现方式完全相同,但是从使用者的角度来看二者的行为和实现方式是一样的,理解了GNUstep的源代码也相当于理解了苹果的Cocoa实现。
先来看alloc类方法。
id obj = [NSObject alloc];
上述调用NSObject类的alloc类方法在NSObject.m的源代码中的实现如下:
+ (id) alloc
{
return [self allocWithZone: NSDefaultMallocZone()];
}
+ (id) allocWithZone: (NSZone*) z
{
return NSAllocateObject(self, 0, z);
}
上述代码使用了在allocWithZone
类方法中,使用NSAllocateOject
函数来分配对象。下面是这个函数的代码:
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];
}
NSAllocateOject
函数通过调用NSZoneMalloc
函数来进行分配存放对象所需的内存空间,之后将该内存空间置0,最后返回作为对象而使用的指针。
NSZone是为了防止内存碎片化而引入的结构。对内存分配的区域本身进行多重化管理,根据使用对象的目的和大小分配内存,从而提高内存管理的效率。
因此简化NSZone方法后的alloc源代码如下:
// 定义 obj_layout 结构体,用于存储引用计数
struct obj_layout {
NSUInteger retained;
};
// 实现 alloc 方法
+ (id)alloc {
// 计算对象所需的内存大小
int size = sizeof(struct obj_layout) + class_getInstanceSize([self class]);
// 分配内存并初始化为 0
struct obj_layout *p = (struct obj_layout *)calloc(1, size);
// 返回指向对象内存的指针(跳过 obj_layout)
return (id)(p + 1);
}
alloc类方法使用obj_layout来保存对象的引用计数,记录在retain字段中,并将其写入对象内存头部,该对象全部置0后返回。alloc后返回的对象如图所示:
alloc后对象引用计数加一。下面是GNUstep源码:
-(NSUInterger) retainCount
{
return NSextraRefCount (self) + 1;
}
inline NSUInteger
NSExtraRefCount(id anObject)
{
return ((struct obj_layout *)anObject) [-1].retained;
}
这里需要由对象寻址找到对象内存头部,访问其中的retained变量。
retain方法使retained变量加1,release方法使retained变量减1。
下面是retain和release的源码:
retain源码:
retain的实例方法中是调用NSIncrementExtraRefCount
函数,该函数的作用是使retained加1。并且为该变量超出最大值做出处理。
release源码:
release
方法先调用NSDecrementExtraRefCountWasZero
函数,该函数的作用是让retained一直减到0。减到0后调用dealloc方法。废弃该对象。
上述代码仅废弃由alloc分配的内存块。
苹果的实现
使用NSObject类的alloc方法时,调用以下方法和函数
- +alloc
- +allocWithZone
- class_createInstance
- calloc
这个调用过程与前文所讲GNUstep相似,先调用allocWithZone
方法,在调用class_createInstance函数,最后通过调用calloc来分配内存块。
接下来看retainCount/retain/release实例方法如何实现:
每个方法都调用了同一个函数_CFDoExternRefOperation
该函数的前缀“CF“表明,它们包含于Core Foundation框架源代码中。我们来看其简化后的源码:
int __CFDoExternRefOperation(uintptr_t op, id obj) {
CFBasicHashRef table = 取得对象对应的散列表(obj);
int count;
switch(op) {
case OPERATION_retainCount:
count = CFBasicHashGetCountOfKey(table, obj);
return count;
case OPERATION_retain:
CFBasicHashAddValue(table, obj);
return obj;
case OPERATION_release:
count = CFBasicHashRemoveValue(table, obj);
return 0 == count;
}
}
该函数按retainCount/retain/release操作进行分发,调用不同的函数。他们的实例方法可能如下所示:
- (NSUInteger)retainCount {
return (NSUInteger)__CFDoExternRefOperation(OPERATION_retain, self);
}
- (id)retain {
return (id)__CFDoExternRefOperation(OPERATION_retain, self);
}
- (void)release {
return __CFDoExternRefOperation(OPERATION_release, self);
}
可以从__CFDoExternRefOperation
函数以及由此函数调用的各个函数名中看出,苹果的实现大概采用散列表(引用计数表)来管理引用计数。如图所示:
GNUstep和苹果在实现引用计数的保存上有所不同
- GNUstep将引用计数保存在对象占用内存头部的变量
少量代码即可完成,能够统一管理引用计数内存块与对象用内存块
- 苹果保存在引用计数表中记录
对象用内存块的分配无需考虑内存块头部。
引用计数表各记录中存有内存块地址,可从各个记录中追溯到各对象的内存块。
这一点在调试故障时非常有效。即使出故障导致对象占用的内存块损坏,但只要引用计数表没有破坏,就能够确认各块内存块的位置。
autorelease
autorelease是自动释放,虽然看上去像ARC,但实际上更类似于C语言中的自动变量(局部变量)的特性。
autorelease会像C语言的局部变量那样对待对象实例。当超出其作用域时,对象的release实例方法才被调用。但是不同的是,autorelease可以设定变量的作用域。具体使用方法如下:
- 生成并持有NSAutoreleasePool对象
- 调用已分配对象的autorelease方法
- 废弃NSAutoreleasePool方法
NSAutoreleasePool对象的生存周期相当于C语言变量的作用域。对于所有调用过autorelease实例方法的对象,在废弃NSAutoreleasePool对象时,都将调用release实例方法。如图所示:
源代码如下:
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
id obj = [[NSObject alloc] init];
[obj autoreless];
[pool drain];
其中,[pool drain]
方法等同于[obj release]
。
在Cocoa框架中,相当于程序主循环的NSRunLoop或者在其他程序可运行的地方,对NSAutoreleasePool对象进行生成,持有,和废弃处理。
但是,只要不废弃NSAutoreleasePool对象,那么生成的对象就不能释放,因此会产生内存不足的现象。我们只需要自定义一个pool,在最后进行[pool drain]
。
for (int i = 0; i < image.count; i++) {
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]];
/*
产生autorelease对象
*/
[pool drain]
}
}
许多类方法也会返回autorelease对象,如NSMutableArray类的arrayWithCapacity类方法。
autorelease实现
我们先来看一下autorelease源码
-(id) autorelease
{
[NSAutoreleasePool addObject:self];
}
该方法的本质就是调用NSAutoreleasePool对象的addObject类方法。
IMP Caching
当我们调用一个方法时,OC通过消息传递机制实现 :
- 查找方法名(selector)。
- 找到方法的实现(IMP,即函数指针)。
- 执行方法。
这个过程非常灵活,但是如果需要频繁的调用方法,如上文的autorelease
方法,那么则会带来一定的性能开销。
为了减少这种开销,我们则采用IMP Caching技术:
- 在程序初始化时,预先查找并缓存方法的实现(IMP)。
- 在后续调用时,直接使用缓存的 IMP,避免重复查找。
通常情况下,IMP Caching的速度是普通方法的两倍。尤其是在频繁调用方法时。
现在我们看NSAutoreleasePool类的实现。由于该类的源代码比较复杂,因此我们假想一个简化的源代码学习:
+ (void) addObject:(id) anObj
{
NSAutoreleasePool *pool = 取得正在使用的NSAutoreleasePool对象;
if(pool != nil) {
[pool addObject:anObj];
} else {
NSLog(@"NSAutoreleasePool对象非存在状态下调用autorelease");
}
}
addObject类方法调用正在使用的NSAutoreleasePool对象的addObject实例方法。
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
id obj = [[NSObject alloc] init];
[obj autorelease];
上述是被赋予pool变量的即为正在使用的NSAutoreleasePool对象。
如果嵌套生成或持有的NSAutoreleasePool对象,则会使用最内侧的对象。
NSAutoreleasePool *pool0 = [[NSAutoreleasePool alloc] init];
NSAutoreleasePool *pool1 = [[NSAutoreleasePool alloc] init];
NSAutoreleasePool *pool2 = [[NSAutoreleasePool alloc] init];
id obj = [[NSObject alloc] init];
[obj autorelease]; // obj 被添加到 pool2 中
[pool2 drain]; // 释放 pool2 中的对象
[pool1 drain]; // 释放 pool1 中的对象
[pool0 drain]; // 释放 pool0 中的对象
上述代码中,obj会被添加到最内层的pool2中。当调用drain时,先释放pool2,再释放pool1和pool0。
我们继续看addObject
实例方法的实现。
-(void) addObject:(id) anObj
{
[array addObject:anObj];
}
GNUstep实现使用的是连接列表,这同在NSMutableArray对象中追加对象参数是一样的。
如果调用NSObject类的autorelease实例方法,该对象被追加到正在使用的NSAutoreleasePool对象中的数组里。
下面我们看drain
实例方法
- (void)drain {
[self dealloc];
}
- (void)dealloc {
[self emptyPool];
[array release];
}
- (void)emptyPool {
for (id obj in array) {
[obj release];
}
}
该方法会释放pool中所有的对象。
苹果的实现
class AutoreleasePoolPage {
public:
static inline void *push() {
相当于生成或持有NSAutoreleasePool类对象;
}
// 释放自动释放池
static inline void pop(void *token) {
相当于废弃NSAutoreleasePool类对象;
releaseAll();
}
static inline id autorelease (id obj)
{
相当于NSAutoreleasePool类的addObject类方法
AutoreleasePoolPage *autoreleasePoolPage = 取得正在使用的AutoreleasePoolPage实例;
autoreleasePoolPage->add(obj);
}
// 将对象添加到内部数组
id *add(id obj) {
}
// 释放内部数组中的所有对象
void releaseAll() {
// 遍历内部数组,调用每个对象的 release 方法
for (id obj : internalArray) {
[obj release];
}
// 清空数组
internalArray.clear();
}
// 创建一个新的自动释放池
void *objc_autoreleasePoolPush(void) {
return AutoreleasePoolPage::push();
}
// 释放自动释放池
void objc_autoreleasePoolPop(void *ctxt) {
AutoreleasePoolPage::pop(ctxt);
}
// 将对象添加到自动释放池
id objc_autorelease(id obj) {
return AutoreleasePoolPage::autorelease(obj);
}
我们还可以使用NSAutoreleasePool类中的调试用非公开类方法showPools来确认已被autorelease的对象的状况。
autorelease NSAutoreleasePool对象会发生异常。
总结
对引用计数相关方法的实现原理进行简单了解。对比GNUstep和苹果对统一操作不同实现方法优劣的好坏。如引用计数的保存。