跳轉到內容

為初學者編寫 Mac OS X Cocoa 程式/一些 Cocoa 核心原則

來自 Wikibooks,開放世界中的開放書籍

上一頁:Objective C,語言及其優勢 | 下一頁:構建 GUI

我們已經看到了 Cocoa 的基本操作。在上一章中,我們討論了從 NSObject 繼承,以及如何確保我們呼叫物件的初始化方法鏈。在這裡,我們將更詳細地討論這一點,因為它非常重要。然後,我們將討論記憶體管理。

正確初始化物件是你的責任。與 C++ 不同,沒有自動的建構函式方法,也沒有自動的解構函式方法。但是,只要我們瞭解一些簡單的規則,我們可以很容易地做到這一點。遵循這些規則對於在 Cocoa 中取得成功至關重要。

當我們例項化一個物件時

id   myObject = [NSString alloc];

我們得到的只是一塊未初始化的記憶體。如果我們現在使用這個物件,我們很可能不會得到我們想要的結果。它甚至可能會崩潰。我們必須呼叫初始化方法

id   myObject = [[NSString alloc] init];

然後我們就可以了。你會經常看到這種 alloc/init 組合。那麼為什麼 alloc 不僅僅在其內部呼叫 init 並完成它呢?這難道不會為我們節省時間並減少忘記的可能性嗎?是的,除了一個問題。有時我們需要一個不同的 init 版本,它需要一個引數。由於 alloc 不知道我們想要哪個,它將呼叫權留給了我們。

每個物件都有一個 init 版本,它充當“主” init - 當我們建立該物件時必須呼叫的 init。事物被安排好,以便任何其他 init 變體都會呼叫它,從而使一切正常運作。主 init 方法被稱為指定初始化程式。在大多數情況下,這將是普通的“init”方法,因此使用任何其他變體將在內部呼叫 init。如果情況並非如此,文件應指示指定初始化程式。

“init”只是一個普通的 方法呼叫,它沒有像 C++ 建構函式那樣的特殊狀態。因此,你有責任正確編寫 init 中的程式碼,確保你首先呼叫你的父類的指定初始化程式(或任何最終透過指定初始化程式呼叫的其初始化程式方法)。完成此操作後,你應該初始化你自己的物件部分。

初始化允許失敗。例如,你的物件可能想要分配一大塊記憶體。如果記憶體不可用,你應該清理已經分配的任何內容,然後從你的 init 方法返回 nil。由於 nil 物件可以安全呼叫,這意味著記憶體不足(例如)不會導致程式崩潰。

當一個物件不再需要時,它的“dealloc”方法將被呼叫。如果你在初始化或物件的操作過程中分配了任何內容,你需要覆蓋此方法並釋放它。完成後,你應該呼叫父類的 dealloc 方法。你將經常覆蓋 dealloc,所以要熟悉如何做到這一點。但是,dealloc 很少被直接呼叫。相反,它在物件被釋放時被間接呼叫。

保留和釋放

[編輯 | 編輯原始碼]

每個繼承自 NSObject 的物件都實現了 retain 和 release。這是一種簡單的記憶體管理技術,依賴於引用計數。物件跟蹤程式中對其的引用數量。當沒有更多引用時,計數為零,它將被釋放。在 Cocoa 中,引用計數通常被稱為保留計數。它是一樣的東西。

retain 方法增加計數,release 方法減少計數。當計數達到零時,dealloc 會自動被呼叫。

當一個物件第一次被建立時,它的保留計數被設定為 1。因此

id  myObject = [[NSString alloc] init];

“myObject”的保留計數為 1,因為只有一個對它的引用。

如果我們然後執行

[myObject release];

它將立即被釋放,因為計數從 1 減少到 0,並且 dealloc 被呼叫。透過呼叫 release,我們告訴 Cocoa 我們不再對這裡這個物件感興趣,也不會再引用它。

有些人覺得 retain 和 release 很令人困惑,但規則實際上很簡單。如果你建立了一個物件,你負責釋放它。如果你建立了對現有物件的第二個引用,你負責釋放它。你使用 alloc 或 copy 在另一個物件上建立了一個物件,因此在你使用這些時,你需要了解 retain/release 責任。這是你的責任。如果你忘記釋放,事情可能會順利進行,但你會有一個記憶體洩漏,這可能會在未來造成問題。

讓我們看一下第二部分,建立第二個引用。假設你有一個物件,它是另一個物件的一部分。我們在前面的示例中看到了這一點,其中我們的 GCHelloView 具有 NSString 作為資料成員。當我們從外部傳入一個 NSString 時,我們不知道是誰建立了它,誰對它負責 - 我們只知道它不是我們!但是,透過將其儲存為資料成員,我們正在建立對該同一字串的另一個引用,因此我們必須保留它。我們之前存在的任何字串呢?嗯,我們建立了對它的引用,因此我們必須釋放它。

 - (void)		setText:(NSString*) text withSize:(int) size
 {
	[text retain];
	[_text release];
	_text = text;
	_size = size;
	[self setNeedsDisplay:YES];
 }

