[編寫高質量iOS程式碼的52個有效方法](七)記憶體管理(上)

究極死胖獸發表於2016-07-27

[編寫高質量iOS程式碼的52個有效方法](七)記憶體管理(上)

參考書籍:《Effective Objective-C 2.0》 【英】 Matt Galloway

先睹為快

29.理解引用計數

30.以ARC簡化引用計數

31.在dealloc方法中只釋放引用並解除監聽

32.編寫異常安全程式碼時留意記憶體管理問題

目錄

第29條:理解引用計數

Objective-C語言使用引用計數來管理記憶體,也就是說每個物件都有個可以遞增或遞減的計數器。如果想使某個物件繼續存活,那就遞增其引用計數;用完之後,就遞減其計數。計數變為0,就表示無人關注此物件了,於是,就可以把它銷燬。

在引用計數架構下,物件有個計數器,用以表示當前有多少個事物想令此物件繼續存活下去。這在Objective-C中叫做保留計數,也叫引用計數。NSObject協議宣告瞭下面3個方法用於操作計數器,以遞增或遞減其值(ARC中不可用):

retain  // 遞增保留計數
release  // 遞減保留計數
autorelease  // 待稍後清理自動釋放池時再遞減保留計數

物件建立出來時,其保留計數至少為1.若想令其繼續存活,則呼叫retain方法。要是某部分程式碼不再使用此物件,那就呼叫release或autorelease方法。最終保留計數歸0時,物件就回收了。

這裡寫圖片描述

為了避免在不經意間使用了無效物件,一般呼叫完release之後都會清空指標。這就能保證不會出現可能指向無效物件的指標,這種指標通常被稱為懸掛指標。

NSMutableArray *array = [[NSMutableArray alloc] init];
// number建立,保留計數為1
NSNumber *number = [[NSNumber alloc] initWithInt:1337]; 
// 陣列引用了number,number保留計數為2
[array addObject:number];
// 釋放number,保留計數為1
[number release];
// 呼叫release後,無法保證number仍然存活(雖然在本例中可以明顯看到number還存活),應當清空指標。
number = nil;

屬性為strong時,屬性的setter方法是先保留新值再釋放舊值,然後更新例項變數,使其指向新值。

- (void)setFoo:(id)foo{
    [foo retain];
    [_foo release];
    _foo = foo;
}

執行順序呢很重要,假如還未保留新值就先把舊值釋放了,而且兩個值又指向同一個物件,那麼先執行的release操作就可能導致系統將此物件永久回收。而後續的retain操作則無法令這個已徹底回收的物件復生,例項變數就成為懸掛指標。

呼叫release會立刻遞減物件的保留計數(可能會令系統回收此物件),然而有時候可以不呼叫它,改為呼叫autorelease,此方法會在稍後遞減計數。(通常是下一次時間迴圈時遞減)

- (NSString*)stringValue{
    NSString *str = [[NSString alloc] initWithFormat:@"I am this %@",self];
    return str;
}

此時返回的str物件其保留計數比期望值要多1,因為呼叫alloc會令保留計數加1,而又沒有與之對應的釋放操作。也不能在方法內釋放str,否則還沒等方法返回,系統就把物件回收了。這裡應該使用autorelease,它會在稍後釋放物件。

- (NSString*)stringValue{
    NSString *str = [[NSString alloc] initWithFormat:@"I am this %@",self];
    return [str autorelease];
}

第30條:以ARC簡化引用計數

使用ARC時,引用計數實際上還是在執行。只不過保留與釋放操作現在是由ARC自動新增。因此直接在ARC下呼叫retain、release、autorelease、dealloc這些記憶體管理方法是非法的。

實際上,ARC在呼叫這些方法時,直接呼叫的是其底層C語言版本,如ARC會呼叫與retain等價的底層函式objc_retain,這樣做效能更好,這也是不能重寫retain、release、autorelease的原因。

ARC確立了方法名的硬性規定。若方法名以alloc、new、copy、mutableCopy開頭,其返回物件歸呼叫者所有。其他方法返回的物件並不歸呼叫者所有。


+ (EOCPerson*)newPerson{
    EOCPerson *person = [[EOCPerson alloc] init];
    return person;
    // 方法以new開頭,返回值歸呼叫者所有,釋放由呼叫者負責,ARC不會在這裡自動新增語句
}

+ (EOCPerson*)somePerson{
    EOCPerson *person = [[EOCPerson alloc] init];
    return person;
    // 返回值不歸呼叫者所有,ARC會自動將return person替換為等價return [person autorelease]的語句
}

- (void)doSomething{
    EOCPerson *person1 = [EPCPerson newPerson];
    EOCPerson *person2 = [EPCPerson somePerson];
    // ARC會自動新增等價[person1 release]的語句(person1是由這塊程式碼所有)
}

