1.1 什么是引用计数
自动引用计数是指内存管理中对引用采取自动计数的技术,在新一代Apple LLVM编译器中设置ARC
为有效状态,就无需再次键入retain
或者release
代码,降低程序崩溃,内存泄漏等等风险的同时,很大程度上减少了开发程序的工作量。编译器完全清楚目标对象,并且能立刻释放那些不再被使用的对象。应用程序将有可预测性,且能流畅运行,速度也将大幅提升。
1.2 内存管理的思考方式
1.自己生成的对象,自己持有
2.非自己生成的对象,自己也能持有
3.不再需要自己持有的对象时释放
4.非自己持有的对象无法释放
alloc/new/copy/mutableCopy,retain,release,dealloc
有关内存管理的方法实际上不包括在OC语言中,而是包含在Cocoa框架中用于OS X,iOS应用的开发。Cocoa框架中的Foundation
框架类库的NSObject
担负内存管理的职责。
自己生成的对象,自己持有
alloc/new/copy/mutableCopy
这些名称开头的方法意味自己生成的对象只有自己持有
/*
*自己生成并持有对象
*/
id obj = [[NSObject alloc] init];
/*
*自己持有对象
*/
使用NSObject
类的alloc
方法就能自己生成并持有
对象。指向生成并持有
对象的指针
被赋值给变量obj,使用new
类方法也可以生成并持有对象
/*
*自己生成并持有对象
*/
id obj = [NSObject new];
/*
*自己持有对象
*/
copy
方法利用基于NSCopying
方法约定,由各类实现的copyWithZone:
方法生成并持有
对象的副本
,mutableCopy
方法利用基于NSMutableCopying
方法约定,由各类实现的mutableCopyWithZone:
方法生成并持有
对象的副本
,copy
生成不可变的对象,mutableCopy
生成可变对象。这些方法生成的对象虽然是对象的副本
,但依旧是“自己生成并持有”
。
非自己生成的对象,自己也能持有
alloc/new/copy/mutableCopy
以外取得的对象,非自己生成并持有
,所以自己不是
该对象的持有者。
/*
*取得非自己生成并持有的对象
*/
id obj = [NSMutableArray array];
/*
*取得对象的存在,但自己不持有
*/
使用retain
方法可以持有
/*
*取得非自己生成并持有的对象
*/
id obj = [NSMutableArray array];
/*
*取得对象的存在,但自己不持有
*/
[obj retain];
/*
*自己持有对象
*/
不再需要自己持有的对象时释放
自己持有的对象,一旦不再需要,持有者有义务释放该对象。使用release
方法
/*
*自己生成并持有对象
*/
id obj = [NSObject new];
/*
*自己持有对象
*/
[obj release];
/*
*释放对象
*
*指向对象的指针仍然被保留在变量obj中,貌似能够访问
*但对象一经释放绝对不可访问
*/
用alloc,new
方法生并持有的对象通过release
释放,自己生成而非自己持有的对象若用retain
持有,也可以通过release
释放。
/*
*取得非自己生成并持有的对象
*/
id obj = [NSMutableArray array];
/*
*取得对象的存在,但自己不持有
*/
[obj retain];
/*
*自己持有对象
*/
[obj release];
/*
*释放对象,对象不可再访问
*/
自己持有的对象一旦不再需要,务必要用release
方法释放。
无法释放非自己持有的对象
自己生成并持有的对象,或者使用retain
方法持有的对象,不需要时需要释放,而由此以外所得到的对象绝不能释放,释放非自己持有的对象会造成程序崩溃。
1.3 alloc/new/copy/mutableCopy实现
GNUstep
是Cocoa框架的互换框架,两者虽然不能说完全相同,但是从使用角度开看,两者的行为方式是一样的,接下来用GNUstep
源码举例理解
id obj = [NSObject alloc];
//上述调用alloc类方法在NSObject.m源代码中实现如下。
+ (id)alloc{
return [self allocWithZone:(struct _NSZone *)NSDefaultMallocZone()];
}
+ (id)allocWithZone:(struct _NSZone *)zone{
return NSAllocateObject(self,0,zone);
}
通过allocWithZone
类方法调用NSAllocateObject
函数分配了对象。下面我们来看看NSAllocateObject
函数。
struct obj_layout{
NSUInteger retained;
};
inline id
NSAllocateObject(Class class, 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
又是什么呢,它是为了防止内存碎片化引入的结构,根据使用对象的目的,对象的大小分配的内存,从而提高了内存管理的效率,但是运行时系统中的内存管理本身已经极具效率,使用ZONE
来进行内存管理反而会引起内存使用效率低下以及源代码复杂化,所以现在的运行时系统简单忽略了区域的概念。
以下是去掉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
后返回。
对象的引用计数可以通过retainCount
实例方法取得。
id obj = [[NSObject alloc] init];
NSLog(@"retainCount=%d",[obj 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
。
retain
方法会运行retained++
代码,release
方法会使retained
变量大于0
时减1
,运行retained--
方法,等于0
时调用delloc
方法废弃由alloc
分配的内存块。
总结:
1.在OC的对象存有引用计数这一整数值。
2.alloc,retain
,引用计数+1。
3.release
,引用计数值-1。
4.引用计数值为0
,调用delloc
废弃对象。
1.4 苹果的实现
由于苹果的NSObject
代码并为开源,利用Xcode的调试器lldb
大概追溯其实现过程。
//alloc方法
+alloc
+allocWithZone:
class_createInstance
calloc //分配内存块
//retainCount
-retainCount
__CFDoExternRefOperation
CFBasicHashGetCountOfKey
//retain
__CFDoExternRefOperation
CFBasicHashAddValue
//release
__CFDoExternRefOperation
CFBasicHashRemoveValue
各个方法都通过一个__CFDoExternRefOperation
函数,调用了一系列名称相似的函数,它们都包含于Core Foundation
框架源代码中,__CFDoExternRefOperation
函数按照retainCount/retain/release
操作进行分发,从各个函数名称可以看出苹果的实现大概是采用散列表
来管理引用计数。
GNUstep
将引用计数保存在对象占用内存块头部变量中,而苹果的实现,则是保存在散列表
的记录中。
保存在头部的好处:
少量代码即可完成
能够统一管理引用计数用内存块与对象用内存块
引用计数表好处:
对象用内存块的分配无需考虑内存块头部
计数表记录中存有内存块地址,可以从各个记录追溯到各个对象的内存块
第二条在调试时有着举足轻重的作用,即使出现故障导致对象占用的内存块损坏,但只要引用计数表没有被破坏就能确认各个内存块的位置。
另外,在利用工具检测内存泄漏时,引用计数表的各记录也有助于检测各个对象的持有者是否存在着。