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

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

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

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

先睹為快

33.以弱引用避免保留環

34.以自動釋放池塊降低記憶體峰值

35.用殭屍物件除錯記憶體管理問題

36.不要使用retainCount

目錄

第33條:以弱引用避免保留環

物件圖裡經常會出現一種情況,就是幾個物件都以某種方式相互引用,從而形成環。這種情況通常會洩漏記憶體,因為最後沒有別的東西會引用環中的物件。而環裡的物件會因為相互間的引用而繼續存活,不被系統回收。

#import <Foundation/Foundation.h>

@class EOCClassA;
@class EOCClassB;

@interface EOCClassA : NSObject
@property (nonatomic, strong) EOCClassB *other;
@end

@interface EOCClassB : NSObject
@property (nonatomic, strong) EOCClassA *other;
@end

本段程式碼中就可能出現了保留環,如果把EOCClassA例項的other屬性設定為了某個EOCClassB例項,而又把EOCClassB例項的other屬性設定成了這個EOCClassA例項。那麼兩個物件就會相互引用,出現保留環。

這裡寫圖片描述

避免保留環的最佳方式就是弱引用。這種引用經常用來表示非擁有關係。將屬性宣告為unsafe_unretained或weak即可。

#import <Foundation/Foundation.h>

@class EOCClassA;
@class EOCClassB;

@interface EOCClassA : NSObject
@property (nonatomic, strong) EOCClassB *other;
@end

@interface EOCClassB : NSObject
@property (nonatomic, weak) EOCClassA *other;
@end

修改之後,EOCClassB例項就不能再通過other屬性來擁有EOCClassA例項了。weak與unsafe_unretained的區別在於,系統把屬性回收後,weak屬性會自動設定為nil,而unsafe_unretained屬性仍然指向那個已經回收的例項,這樣可能會不安全。不過無論如何,只要所在物件已經被系統回收後,都不應該繼續使用弱引用。

第34條:以自動釋放池塊降低記憶體峰值

在執行迴圈體時,一般會持續有新物件建立出來,並加入自動釋放池中。這種物件都要等到迴圈執行完才會釋放。這樣一來,在執行迴圈時,應用程式所佔記憶體量會持續上漲,而等到所有臨時物件都釋放後,記憶體用量又會突然下降。

NSArray *databaseRecords = /* ... */;
NSMutableArray *people = [NSMutableArray new];
for(NSDictionary *record in databaseRecords){
    EOCPerson *person = [[EOCPerson alloc] initWithRecord:record];
    [people addObject:person];
}

這種情況不甚理想,尤其是迴圈長度無法預知時,再建立出一些臨時的EOCPerson物件,它們本該提早回收的。增加一個自動釋放池即可解決問題,把迴圈內的程式碼包裹在自動釋放池塊中,那麼迴圈體中自動釋放的物件就會在這個池,而不是執行緒的主池裡:

NSArray *databaseRecords = /* ... */;
NSMutableArray *people = [NSMutableArray new];
for(NSDictionary *record in databaseRecords){
    @autoreleasepool{
        EOCPerson *person = [[EOCPerson alloc] initWithRecord:record];
        [people addObject:person];
    }
}

加上自動迴圈池之後,就會降低應用程式在執行迴圈時的記憶體峰值。因為系統會在塊的末尾將臨時物件回收掉。如果迴圈的記憶體用量不高,則儘量不建立額外的自動迴圈池,因為自動釋放池塊還是存在開銷(雖然不大)。

在ARC出現之前一般使用NSAutoreleasePool物件,這樣可以不用每次迴圈都清空池,通常用來建立偶爾需要清空的池:

NSArray *databaseRecords = /* ... */;
NSMutableArray *people = [NSMutableArray new];
int i = 0;

// 建立自動釋放池會被推入棧中,在物件上執行autorelease等於將其放到棧頂的自動釋放池中。
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
for(NSDictionary *record in databaseRecords){
    EOCPerson *person = [[EOCPerson alloc] initWithRecord:record];
    [people addObject:person];

    // 每執行10次迴圈,清空一次自動釋放池
    if (++i == 10){
        [pool drain];
        i = 0;;
    }
}
// 結束迴圈後,再次清空自動釋放池
[pool drain];

第35條:用殭屍物件除錯記憶體管理問題

向已回收的物件傳送訊息是不安全的。這麼做是否可行完全取決於物件所佔記憶體有沒有為其他內容所覆寫。Cocoa提供了殭屍物件這個方便的功能。啟用這項除錯功能後,執行期系統會把所有已經回收的例項轉化為特殊的殭屍物件,而不會真正回收它們。殭屍物件收到訊息後,會丟擲異常,其中準確說明了傳送過來的訊息,並描述回收之前的那個物件。殭屍物件是除錯記憶體管理的最佳方式。

在Xcode中打啟用殭屍物件:點選下圖中左上角標註的位置選擇 Edit Scheme,再選擇run中的Diagnostics分頁,勾選Enabled Zombine Objects選項

這裡寫圖片描述

下面程式碼就演示普通物件轉換為殭屍物件的過程
注意:採用的是手動計數,在Build Settings中將Objective-C Automatic Reference Counting設為NO即可不用ARC。

