高階記憶體管理程式設計指南-實用的記憶管理

pengyuan_D發表於2019-02-12

實用的記憶管理

雖然記憶體管理策略中描述的基本概念很簡單,但您可以採取一些實際步驟來簡化記憶體管理,並幫助確保您的程式保持可靠和健壯,同時最大限度地減少其資源需求。

使用訪問器方法使記憶體管理更容易

如果您的類具有作為物件的屬性,則必須確保在您使用它時不會釋放任何設定為該值的物件。因此,您必須在設定物件時宣告物件的所有權。您還必須確保放棄任何當前持有的價值的所有權。

有時它可能看起來很乏味或迂腐,但如果你一直使用訪問器方法,那麼記憶體管理問題的可能性會大大降低。如果您在整個程式碼中使用retainrelease例項變數,那麼您幾乎肯定會做錯事。

考慮一個您想要設定其計數的 Counter 物件。

@interface Counter:NSObject
@property(非原子,保留)NSNumber *計數;
@結束;

屬性宣告瞭兩種訪問方法。通常,您應該要求編譯器合成方法;但是,瞭解它們如何實施是有益的。

在 “get” 訪問器中,您只需返回合成的例項變數,因此不需要retainrelease

- (NSNumber *)count {
    return _count;
}

在 “設定” 方法中,如果其他所有人都按照相同的規則進行遊戲,則必須假設新計數可以隨時處理,因此您必須取得物件的所有權 - 通過向其傳送retain訊息 - 以確保贏得不會。您還必須通過向其傳送release訊息來放棄舊計數物件的所有權。(nil在 Objective-C 中允許傳送訊息,因此如果_count尚未設定,實現仍然有效。)如果[newCount retain]兩個是同一個物件,你必須傳送它 - 你不想無意中導致它要解除分配。

- (void)setCount:(NSNumber *)newCount {
    [newCount保留];
    [_count release];
    //進行新的作業。
    _count = newCount;
}

使用訪問器方法設定屬性值

假設您要實現重置計數器的方法。你有幾個選擇。第一個實現建立NSNumber例項alloc,所以你用 a 平衡它release

- (void)reset {
    NSNumber * zero = [[NSNumber alloc] initWithInteger:0];
    [self setCount:zero];
    [零釋放];
}

第二個使用便利建構函式來建立新NSNumber物件。因此不需要retainrelease訊息

- (void)reset {
    NSNumber * zero = [NSNumber numberWithInteger:0];
    [self setCount:zero];
}

請注意,兩者都使用 set 訪問器方法。

對於簡單的情況,以下幾乎肯定會正常工作,但是儘可能避免使用訪問器方法,這樣做幾乎肯定會在某個階段導致錯誤(例如,當您忘記保留或釋放時,或者如果例項變數的記憶體管理語義更改)。

- (void)reset {
    NSNumber * zero = [[NSNumber alloc] initWithInteger:0];
    [_count release];
    _count =零;
}

另請注意,如果使用鍵值觀察,則以這種方式更改變數不符合 KVO。

不要在初始化方法和 dealloc 中使用訪問器方法

您不應該使用訪問器方法來設定例項變數的唯一地方是初始化方法和dealloc。要使用表示零的數字物件初始化計數器物件,您可以實現init如下方法:

- 在裡面 {
    self = [super init];
    if(self){
        _count = [[NSNumber alloc] initWithInteger:0];
    }
    迴歸自我;
}

要允許使用非零計數初始化計數器,您可以實現initWithCount:如下方法:

-  initWithCount:(NSNumber *)startingCount {
    self = [super init];
    if(self){
        _count = [startingCount copy];
    }
    迴歸自我;
}

由於 Counter 類具有物件例項變數,因此還必須實現dealloc方法。它應該通過向它們傳送release訊息來放棄任何例項變數的所有權,並最終應該呼叫 super 的實現:

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

使用弱引用來避免保留週期

