在大部分关于Objective-C的书中,一般对于引用计数的讲解基本类似于下面(以 Objective-C基础教程 为例):
Cocoa采用了一种称为引用计数的技术。每个对象有一个与之相关联的整数,称作它的引用计数器。当某段代码需要访问一个对象时,该代码将该对象的引用计数器值加1。当该代码结束访问时,将该对象的引用计数器值减1。当引用计数器值为0时,表示不再有代码访问该对象,因此对象将被销毁,其占用的内存被系统回收以便重用。
概括一下就是,每个对象都会有个引用计数器,当且仅当引用计数器的值大于0时,该对象才可能是存活的。
引用计数的内存回收是分布于整个运行期的,基本类似于下图。图中红色表示引用计数的活动。(图片来自于 https://github.com/kenfox/gc-viz )
从图中我们可以很直接的看出一些 优点 ,比如:
当然,引用计数也有一些 缺点 :
在苹果开源的 runtime 中,在 objc-object.h 中有部分关于 retain
和 release
的实现代码,具体如下:
objc_object::rootRetain(bool tryRetain, bool handleOverflow) { assert(!UseGC); if (isTaggedPointer()) return (id)this; ... do { transcribeToSideTable = false; oldisa = LoadExclusive(&isa.bits); newisa = oldisa; if (!newisa.indexed) goto unindexed; if (tryRetain && newisa.deallocating) goto tryfail; uintptr_t carry; newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry); ... } while (!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)); ... }
ALWAYS_INLINE bool objc_object::rootRelease(bool performDealloc, bool handleUnderflow) { assert(!UseGC); if (isTaggedPointer()) return false; ... do { oldisa = LoadExclusive(&isa.bits); newisa = oldisa; if (!newisa.indexed) goto unindexed; uintptr_t carry; newisa.bits = subc(newisa.bits, RC_ONE, 0, &carry); ... } while (!StoreReleaseExclusive(&isa.bits, oldisa.bits, newisa.bits)); ... }
在 draveness 的 黑箱中的 retain 和 release 中,draveness 对此进行了比较详细的讲解,我在此也不再赘述了,只补充几点:
对 Tagged Pointer 类型的对象进行 retain
和 release
是没有意义的,从 rootRetain
的 if (isTaggedPointer()) return (id)this;
可以看出。
上面说到,引用计数有个缺点是读写的原子化,在源码中,不管是 retain
、 release
、 retainCount
操作都是加锁的。
这里加解锁的方法是 sidetable_lock()
和 sidetable_unlock()
。在 NSObject.mm 中, sidetable_lock()
的具体结构是:
void objc_object::sidetable_lock() { SideTable& table = SideTables()[this]; table.lock(); }
SideTable
中使用的锁是 spinlock_t
。
struct SideTable { spinlock_t slock; ... };
这是类似于 Linux 上的自旋锁,和 OSSpinLock
有一些不同,应该不存在 OSSpinLock
的 优先级反转问题 ,因为,苹果很多地方依然在使用,比如苹果的 atomic
使用的也是 spinlock_t
。(参考 objc-accessors.mm )
我们知道,ARC是苹果的一项编译器功能,ARC会在编译期自动添加代码,但是,除此之外,还需要 Objective-C 运行时的协助。
ARC让我们不需要再手写一些类似于 retain
、 release
、 autorelease
的代码。这看上去有点像GC了,但是,它依然解决不了循环引用等问题,所以,只能说ARC是一种处于GC和手动管理内存中间的一个状态。
那 Objective-C 有过GC吗,有,以前有过,用的是类似于标记-清除的GC算法,后来在iOS上就完全使用手动管理内存了,再后来就是ARC了。(我们上面的 rootRetain
代码中就有这么一行: assert(!UseGC);
)
ARC大家都很熟了,它的一些规则什么的,我们就不重复了,就讲讲一些需要注意的点吧。
ARC只能作用于 Objective-C 类型,CoreFoundation 等类型的依然需要手动管理。Objective-C 对象的指针和 CoreFoundation 类型的指针是不一样的。
我们一般有三种类型 __bridge
、 __bridge_transfer
、 __bridge_retained
。
如果 CoreFoundation 对象和 Objective-C 对象转换只涉及类型,不涉及所有权的话,可以使用 __bridge
,比如这样:
id obj = (__bridge id)CFDictionaryGetValue(cfDict, key);
这时候ARC就可以接管这个对象并自动管理。
但是,如果所有权被变更了,那么,再使用 __bridge
的话,就会发生内存泄露。
NSString *value = (__bridge NSString *)CFPreferencesCopyAppValue(CFSTR("someKey"), CFSTR("com.company.someapp")); [self useValue: value];
其实,上面这段就等同于:
CFStringRef valueCF = CFPreferencesCopyAppValue(CFSTR("someKey"), CFSTR("com.company.someapp")); NSString *value = (__bridge NSString *)valueCF; //CFRelease(valueCF); [self useValue: value];
其实这时候是需要加一行 CFRelease(valueCF)
的,如果没有的话, valueCF
是会内存泄露的。
当然,上面的写法也是可以的,只是这个临时变量存在的意义不大,写法也比较啰嗦,可以使用 __bridge_transfer
去解决这个问题。
NSString *value = (__bridge_transfer NSString *)CFPreferencesCopyAppValue(CFSTR("someKey"), CFSTR("com.company.someapp")); [self useValue: value];
和 __bridge
不一样, __bridge_transfer
会将值和所有权都移交出去,ARC接管到所有权之后,ARC在这个对象用完之后会进行释放。
__bridge_retained
和 __bridge_transfer
类似,只是 __bridge_retained
用于将 Objective-C 对象转化为 CoreFoundation 对象,而 __bridge_transfer
用于将 CoreFoundation 对象转化为 Objective-C 对象。
举个例子,假设 [self someString]
这个方法会返回一个 NSString
类型的值,现在要将 NSString
类型的值转化为 CFStringRef
类型,使用 __bridge_retained
的话,相当于告诉ARC,对于这个对象,你的所有权已经没有了,我要自己来管理了。所以,我们要手动在后面加上 CFRelease()
方法。
CFStringRef value = (__bridge_retained CFStringRef)[self someString]; UseCFStringValue(value); CFRelease(value);
上面的例子来自于 Mikeash 。
总结一下就是:
__bridge
会将非Objective-C对象和Objective-C对象进行转换,但并不会移交所有权。 __bridge_transfer
会将非Objective-C对象转化为Objective-C对象,同时会移交所有权,ARC会帮你释放这个对象。 __bridge_retained
会将Objective-C对象转化为非Objective-C对象,同时会移交所有权,你需要手动管理这个对象。 一般来说,我们很少使用 try...catch
,我们一般抛 Error
而不是 Exception
,但是,总有一些特殊的情况, try...catch
的存在依然是有意义的。
如果我们在 try
中进行一些对象创建的操作的话,可能会造成内存泄露,比如:
@try { SomeObject *obj = [[SomeObject alloc] init]; [obj doSomething]; } @catch (NSException *exception) { NSLog(@"%@", exception); }
如果 try
代码段中发成错误, obj
将不会得到释放。如果现在是MRC,那你可以在 finally
中添加 [obj release]
,但是在ARC下,你无法添加,ARC也不会帮你添加。
所以,不要在 try
中进行对象的创建操作,要移出来。
在 Effective Objective-C 2.0 一书中,作者说到:
编译器并不知道将要调用的选择子是什么,因此,也就不了解其方法签名及返回值,甚至连是否有返回值都不清楚。而且,由于编译器不知道方法名,所以就没办法运用ARC的内存管理规则来判定返回的值是不是应该释放。鉴于此,ARC采用了比较谨慎的做法,就是不添加释放操作。然而,这么做会导致内存泄露。
我在iOS 常用Timer 盘点一文中进行了试验, 原文如下 :
我们试验一下,这里 printDescriptionA
和 printDescriptionB
方法各会返回一个不同类型的 View
(此 View
是新建的对象), printDescriptionC
会返回Void。
NSArray *array = @[@"printDescriptionA", @"printDescriptionB", @"printDescriptionC"]; NSString *selString = array[arc4random()%3]; NSLog(@"sel = %@", selString); SEL tempSel = NSSelectorFromString(selString); if ([self respondsToSelector:tempSel]) { [self performSelector:tempSel withObject:nil afterDelay:3.0f]; }
几次尝试之后,我发现,这是可以正常释放的。
如果我的试验正确的话,那么,ARC肯定不只是在编译期的优化,在运行时也是有优化的。这也印证了我上面所说的, ARC会在编译期自动添加代码,但是,除此之外,还需要 Objective-C 运行时的协助 。
而不是苹果文档中说的:
ARC works by adding code at compile time to ensure that objects live as long as necessary, but no longer.
当然,也可能是我的试验不正确,如果你知道如何触发这种内存泄露,请告诉我。
我们来实现一个简单引用计数的代码,我们需要实现以下方法:
依据我们上面提到的 引用计数读写操作要原子化 ,我们需要添加锁的操作,并且,我们这里 简单理解 为当引用计数为 0 时,进行 dealloc
方法的调用。
为了方便,我们用 pthread_mutex
来代替 spinlock_t
( pthread_mutex
是一种互斥锁,性能也挺高)。
基本代码类似于下面:
#import "FKObject.h" #import <objc/runtime.h> #include <pthread.h> @interface FKObject () { pthread_mutex_t fk_lock; } @property (readwrite, nonatomic) NSUInteger fk_retainCount; @end @implementation FKObject - (instancetype)init { if (self = [super init]) { pthread_mutex_init(&fk_lock, NULL); _fk_retainCount = 1; } return self; } - (void)fk_retain { [self addReference]; } - (void)fk_release { NSUInteger count = [self deleteReference]; if (count == 0) { [self fk_dealloc]; } } - (void)fk_dealloc { //因为ARC下不能主动调用dealloc方法,所以这里伪造一个fk_dealloc来模拟 NSLog(@"%@ dealloc", self); } - (void)addReference { pthread_mutex_lock(&fk_lock); NSUInteger count = [self fk_retainCount]; [self setFk_retainCount:++count]; pthread_mutex_unlock(&fk_lock); } - (NSUInteger)deleteReference { pthread_mutex_lock(&fk_lock); NSUInteger count = [self fk_retainCount]; [self setFk_retainCount:--count]; pthread_mutex_unlock(&fk_lock); return count; } @end
我们来测试一下:
FKObject *object = [[FKObject alloc] init]; NSLog(@"%ld", object.fk_retainCount); [object fk_retain]; NSLog(@"%ld", object.fk_retainCount); [object fk_release]; NSLog(@"%ld", object.fk_retainCount); [object fk_release];
https://github.com/Forkong/ReferenceCountingTest