#import <Foundation/Foundation.h>
#import <objc/runtime.h>

@interface EOCClass : NSObject
@end

@implementation EOCClass
@end

void PrintClassInfo(id obj){
    Class cls = object_getClass(obj);
    Class superCls = class_getSuperclass(cls);
    NSLog(@"=== %s : %s ===", class_getName(cls), class_getName(superCls));
}

int main(int argc, const char * argv[]) {
    EOCClass *obj = [[EOCClass alloc] init];
    NSLog(@"Before release:");
    PrintClassInfo(obj);

    [obj release];
    NSLog(@"After release");
    PrintClassInfo(obj);
    return 0;
}

執行結果:

2016-07-27 14:47:31.096 MRR Orders[89086:765092] Before release:
2016-07-27 14:47:31.097 MRR Orders[89086:765092] === EOCClass : NSObject ===
2016-07-27 14:47:31.097 MRR Orders[89086:765092] After release
2016-07-27 14:47:31.097 MRR Orders[89086:765092] === _NSZombie_EOCClass : nil ===

物件所屬的類已經由EOCClass變為NSZombie_EOCClass。這個類是程式碼中沒有定義的,在執行期生成的。編譯器首次遇到EOCClass類物件要變成殭屍物件時,就會在類名前加上_NSZombie字首生成對應的殭屍類。

殭屍類只是充當一個標記,它的作用會在訊息轉發過程中體現出來。當執行到完整轉發時,“forwarding”函式會檢查物件所屬的類名,若名稱字首為NSZombie,表明訊息接收者是殭屍物件,需要特殊處理,此時會列印一條訊息,其中指明殭屍物件收到的訊息及原來所屬的類(殭屍類去掉字首),然後應用程式終止。

在之前程式碼末尾加上一句程式碼向殭屍物件傳送訊息:

int main(int argc, const char * argv[]) {
    EOCClass *obj = [[EOCClass alloc] init];
    NSLog(@"Before release:");
    PrintClassInfo(obj);

    [obj release];
    NSLog(@"After release");
    PrintClassInfo(obj);

    // 向殭屍物件傳送訊息
    [obj description];
    return 0;
}

執行結果

2016-07-27 15:02:32.822 MRR Orders[89855:774958] Before release:
2016-07-27 15:02:32.823 MRR Orders[89855:774958] === EOCClass : NSObject ===
2016-07-27 15:02:32.823 MRR Orders[89855:774958] After release
2016-07-27 15:02:32.823 MRR Orders[89855:774958] === _NSZombie_EOCClass : nil ===
2016-07-27 15:02:32.823 MRR Orders[89855:774958] *** -[EOCClass description]: message sent to deallocated instance 0x1006002e0

可以看到殭屍物件原來所屬的類,收到的選擇器以及對應的指標值都列印出來了。

第36條:不要使用retainCount

Objective-C通過引用計數來管理記憶體,每個物件都有一個計數器,其值表明還有多少個其他物件想令此物件繼續存活。NSObject協議中定義了下列方法,用於查詢物件當前的保留計數:

- (NSUInteger)retainCount

ARC中已經廢棄此方法了,非ARC環境仍然可用,但是不應該用。
首要原因在於:它所返回的保留計數只是某個給定時間點上的值,並未考慮稍後清空自動釋放池,因此未必能真是反應實際的保留計數。

while([object reatinCount]){
    [object release];
}

這種寫法的錯誤在於,它沒有考慮後續的自動釋放操作,假如物件在自動釋放池中,稍後系統清空池子還要再釋放物件一次,引起程式崩潰。而且reatinCount可能永遠不返回0,因為有時系統會優化物件的釋放行為,在保留計數還是1的時候就把它回收了。如果物件已經回收了,迴圈還在進行,也會導致程式崩潰。

reatinCount返回的保留計數具體值也不一定有用

NSString *string = @"Some string";
NSLog(@"string retainCount = %lu",[string retainCount]);

NSNumber *numberI = @1;
NSLog(@"numberI retainCount = %lu",[numberI retainCount]);

NSNumber *numberF = @3.14f;
NSLog(@"numberF retainCount = %lu",[numberF retainCount]);

執行結果:

2016-07-27 15:16:59.776 MRR Orders[90612:784462] string retainCount = 18446744073709551615
2016-07-27 15:16:59.777 MRR Orders[90612:784462] numberI retainCount = 9223372036854775807
2016-07-27 15:16:59.777 MRR Orders[90612:784462] numberF retainCount = 1

第一個物件的保留計數是2的64次方減1,第二個是2的63次方減一.由於二者都是單例物件,所以其保留計數都很大。系統會盡可能把NSString實現成單例物件,NSNumber也類似,它使用了一種叫做標籤指標的概念來標註特定型別的數值,將有關資訊都存放在指標值裡。由於浮點數沒有此優化,所以保留計數為1。

對於單例物件來說,保留計數永遠不會變,保留及釋放都是空操作。

由於物件可能出在自動釋放池中,其保留計數未必如想象般精確,而且其他程式庫也可能自行保留或釋放物件,者都會擾亂計數的具體取值。所以任何情況下都不要使用retainCount。

相關文章