保留物件會建立對該物件的強引用。在釋放所有強引用之前,不能釋放物件。因此,如果兩個物件可能具有迴圈引用,則會出現一個被稱為保留週期的問題 - 也就是說,它們彼此之間具有強烈的引用(直接或通過一系列其他物件,每個物件都強烈引用下一個物件回到第一個)。

圖 1 中所示的物件關係說明了潛在的保留週期。Document 物件具有文件中每個頁面的 Page 物件。每個 Page 物件都有一個屬性,用於跟蹤它所在的文件。如果 Document 物件具有對 Page 物件的強引用,並且 Page 物件具有對 Document 物件的強引用,則任何物件都不能被釋放。在釋放 Page 物件之前,Document 的引用計數不能為零,並且在取消分配 Document 物件之前不會釋放 Page 物件。

圖 1   週期性參考的圖示

保留週期的例證

保留週期問題的解決方案是使用弱引用。甲弱引用是一個非所屬關係,其中源物件不保留它具有參考的物件。

但是,為了保持物件圖完好無損,必須在某處有強引用(如果只有弱引用,則頁面和段落可能沒有任何所有者,因此將被釋放)。因此,可可建立了一個慣例,即 “父母” 物件應該對其 “孩子” 保持強烈的引用,並且孩子們應該對父母的弱引用。

因此,在圖 1 中,文件物件具有對(保留)其頁面物件的強引用,但頁面物件具有對(不保留)文件物件的弱引用。

Cocoa 中弱引用的示例包括但不限於表資料來源,大綱檢視項,通知觀察器以及其他目標和委託

您需要注意將訊息傳送到僅包含弱引用的物件。如果在取消分配物件後向物件傳送訊息,則應用程式將崩潰。物件有效時,您必須具有明確定義的條件。在大多數情況下,弱引用物件知道另一個物件對它的弱引用,就像迴圈引用的情況一樣,並且負責在解除分配時通知另一個物件。例如,當您向通知中心註冊物件時,通知中心會儲存對該物件的弱引用,並在釋出相應的通知時向其傳送訊息。取消分配物件後,需要將其登出到通知中心,以防止通知中心向物件傳送任何進一步的訊息,不再存在。同樣,當取消分配委託物件時,您需要通過傳送一個來刪除委託連結setDelegate:帶有nil另一個物件的引數的訊息。這些訊息通常是從物件的dealloc方法傳送的。

避免導致重新分配您正在使用的物件

Cocoa 的所有權策略指定接收的物件通常應該在呼叫方法的整個範圍內保持有效。還應該可以從當前範圍返回接收到的物件而不用擔心它被釋放。對於您的應用程式而言,物件的 getter 方法返回快取的例項變數或計算值應該無關緊要。重要的是該物件在您需要的時間內仍然有效。

此規則偶爾會有例外情況,主要分為兩類。

  1. 從一個基本集合類中刪除物件時。

    heisenObject = [array objectAtIndex:n];
    [array removeObjectAtIndex:n];
    // heisenObject現在可能無效。

    當從一個基本集合類中刪除一個物件時,會傳送一個release(而不是autorelease)訊息。如果集合是已刪除物件的唯一所有者,heisenObject則會立即釋放已刪除的物件(在示例中)。

  2. 當 “父物件” 被釋放時。

    id parent = <#create父物件#>;
    // ...
    heisenObject = [父子];
    [家長髮布]; //或者,例如:self.parent = nil;
    // heisenObject現在可能無效。

    在某些情況下,您從另一個物件檢索物件,然後直接或間接釋放父物件。如果釋放父節點導致它被釋放,並且父節點是子節點的唯一所有者,那麼子節點(heisenObject在示例中)將同時被釋放(假設在父節點中傳送它release而不是autorelease訊息)dealloc方法)。

為了防止這些情況發生,您heisenObject在收到它後會保留,並在完成後將其釋放。例如:

heisenObject = [[array objectAtIndex:n] retain];
[array removeObjectAtIndex:n];
//使用heisenObject ...
[heisenObject釋出];