所以現在我們可以看到這裡發生了什麼。我們保留“text”,因為我們正在建立對它的另一個引用,我們的 _text 資料成員,並且我們正在釋放上一個值,因為我們不再關心它。我們不在乎字串最初來自哪裡,也不知道是誰建立的,我們已經完成了它,所以我們釋放它。如果其他人仍然保留它,它將繼續存在,但如果沒有,它將被釋放。現在,如果“text”和“_text”實際上是同一個字串呢?這很容易發生。如果我們首先釋放舊的,它可能會被釋放。接下來的 retain 將作用於一個過時的引用,這很可能會導致崩潰(注意,與訪問 nil 不同,訪問已釋放的物件是不安全的)。因此,透過在釋放之前保留,我們避免了這種潛在的障礙。“保留在釋放之前”是一種常見且合理的做法。

請注意,物件的保留非常有效,因為只需要增加一個簡單的計數。如果你需要對每個物件進行復制,這不僅會慢得多,而且你還可能導致不同的副本彼此不同步,並使用比必要更多的記憶體。

自動釋放

[編輯 | 編輯原始碼]

當我們建立一個物件,但我們想要將其傳遞給其他人時會發生什麼?我們如何確保它在被另一個物件保留之前保持有效,然後我們再釋放它?看看這個

id myObject = [[NSString alloc] init];
return myObject;

接收者知道這個物件需要被釋放嗎?如果沒有人釋放這個物件,那麼我們就會有記憶體洩漏。為了消除釋放的負擔,我們希望在接收者完成操作後將其標記為釋放。自動釋放將物件指定為稍後釋放,它表示“保留物件,直到下一個記憶體池被清空”。如果你沒有明確地清空池,那麼你可以確保返回值將保留在接收者的直接範圍內。

id myObject = [[[NSString alloc] init] autorelease];
return myObject;

這段程式碼是可以的,因為返回的物件是有效的,但所有權的責任已經傳遞給了池。

當控制權返回到主事件迴圈時,池可以確定對物件感興趣的任何物件都會保留它,因此它會釋放它所擁有的所有物件。任何沒有引用的物件將被刪除,任何有引用的物件將成為保留它的物件的擁有者。一切都保持井井有條。

大多數工廠方法返回自動釋放的物件。這是完全合理的,如果你編寫一個工廠方法,你應該這樣做。例如

NSColor* red = [NSColor redColor];

NSColor 的 redColor 類方法返回一個自動釋放的顏色物件。如果要保留此物件,則應保留它。如果只想在幾行程式碼中使用它,則無需執行任何操作 - 它將在稍後自動刪除,但僅在您完成後才會刪除。

在實踐中,保留、釋放和自動釋放很簡單。有些人似乎覺得它很混亂,但也許他們認為它比實際更復雜。要保留給你的物件,請保留它。你建立的物件已經保留了。要丟棄它不再引用它,請釋放它。如果你寫了一個工廠方法,就自動釋放它。如果您只是在短時間內使用工廠方法返回的物件,請什麼也不做。

物件值

[編輯 | 編輯原始碼]

當你的物件的屬性是另一個物件,而其他人想讀取該屬性時會怎樣?你應該保留、自動釋放,還是什麼?不,你什麼也不應該做。你已經保留它了,因為它是你自己的屬性。如果呼叫者想保留它,那是他們的事,自動釋放與它無關,因為它不是工廠方法。所以直接返回物件。所以

- (NSString*) text
{
   return _text;
}

絕對正確。

請注意,如果呼叫者確實保留了它,你仍然可以自由地在完成它後釋放它。錯誤的做法是呼叫者在想在較長時間內使用該值(比當前事件更長的時間)時,沒有保留它。這不是一個新規則 - 如果呼叫者建立對物件的引用,它必須保留它。但是,如果它只需要在短時間內使用該物件,例如在一個函式中,它可以簡單地這樣做,而不必擔心它會被其他人釋放。

事件迴圈

[編輯 | 編輯原始碼]

我們已經接觸過事件迴圈,但沒有解釋它是什麼。在 Cocoa 應用程式中,使用者活動會導致 *事件*。這些可能是滑鼠點選或拖動、鍵盤輸入、選擇選單項等等。其他事件可以自動生成,例如定時器定期觸發,或者網路上出現某些內容。對於每個事件,Cocoa 都期望有一個物件或物件組準備好適當地處理該事件。事件迴圈是檢測和路由這些事件到適當位置的地方。每當 Cocoa 沒有做其他事情時,它就坐在事件迴圈中等待事件到來。(事實上,Cocoa 不會像建議的那樣輪詢事件,而是它的主執行緒進入睡眠狀態。當事件到來時,作業系統會喚醒執行緒並恢復事件處理。這比輪詢效率高得多,並允許其他應用程式更流暢地執行)。

每個事件都被視為一個獨立的事物,然後事件迴圈獲得下一個事件,依此類推。如果事件導致需要更新,則會在事件結束時檢查此更新,如果需要,則執行視窗重新整理。

上一頁:Objective C,語言及其優勢 | 下一頁:構建 GUI

華夏公益教科書