所謂,引用計數

博文連接: 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

  • 即便只讀操做,也會引發內存寫操做(引用計數的修改)。

  • 引用計數讀寫操做要原子化。

retain release

在蘋果開源的 runtime 中,在objc-object.h中有部分關於retainrelease的實現代碼,具體以下:

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 類型的對象進行retainrelease是沒有意義的,從 rootRetainif (isTaggedPointer()) return (id)this;能夠看出。

原子化

上面說到,引用計數有個缺點是讀寫的原子化,在源碼中,不論是retainreleaseretainCount操做都是加鎖的。

這裏加解鎖的方法是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是蘋果的一項編譯器功能,ARC會在編譯期自動添加代碼,可是,除此以外,還須要 Objective-C 運行時的協助。

ARC讓咱們不須要再手寫一些相似於retainreleaseautorelease的代碼。這看上去有點像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 盤點一文中進行了試驗,原文以下

咱們試驗一下,這裏printDescriptionAprintDescriptionB方法各會返回一個不一樣類型的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_tpthread_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

參考文檔

相關文章
相關標籤/搜索