不要使用 dealloc 來管理稀缺資源

您通常不應該管理方法中的稀缺資源,例如檔案描述符,網路連線以及緩衝區或快取dealloc。特別是,您不應該設計類,以便dealloc在您認為將呼叫它時呼叫它們。dealloc由於錯誤或應用程式拆除,呼叫可能會被延遲或迴避。

相反,如果你有一個例項管理稀缺資源的類,你應該設計你的應用程式,以便你知道何時不再需要資源,然後可以告訴例項在那時 “清理”。您通常會釋放該例項,然後dealloc關注,但如果不這樣做,您將不會遇到其他問題。

如果你試圖在上面捎帶資源管理,可能會出現問題dealloc。例如:

  1. 物件圖拆除的順序依賴性。

    物件圖拆除機制本質上是無序的。雖然您通常會期望並獲得特定訂單,但您會引入脆弱性。如果物件意外地自動釋放而不是例如釋放,則拆卸順序可能會改變,這可能會導致意外結果。

  2. 不回收稀缺資源。

    記憶體洩漏是應該修復的錯誤,但它們通常不會立即致命。但是,如果在您希望釋放資源時沒有釋放稀缺資源,則可能會遇到更嚴重的問題。例如,如果您的應用程式用完了檔案描述符,則使用者可能無法儲存資料。

  3. 在錯誤的執行緒上執行清理邏輯。

    如果一個物件在意外的時間自動釋放,它將在它碰巧進入的任何執行緒的自動釋放池塊中被釋放。對於只能從一個執行緒觸及的資源來說,這很容易致命。

集合擁有它們包含的物件

將物件新增到集合(例如陣列,字典或集合)時,集合將獲得物件的所有權。當從集合中移除物件或集合本身被釋放時,集合將放棄所有權。因此,例如,如果要建立數字陣列,可以執行以下任一操作:

NSMutableArray * array = <#Get a mutable array#>;
NSUInteger i;
// ...
for(i = 0; i <10; i ++){
    NSNumber * convenienceNumber = [NSNumber numberWithInteger:i];
    [array addObject:convenienceNumber];
}

在這種情況下,您沒有呼叫alloc,因此無需呼叫release。沒有必要保留新的數字(convenienceNumber),因為陣列會這樣做。

NSMutableArray * array = <#Get a mutable array#>;
NSUInteger i;
// ...
for(i = 0; i <10; i ++){
    NSNumber * allocedNumber = [[NSNumber alloc] initWithInteger:i];
    [array addObject:allocedNumber];
    [allocedNumber release];
}

在這種情況下,你需要傳送allocedNumber一個release該範圍內的訊息for迴圈平衡alloc。由於陣列在新增時保留了數字,因此在陣列中addObject:它不會被釋放。

要理解這一點,請將自己置於實現集合類的人的位置。你想要確保沒有給你照看的物件從你身下消失,所以retain當你傳入它們時你會傳送一條訊息。如果它們被刪除,你必須傳送一個平衡release訊息,任何應該release在您自己的dealloc方法中向剩餘物件傳送訊息。

使用保留計數實施所有權政策

所有權政策通過引用計數實現 - 通常在retain方法之後稱為 “保留計數” 。每個物件都有一個保留計數。

  • 建立物件時,其保留計數為 1。

  • 向物件傳送retain訊息時,其保留計數增加 1。

  • 向物件傳送release訊息時,其保留計數減 1。

    向物件傳送autorelease訊息時,其保留計數在當前自動釋放池塊的末尾減 1。

  • 如果物件的保留計數減少到零,則將其取消分配。

重要提示:  沒有理由明確詢問物件的保留計數是什麼(請參閱參考資料retainCount)。結果通常會產生誤導,因為您可能不知道哪些框架物件保留了您感興趣的物件。在除錯記憶體管理問題時,您應該只關心確保程式碼遵守所有權規則。

 

相關文章