所谓,引用计数
博文鏈接: http://ifujun.com/suo-wei-yin-yong-ji-shu/
簡介
在大部分關于Objective-C的書中,一般對于引用計數的講解基本類似于下面(以 Objective-C基礎教程 為例):
Cocoa采用了一種稱為引用計數的技術。每個對象有一個與之相關聯的整數,稱作它的引用計數器。當某段代碼需要訪問一個對象時,該代碼將該對象的引用計數器值加1。當該代碼結束訪問時,將該對象的引用計數器值減1。當引用計數器值為0時,表示不再有代碼訪問該對象,因此對象將被銷毀,其占用的內存被系統回收以便重用。
概括一下就是,每個對象都會有個引用計數器,當且僅當引用計數器的值大于0時,該對象才可能是存活的。
引用計數的內存回收是分布于整個運行期的,基本類似于下圖。圖中紅色表示引用計數的活動。(圖片來自于https://github.com/kenfox/gc-viz)
從圖中我們可以很直接的看出一些優點,比如:
不需要等到內存不夠才回收。
不需要掛起應用程序才回收,回收分布于整個運行期。
當然,引用計數也有一些缺點:
無法完全解決循環引用導致的內存泄露問題。
即使只讀操作,也會引起內存寫操作(引用計數的修改)。
引用計數讀寫操作要原子化。
retain release
在蘋果開源的 runtime 中,在objc-object.h中有部分關于retain和release的實現代碼,具體如下:
Retain
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));... }Release
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
對 Tagged Pointer 類型的對象進行retain和release是沒有意義的,從 rootRetain 的 if (isTaggedPointer()) return (id)this;可以看出。
原子化
上面說到,引用計數有個缺點是讀寫的原子化,在源碼中,不管是retain、release、retainCount操作都是加鎖的。
這里加解鎖的方法是sidetable_lock()和sidetable_unlock()。在
NSObject.mm中,sidetable_lock()的具體結構是:
SideTable中使用的鎖是spinlock_t。
struct SideTable {spinlock_t slock;... };這是類似于 Linux 上的自旋鎖,和OSSpinLock有一些不同,應該不存在OSSpinLock的優先級反轉問題,因為,蘋果很多地方依然在使用,比如蘋果的atomic使用的也是spinlock_t。(參考objc-accessors.mm)
ARC
我們知道,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中進行對象的創建操作,要移出來。
performSelector
在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.
當然,也可能是我的試驗不正確,如果你知道如何觸發這種內存泄露,請告訴我。
實現簡單引用計數
我們來實現一個簡單引用計數的代碼,我們需要實現以下方法:
-
retain
addReference
-
release
deleteReference
retainCount
依據我們上面提到的引用計數讀寫操作要原子化,我們需要添加鎖的操作,并且,我們這里簡單理解為當引用計數為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
參考文檔
https://book.douban.com/subject/26740958/
https://spin.atomicobject.com/2014/09/03/visualizing-garbage-collection-algorithms/
http://clang.llvm.org/docs/AutomaticReferenceCounting.html
https://mikeash.com/pyblog/friday-qa-2011-09-30-automatic-reference-counting.html
總結
- 上一篇: gzip命令
- 下一篇: Linux下安装配置NTP时间同步服务器