Objc高级编程之ARC-读书笔记
内存管理原则
MRC和ARC都使用引用计数方式来对内存进行管理,简单地说就是下面几条原则:
- 自己生成的对象,自己所持有。
- 非自己生成的对象,自己也能持有。
- 自己不再需要持有对象时需要释放。
- 非自己持有的对象无法释放。
以上对对象的操作与Objc方法的映射关系为:
- 生成并持有对象 = alloc/new/copy/mutableCopy等
- 持有对象 = retain
- 释放对象 = release
- 销毁对象 = dealloc
实际上以上这些方法并不是语言本身,而是Cocoa/CocoaTouch Framework的Foundation里面的NSObject的方法。Apple建议所有的Objc对象都继承自NSObject,这样可以依赖已有的稳定安全内存管理不用自己操心。
GNUstep是一个盒Cocoa Framework兼容的实现,虽然不能期望它是完全和Apple一样实现的,但是它有相同的表现所以实现上也应该很相似。
理解了GNUstep的源代码可以帮助我们猜测Apple的Cocoa是如何实现的。以下是GNUstep的实现:
struct obj_layout {
NSUInteger retained;
};
+ (id)alloc {
return [self allocWithZone:NSDefaultMallocZone()];
}
+ (id)allocWithZone:(NSZone *)z {
return NSAllocateObject(self, 0, z);
}
inline id NSAllocateObject(Class aClass, NSUInteger extraBytes, NSZone *zone) {
int size = baseBytes + extraBytes
id new = NSZoneMalloc(zone, size);
memset(new, 0, size);
new = (id)&((struct obj_layout *)new)[1];
}
- (NSUInteger)retainCout {
return NSExtraRefCount(self) + 1;
}
inline NSUInteger NSExtraRefCount(id anObject) {
return ((struct obj_layout *)anObject)[-1].retained;
}
- (id)retain {
NSIncrementExtraRefCount(self);
return self;
}
inline void NSIncrementExtraRefCount(id anObject) {
if (((struct obj_layout *)anObject)[-1].retained == UINT_MAX - 1) {
NSString *format = @"NSIncrementExtraRefCount() asked to increment too far";
[NSException raise:NSInternalInconsistencyException format:format];
}
((struct obj_layout *)anObject)[-1].retained++;
}
- (void)release {
if (NSDecrementExtraRefCountWasZero(eslf)) {
[]self dealloc];
}
}
inline BOOL NSDecrementExtraRefCountWasZero(id anObject) {
if (((struct obj_layout *)anObject)[-1].retained == 0) {
return YES;
} else {
((struct obj_layout *)anObject)[-1].retained--;
return NO;
}
}
- (void)dealloc {
NSDeallocateObject(self);
}
inline void NSDeallocateObject(id anObject) {
struct obj_layout *o = &((struct obj_layout *)anObject)[-1];
free(o);
}
可以看出每个Objc对象,在其内存布局前都有一个NSUInteger类型变量,用来表示引用计数。alloc/new/copy/mutableCopy/retain都会增加计数,release会减少计数,当计数减至为0时调用dealloc从内存中销毁该对象。
研究Apple的实现之前因为NSObject的源代码没有公开,通过使用Xcode的调试器(lldb)来研究其实现。在NSObject类的alloc里设置端点,可以发现栈中的调用顺序为:
+alloc
+allocWithZone:
class_createInstance
calloc
大体上Apple和GNUstep的实现没有什么差别。同样的方法能够得到retainCount和retain还有release的调用顺序。
-retainCount
__CFDoExternRefOperation
CFBasicHashGetCountOfKey
-retain
__CFDoExternRefOperation
CFBasicHashAddValue
-release
__CFDoExternRefOperation
CFBaseHashRemoveValue
在Core Foundation Framework中找到__CFDoExternRefOperation:
int __CFDoExternRefOperation(uintptr_t op, id obj) {
CFBasicHashRef table = get hashtable from 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_retainCount, self);
}
- (id)retain {
return (id)__CFDoExternRefOperation(OPERATION_retain, self);
}
- (void)release {
return __CFDoExternRefOperation(OPERATION_release, self);
}
同时也可以猜测,Apple的实现大概是采用Hash散列表(引用计数表)完成的:
GNUstep将引用计数保存在对象内存块头部的变量中,Apple将引用计数保存在一张散列表中。GNUstep的实现看起来简单又高效,但Apple这样实现也必然有它的好处。GNUstep的好处是:少量代码就能实现;引用计数内存和对象内存能统一管理。Apple的好处是:为对象分配内存时无需考虑引用计数,不用担心对象内存块头部问题;散列表用对象内存地址作为Key,这样能追溯各个对象内存块位置,在调试时非常有用。即使出现错误导致对象内存块损坏,只要散列表没问题,就能找到正确的内存块位置。
另外利用Instrument检查内存泄漏时,散列表的记录也有助于帮助检测各对象持有者是否存在。
GNUstep实现的Autorelease
- (id)autorelease {
[NSAutoreleasePool addObject:self];
}
实际上,GNUstep应用了IMP缓存技术,缓存了不变的Class,SEL,IMP使得运行效率能满足频繁调用的autorelease
id autorelease_class = [NSAutoreleasePool class];
SEL autorelease_sel = @selector(addObject:);
IMP autorelease_imp = [autorelease_class methodForSelector: autorelease_sel];
- (id)autorelease {
(*autorelease_imp)(autorelease_class, autorelease_sel, self);
}
NSAutoreleasePool:
+ (void)addObject:(id)anObj {
NSAutoreleasePool *pool = //获取当前的autoreleasePool;
if (pool != nil) {
[pool addObject:anObj];
} else {
NSLog(@"autorelease is called without active NSAutoreleasePool.");
}
}
- (void)drain {
[self dealloc];
}
- (void)dealloc {
[self emptyPool];
}
- (void)emptyPool {
for (id obj in array) {
[obj release];
}
}
Apple实现的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 = //获取当前的autoreleasePool;
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);
}
使用extern void _objc_autoreleasePoolPrint()私有方法可以查看AutoreleasePool的状态。如果对AutoreleasePool对象进行autorelease的话,会产生NSInvalidArgumentException的异常。
所有权
ARC有效时,objc的对象类型必须附加所有权修饰符,一共有4种:
__strong表示对修饰的对象的“强引用”,持有强引用的变量在超出其作用域时失效,其引用的对象会被释放。默认情况下,缺省所有权声明的对象类型变量,都隐式声明为__strong类型。通过遵循内存管理原则的__strong修饰符,不必再依赖retain或release,也能正确管理内存。
__weak的设计主要是解决循环引用的问题。__weak修饰符与__strong相反,仅仅引用但不持有。同时__weak的另一个优点是,其所引用的对象如果被销毁,该变量将被自动安全地设置为nil,再也不会出现指向已经释放的对象内存块的野指针了。
__unsafe_unretained正如其名unsafe所示,是不安全的所有权修饰符。其基本与__weak表现一致,除了在引用对象销毁时不被自动设置为nil。
__autoreleasing修饰符的变量被某对象赋值时,就代表该对象在MRC时调用了autorelease的方法,即对象被注册到了AutoreleasePool里。同时,使用@autoreleasepool{}块代替NSAutoreleasePool的方法调用。
隐式使用__autoreleasing
获取并持有非自己生成的对象时,该对象已经被注册到了autoreleasepool。这是由于编译器会检查方法名是否以alloc/new/copy/mutableCopy开头,如果不是则自动将方法返回的对象注册到autoreleasepool(当然init方法不会)。这些方法里,return使得对象变量超出其作用域,__strong类型变量会释放其持有的对象,所以编译器会使用__autorelease修饰符将对象注册到autoreleasePool延迟释放。
+ (id)array {
return [[NSArray alloc] init];
}
等价于
+ (id)arrya {
__autoreleasing obj = [[NSArray alloc] init];
return obj;
}
使用__weak类变量时,因为该变量只有引用并不持有,而在访问该变量的过程中有被销毁的可能性。如果把指向的对象注册到autoreleasePool中,那么在@autoreleasePool{}结束前都能保证期间对象存在。
id _weak obj = XXX;
NSLog(@"Class = %@", [obj class]);
等价于
id _weak obj = XXX;
id __autoreleasing tmp = obj;
NSLog(@"Class = %@", [tmp class]);
默认情况下,缺省所有权声明对象的指针变量,都隐式声明为__autoreleasing。因为编译器将alloc/new/copy/mutableCopy开头的方法产生的新对象看作自己生成并持有,其他情况下产生的新对象都应该看作非自己生成并持有。虽然strong类型变量也能够传递,但还是应该统一遵循内存管理原则。同时,对象指针的赋值操作时所有权修饰符必须一致否则编译不通过。
- (BOOL)doSomething:(NSError *)error {
//发生错误
*error = [[NSError alloc] init];
return NO;
}
NSError *error = nil;
BOOL result = [self doSomething:&error];
等价于
- (BOOL)doSomething:(__autoreleasing NSError *)error {
//发生错误
*error = [[NSError alloc] init];
return NO;
}
__autoreleasing NSError *error = nil;
BOOL result = [self doSomething:&error];
另外,在显示地指定__autoreleasing修饰符时,必须注意修饰的对象要为自动变量(包括局部变量、方法参数)。
ARC使用规则
在ARC有效时必须要遵循以下规则:
- 不能使用retain/release/retainCount/autorelease
- 不能使用NSAllocateObject/NSDeallocateObject(分别实现了影响内存管理的alloc和dealloc)
- 方法命名需遵循内存管理原则
- 不再显示调用dealloc(遵循无法显示影响内存管理)
- 使用@autoreleasepool{}代替NSAutoreleasePool
- 不能使用NSZone
- objc对象类型变量不能作为C语言结构体(struct/union)的成员(没有方法管理C语言结构体成员的生命周期)
- id和void*之间转换需要显示指明所有权变化(C语言类型变量超出ARC对objc对象类型变量的管理范围,需手动指明所有权变化)
__bridge对象所有权不变,编译器继续自动管理;
__bridge_retained或CFBridgingRetain移交对象所有权从编译器到开发者,至此开发者需手动管理;
__bridge_transfer或CFBridgingRelease移交对象所有权从开发者到编译器,至此编译器能自动管理
实现
Apple称ARC是“右边一起进行内存管理”的,但实际上只有编译器是无法完全胜任的,在此基础上还需要Runtime的支持。也就是说,ARC是由Clang(LLVM编译器)3.0+和Objc Runtime493.9+实现的。编译器选项“-S”运行clang,可以取得程序汇编输出。通过阅读汇编和objc4库源代码,可以模拟ARC是如何实现的。
__strong修饰符
{
id __strong obj = [[NSObject alloc] init];
}
//编译器模拟代码
{
id obj = objc_msgSend(NSObject, @selector(alloc));
objc_msgSend(obj, @selector(init));
objc_release(obj);
}
由此可知,对于自己生成并持有的对象,编译器自动地在作用域结束前插入release。
+ (id)array
{
return [[NSMutableArray alloc] init];
}
{
id __strong obj = [NSMutableArray array];
}
//模拟器代码
+ (id)array
{
id obj = objc_msgSend(NSMutableArray, @selector(alloc));
objc_msgSend(obj, @selector(init));
return objc_autoreleaseReturnValue(obj);
}
{
id obc = objc_msgSend(NSMutableArray, @selector(array));
objc_retainAutoreleasedReturnValue(obj);
objc_release(obj);
}
由此可知,对于非标准对象生成方法里,编译器自动地在作用域结束前插入objc_autoreleaseReturnValue方法(该方法将生成的对象autorelease-即注册到autorelresepool)。而对于非自己生成能持有的对象,和之前一样编译器自动地在作用域结束前插入release,同时在获取对象后,会插入objc_retainAutoreleasedReturnValue方法(该方法将持有返回得到的autoreleased对象)。
不过,objc_autoreleaseReturnValue和objc_autorelase不同的是,前者是要用于最优化程序。objc_retainAutoreleasedReturnValue会检查调用方的执行命令列表(参见Autorelease),如果发现调用房在调用非标准对象生成方法(例如+array)后紧结着调用了objc_retainAutoreleasedReturnValue的话,那么就不将生成的对象autorelease-即注册到autorelresepool,而是直接返回。同理,且objc_retainAutoreleasedReturnValue和objc_retain不同的是,如果之前调用过objc_autoreleaseReturnValue,那么就不将返回的对象retain,而是直接使用。这两个方法共同协作,可以将生成的对象不注册到autoreleasepool中直接使用,省去了2次objc的msgSend方法,达到最优化。同时,也解决了非标准方法在ARC和MRC模块之间传递对象的问题。
__weak修饰符
{
id __weak obj = another;
}
//编译器模拟代码
{
id obj = 0;
objc_initWeak(&obj, another);
objc_destroyWeak(&obj);
}
//等价于
{
id obj = 0;
objc_storeWeak(&obj, another);
objc_storeWeak(&obj, 0);
}
objc_storeWeak方法将__weak修饰符变量的地址(即第1个参数)作为Value,将被引用的对象(即第2个参数)作为Key,注册到runtime维护的weak关系映射表(Hash散列表)中,当Key为0时,将Value从对应的weak关系映射表中删除。使用weak表,将销毁的对象进行检索,就能快速地获得weak引用它的所有变量的地址。对象的销毁过程:
- objc_release
- 当引用计数为0时而执行dealloc
- _objc_rootDealloc
- object_dispose
- objc_destructInstance
- objc_clear_deallocating
最后调用objc_clear_deallocating时,首先从weak表中获取销毁对象为Key的所有Value(即所有weak变量的地址),然后通过指针将weak变量的值设为nil,再在weak表中删除销毁对象对应记录,最后把销毁对象也从计数表中删除对应记录。由此可知,如果有大量__weak修饰符变量,则会在引用的对象被销毁时大量被设置为nil消耗CPU,所以应该尽可能在需要的地方使用__weak。
使用__weak修饰符变量即是使用autoreleased对象:
{
id __weak obj = another;
NSLog(@"%@", obj);
}
//编译器模拟代码
{
id obj = 0;
objc_initWeak(&obj, another);
id tmp = objc_loadWeakRetained(&ojc);
objc_autorelease(tmp);
NSLog(@"%@", tmp);
objc_destroyWeak(&obj);
}
objc_loadWeakRetained取出__weak修饰符变量引用的对象并持有,objc_autorelease将获取的对象注册到autoreleasepool中延迟释放。所以,__weak引用的对象在@autoreleasepool{}期间都是有效的可以放心使用。但是,如果大量地使用__weak变量,会产生大量的autoreleased对象,因为在使用__weak变量时最好暂时赋值给一个__strong变量再使用__strong。
{
id __weak objc = another;
NSLog(@"1 %@", obj);
NSLog(@"2 %@", obj);
NSLog(@"3 %@", obj);
}
//等价于
{
id _weak objc = another;
id tmp1 = objc_loadWeakRetained(&ojc);
objc_autorelease(tmp1);
NSLog(@"1 %@", tmp1);
id tmp2 = objc_loadWeakRetained(&ojc);
objc_autorelease(tmp2);
NSLog(@"2 %@", tmp2);
id tmp3 = objc_loadWeakRetained(&ojc);
objc_autorelease(tmp3);
NSLog(@"3 %@", tmp3);
}
//使用__strong
{
id __weak objc = another;
id __strong tmp = objc;
NSLog(@"1 %@", tmp);
NSLog(@"2 %@", tmp);
NSLog(@"3 %@", tmp);
}
//等价于
{
id _weak objc = another;
id tmp = objc_loadWeakRetained(&ojc);
NSLog(@"1 %@", tmp);
NSLog(@"2 %@", tmp);
NSLog(@"3 %@", tmp);
objc_release(tmp);
}
__autoreleasing修饰符
@autoreleasepool {
id __autoreleasing obj = [[NSObject alloc] init];
}
@autoreleasepool {
id __autoreleasing obj = [NSMutableArray array];
}
//等价于
id pool = objc_autoreleasePoolPush();
id obj = objc_msgSend(NSObject, @selector(alloc));
objc_msgSend(obj, @selector(init));
objc_autorelease(obj);
objc_autoreleasePoolPop(pool);
id pool = objc_autoreleasePoolPush();
id obj = objc_msgSend(NSMutableArray, @selector(array));
objc_retainAutoreleasedReturnValue(obj);
objc_autorelease(obj);
objc_autoreleasePoolPop(pool);
引用计数
ARC虽然不能使用retainCount查看引用计数,但能参考**extern uintptr_t _objc_rootRetainCount(id obj)**私有方法查看。不过也不能够完全信任该方法。对于已经销毁的对象已经地址不正确的对象,有时候也返回“1”。另外,在多线程环境中使用时,因为存在静态条件的问题,所以取得的数值不一定完全可信。