在應用程式中,可用下列修飾符來改變區域性變數與例項變數的語義:

__strong  // 預設語義,保留此值
__unsafe_unreatined  // 不保留此值,可能不安全,出現懸掛指標
__weak  // 不保留此值,安全,系統回收物件時會清空物件
__autorelease  // 把物件按引用傳遞給方法時,使用該修飾符。此值在方法返回時自動釋放。

在手動管理引用計數時,可能會像下面這樣來編寫dealloc方法清空例項變數

- (void)dealloc{
    [_foo release];
    [_bar release];
    [super dealloc];
}

用了ARC之後,就不需要再編寫這樣的dealloc方法了,ARC會自動清理記憶體。不過,如果有非Objective-C對像(如CoreFoundation)中的物件或是由malloc()分配在堆中的記憶體,仍然需要清理。在ARC中不能直接呼叫dealloc,但是可以重寫dealloc方法,ARC會自動執行此方法,並呼叫其中超類的dealloc方法。

- (void)dealloc{
    // 釋放非Objective-C物件
    CFRelease(_coreFoundationObject);
    // 釋放malloc()分配的堆記憶體
    free(_heapAllocatedMemoryBlob);
}

第31條:在dealloc方法中只釋放引用並解除監聽

物件在經歷其生命期後,最終會為系統所回收,這時就要執行dealloc方法了。在每個物件的生命週期內,此方法僅執行一次,也就是當保留計數降為0的時候。dealloc方法會由執行期系統呼叫,開發者不能自己呼叫。

ARC會自動釋放所有Objective-C物件,dealloc中需要手動釋放非Objective-C物件,除此之外,還需要把原來配置過的觀測行為都清理掉。如果用NSNotificationCenter給此物件註冊過某種通知,那麼一般應該在這裡登出。不然通知系統可能會把通知傳送給已回收的物件,引起系統崩潰。

- (void)dealloc{
    CFRelease(_coreFoundationObject);
    // 登出通知
    [[NSNotificationCenter defaultCenter] removeObserver:self];
}

雖說應該於dealloc中釋放引用,但是開銷較大或系統內稀缺資源不在此列。如檔案描述符、套接字、大塊記憶體,不然會導致保留稀缺資源時間過長。通常應該實現另一個方法,但應用程式用完資源後就呼叫此方法清理資源。再在dealloc中進行檢查,防止開發者忘記清理資源。

// 清理資源的方法
- (void)close{
    /* 清理資源 */
    _closed = YES;
}

- (void)dealloc{
    // 如果忘記清理資源,則輸出錯誤日誌並清理資源
    if(!_closed){
        NSLog(@"ERROR:close was not called before dealloc!");
        [self close];
    }
}

本例中,在dealloc呼叫了其它方法,不過是為了偵測程式設計錯誤而破例。正常情況下,不要在dealloc中隨便呼叫其他方法。因為物件已經處於正在回收狀態,如果在這裡呼叫的方法又要非同步執行某些任務,等任務結束後這個物件可能已經被徹底摧毀了,導致程式崩潰。

dealloc裡也不要呼叫屬性的存取方法,屬性可能正處於KVO機制的監控之下,屬性的觀察者可能會在屬性值改變時保留或使用這個即將回收的物件,導致錯誤。

第32條:編寫異常安全程式碼時留意記憶體管理問題

Objective-C的錯誤模型表明,異常只應在發生嚴重錯誤後丟擲,不過有時候仍然需要編寫程式碼來捕獲並處理異常。

使用手動計數時:

@try{
    EOCSomeClass *object = [[EOCSomeClass alloc] init];
    [object doSomethingThatMayThrow];
}
@catch(...){
    Nslog(@"There was an error!");
}
// 無論是否發生異常,@finally塊中的程式碼都會執行
@finally{
    [object release];
}

使用@finally塊可以在發生異常時也能釋放物件。

而ARC環境下,不能呼叫release,無法像手動管理那樣將釋放操作移到@finally塊中。ARC又不會自動處理,這種情況下可以開啟編譯器的-fobjc-arc-exceptions標誌來開啟ARC生成安全處理異常所用的附加程式碼。只是這段程式碼會嚴重影響執行期的效能,即使不丟擲異常。

所以一般來說,只有當應用程式必須因異常情況而終止時才應丟擲異常,這時候應用程式即將終止,是否發生記憶體洩漏已經無關緊要了,因此不用新增安全處理異常所用的附加程式碼了。如果有大量異常捕獲操作時,應考慮重構程式碼,用21條的NSError式錯誤資訊傳遞法來取代異常。

相關文章