【iOS】OC高级编程 iOS多线程与内存管理阅读笔记——自动引用计数(二)
alloc/retain/release/dealloc实现
包含NSObject类的Foundation框架的源代码没有公开,不过Foundation框架使用的Core Foundation框架的源代码以及通过调用NSObject类进行内存管理部分的源代码是公开的。但是没有NSObject类的源代码,就很难了解NSObject类的内部实现细节。为此,我们使用开源软件GNUstep来说明,
GNUstep是Cocoa框架的互换框架,也就是说在使用者看来,两者的行为和实现方式是一样的,或者说非常相似。
GNUstep源代码中NSObject类的alloc类方法是这样的:
id obj = [NSOject alloc];
在NSObject.m源代码中的实现如下:
+ (id)alloc {
return [self allocWithZone:NSDefaultMallocZone()];
}
+ (id)allocWithZone:(NSZone*)z {
return NSAllocateObject(self, 0, z);
}
通过allocWithZone:类方法调用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,最后返回作为对象而使用的指针。
NSDefaultMallocZone、NSZoneMalloc等名称中包含的NSZone,是为防止内存碎片化而引入的结构。对内存分配的区域本身进行多重化管理,根据使用对象的目的、对象的大小分配内存,从而提高内存管理的效率。
但是现在的运行时系统只是简单地忽略了区域的概念,运行时系统中的内存管理本身已极具效率,使用区域来管理内存反而会引起内存使用效率低下以及源代码复杂化等问题。
以下是去掉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整数来保存引用计数,并将其写入对象内存头部,该对象内存块全部置零后返回。
对象的引用计数可以通过retainCount实例方法取得。
id obj = [[NSObject alloc] init];
NSLog(@"retainCount = %d", [obj retainCount]);
//显示retainCount = 1
执行alloc后对象的retainCount是"1"。可以通过GNUstep的源代码来确认
- (NSUInteger)retainCount {
return NSExtraRefCount(self) + 1;
}
inline NSUInteger
NSExtraRefCount (id anObject) {
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 {
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变量加一的retained++代码/同样,release实例方法进行retained--并在该引用计数变量为0时作出处理。下面通过源代码来确认。
[obj release];
- (void)release {
if (NSDecrementExtraRefCountWasZero(self))
[self dealloc];
}
BOOL
NSDecrementExtraRefCpuntWasZero (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实例方法的实现。
- (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中的实现。总结如下:
-
OC对象中存有引用计数这一整数值
-
调用alloc或是retain方法,引用计数加一
-
调用release后,引用计数减一
-
引用计数值为零时,调用dealloc方法废弃对象
苹果的实现
接下来看看苹果是怎么实现内存管理和引用计数的。由于NSObject类的源代码没有公开,我们利用Xcode的调试器和iOS大概追溯出其实现过程。
在NSObejct类的alloc类方法上设置断点,追踪程序的执行,可以得到执行所调用的方法和函数。
可以看到alloc类方法是先调用allocWithZone:类方法和GNUstep的实现相同,然后调用class_createInstance函数,最后调用calloc来分配内存块。和前面GNUstep的实现差异不大。
那retainCount/retain/release实例方法又是怎么实现的呢?同刚才一样,先列出调用的方法和函数
可以看到每个方法都通过调用同一个__CFDoExternRefOperation函数,调用了一系列名称相似的函数。这些函数名前缀为"CF",它们包含于Core Foundation框架源代码中。下面给出简化了CFDoExternRefOperation函数后的源代码(包含在CFRuntime.c中)
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;
}
}
可以看到__CFDoExternRefOperation函数按retainCount/retain/release操作进行分发,调用不同的函数,NSObejct类的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);
}
可见,苹果对内存管理的实现大概就是采用散列表(引用计数表)来管理引用计数。
GNUstep将引用计数保存在对象占用内存块头部的变量中,而苹果的实现,则是保存在引用计数表的记录中,两者各有好处:
通过内存块头部管理引用计数有以下好处:
-
少量代码即可完成
-
能够统一管理引用计数用内存块与对象用内存块
通过引用计数表管理引用计数有以下好处:
-
对象用内存块的分配无需考虑内存块头部
-
引用计数表各记录中存有内存块地址,可以从各个记录追溯到各对象的内存块
这里第二条特性在调试时有很重要的作用,即使出现故障导致对象占用的内存块损坏,但只要引用计数表没有被破坏,就能确认各内存块的位置
autorelease
autorelease就是自动释放,类似于C语言中局部变量的特性。autorelease会像C语言的局部变量那样来对待对象实例,当超出其作用域时,对象实例的release实例方法被调用。但是与C语言的局部变量不同的是,编程人员可以设定变量的作用域
autorelease的具体使用方法如下:
-
生成并持有NSAutoreleasePool对象
-
调用已分配对象的autorelease实例方法
-
废弃NSAutoreleasePool对象
NSAutoreleasePool对象的生存周期相当于C语言变量的作用域。对于所有调用过autorelease实例方法的对象,在废弃NSAutoreleasePool对象时,都将调用release实例方法。用源代码表示如下:
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
id obj = [[NSObject alloc] init];
[obj autorelease];
[pool drain];
在Cocoa框架中,相当于程序主循环的NSRunLoop或者在其他程序可运行的地方,对NSAutoreleasePool对象进行生成、持有和废弃处理。因此,开发者不一定非得使用NSAutoreleasePool对象来进行开发工作
尽管如此,在大量产生autorelease的对象时,只要不废弃NSAutoreleasePool对象,那么生成的对象就不能被释放,因此有时会产生内存不足的现象。
Cocoa框架中有很多类方法用于返回autorelease的对象,比如NSMutableArray类的arrayWithCapacity类方法。
id array = [NSMutableArray arrayWithCapacity:1];
id array = [[[NSMutableArray alloc] initWithCapacity:1] autorelease];
autorelease实现
首先看GNUstep的源代码:
[obj autorelease]
- (id)autorelease{
[NSAutoreleasePool addObject:self];
}
autorelease实例方法的本质就是调用NSAutoreleasePool对象的addObject类方法。
下面是简化之后的NSAutoreleasePool类的源代码:
+ (void)addObject:(id)anObj {
NSAutoreleasePool *pool = 取得正在使用的NSAutoreleasePool对象;
if (pool != nil) {
[pool addObject:anObj];
} else {
NSLog(@"NSAutoreleasePool对象非存在状态下调用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)addObject:(id)anObj {
[array addObject:anObj];
}
实际的GNUstep实现使用的是连接列表,这同在NSMutableArray对象中追加对象参数是一样的。如果调用NSObject类的autorelease实例方法,该对象将被追加到正在使用的NSAutoreleasePool对象中的数组里。既然NSAutoreleasePool实例底层是用数组实现的,那么在使用drain实例方法时同样有处理数组的过程
[pool drain];
- (void)drain {
[self dealloc];
}
- (void)dealloc {
[self emptyPool];
[array release];
}
- (void)emptyPool {
for (id obj in array) {
[obj release];
}
}
这样可以确保对数组中的所有对象都调用了release实例方法
苹果的实现
可通过objc4库的runtime/obj-arr.mm来确认苹果中autorelease的实现。
class AutoreleasePoolPage {
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实例方法;
}
};
void *objc_autoreleasePoolPush(void) {
return AutoreleasePoolPage::push();
}
void objc_autoreleasePoolPop(void *ctxt) {
AutoreleasePoolPage::pop(ctxt);
}
id *objc_autorelease(id obj) {
return AutoreleasePoolPage::autorelease(obj);
}
C++类中虽然有动态数组的实现,但其行为和GNUstep的实现完全相同。