博文連接: http://ifujun.com/suo-wei-yin-yong-ji-shu/html
在大部分關於Objective-C的書中,通常對於引用計數的講解基本相似於下面(以 Objective-C基礎教程 爲例):ios
Cocoa採用了一種稱爲引用計數的技術。每一個對象有一個與之相關聯的整數,稱做它的引用計數器。當某段代碼須要訪問一個對象時,該代碼將該對象的引用計數器值加1。當該代碼結束訪問時,將該對象的引用計數器值減1。當引用計數器值爲0時,表示再也不有代碼訪問該對象,所以對象將被銷燬,其佔用的內存被系統回收以便重用。git
歸納一下就是,每一個對象都會有個引用計數器,當且僅當引用計數器的值大於0時,該對象纔多是存活的。github
引用計數的內存回收是分佈於整個運行期的,基本相似於下圖。圖中紅色表示引用計數的活動。(圖片來自於https://github.com/kenfox/gc-viz)算法
從圖中咱們能夠很直接的看出一些優勢,好比:編程
不須要等到內存不夠纔回收。segmentfault
不須要掛起應用程序纔回收,回收分佈於整個運行期。app
固然,引用計數也有一些缺點:dom
沒法徹底解決循環引用致使的內存泄露問題。ide
即便只讀操做,也會引發內存寫操做(引用計數的修改)。
引用計數讀寫操做要原子化。
在蘋果開源的 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()
的具體結構是:
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.
固然,也多是個人試驗不正確,若是你知道如何觸發這種內存泄露,請告訴我。
咱們來實現一個簡單引用計數的代碼,咱們須要實現如下方法:
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