參考資料:
How To Debug Memory Leaks with XCode and Instruments Tutorial | Ray Wenderlich
http://www.raywenderlich.com/2696/how-to-debug-memory-leaks-with-xcode-and-instruments-tutorial
一. 環境:
A. Mac OS X: 10.6.6 Snow Leopard
B. Xcode: 4.0
二. 相關軟體下載:
http://www.raywenderlich.com/downloads/LeakyApp.zip
三. 用 Xcoe 開啟下載的專案並執行 (LeakyApp.zip 解壓縮後, 產生: PropMemFun 專案)
A. 當 Simulator 跑起來時, 可以在 table view 看到 sushi(壽司) 的清單.
但是卻不知道產生問題的明確地方.
1. 在專案的執行設定項目裡, 設定 NSZombieEnabled 參數;
通常可以縮小問題的範圍.
2. 執行 Apple 的 Instruments, 例如用 Leaks 來查找記憶體的問題.
3. 在程式碼中設置 breakpoint, 然後一步步地縮小範圍尋找造成 crash 的原因.
4. 先將程式碼作註解(comment)直到它正常運作, 然後慢慢地反向取消註解
(backtrack).
四. 啟用 NSZombieEnabled 方式: (圖誤)
deallocated 的物件, 它就會提供相關的警告訊息. 此外, 最常造成應用程
式 crash 的原因就是去存取 deallocated 的記憶體.
B. 設置方式:
1. 點取左上方 active scheme 的下拉式選單, 選擇 Edit Scheme...
(在此為: Run PropMemFun), 然後點選右上方 "Arguments" 頁籤,
於 "Environment Variables" 的地方按下 "+" 號
新增: Name: NSZombieEnabled Value: YES
最後按下 "OK".
可以在最下方的 console log 看到訊息:
2011-03-16 16:29:39.572 PropMemFun[2946:207] ***
-[CFString respondsToSelector:]: message sent to deallocated instance ...
在此為: tableView:didSelectRowAtIndexPath 方法.
1. 發生 crash 的程式碼:
NSString *sushiName = [_sushiTypes objectAtIndex:indexPath.row];
NSString *sushiString = [NSString stringWithFormat:@"%d: %@", indexPath.row, sushiName];
NSString *message = [NSString stringWithFormat:@"Last sushi: %@. Cur sushi: %@", _lastSushiSelected, sushiString];
UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"Sushi Power!"
message:message
delegate:nil
cancelButtonTitle:nil
otherButtonTitles:@"OK", nil];
[alertView show];
_lastSushiSelected = sushiString;
2. 由於錯誤訊息指出: message sent to deallocated instance ...
在上方的程式碼中, 被標示的那行使用了二個 strings:
_lastSushiSelected 與 sushiString.
3. sushiString 看起來是 OK 的, 因為它使用 stringWithFormat 來作初始化
(會回傳 autorelease 的變數), 所以在下次回圈之前使用, 應該都是安全的.
4. 看看 _lastSushiSelected, _lastSushiSelected 的值是上一次此方法被呼叫時,
由 sushiString 設的; 但是 sushiString 是一個 autorelease variable, 所以在
某個時間點 sushiString 會被 release 掉, 其記憶體就會被 deallocated.
5. 但是 _lastSushiSelected 仍然會被指到 deallocated memory! 這就解釋了
問題所在:
發送一個訊息(message) 給 deallocated memory 造成了程式 crash.
6. 解決方式: 我們只要將 sushiString retain 一份參照給 _lastSushiSelected 使用,
這樣記憶體就不會遺失. 因此如下所示, 修改程式碼最後一行並且編譯
執行就不會再 crash 了.
_lastSushiSelected = [sushiString retain];
五. 使用 Build and Analyze 功能檢查記憶體洩露問題:
A. 說明:
1. 目前應用程式已經不會 crash 了, 接下來要確認是否有
記憶體洩露(memory leaks)的問題.
2. 有一個簡單的方法用來執行掃視你的應用程式, 看看是否有記憶體洩露或
其它的問題 ------> 使用 Xcode 內建的 Build 與 Analyze 功能.
3. 這會讓 Xcode 跑遍所有的程式碼, 並且找尋它能自動偵測到的錯誤, 然後
對任何潛在的問題發出警告. Xcode 並不會捕捉到所有的錯誤, 但是一但
它捕捉到錯誤的地方, 的確會讓你快速且容易地發現問題所在.
B. 使用步驟:
1. 執行: Product > Build
可以看到它偵測到了一個記憶體洩露的問題, 如下所示:
Potential leak of an object allocated on line 144 and stored into 'alertView'
訊息說明了有一個跟 "alertView" 有關的潛在的洩露問題.
你會發現 UIAlertView 是由 alloc/init 所建立的
(它會回傳一個 reference count 為1的物件),
但是從未被 released! 其中一種解決的辦法是在 [alertView show] 之後,
作 release 的處理, 如下所示:
[alertView show];
[alertView release];
重新再作一次 Build & Analyze, 就不會有問題了.
六. 使用 Leaks Instrument 工具檢查記憶體洩露問題:
A. 說明: 你無法利用 Build & Analyze 捕捉所有的東西; 有另一個偉大的自動工具
可以幫助你檢查應用程式的洩露問題 ------> Leaks Instrument
B. 使用步驟:
1. 執行: Product > Build For > Build For Profiling:
2. 執行: Product > Perform Action > Profile Without Building:
3. 先選擇左邊 iOS Simulator 下的 Memory, 接著選擇右邊的 "Leaks",
再按下 "Profile".
在 Table View 點選數列, 接著將 Table View 從最上方捲到最下方再捲回來.
(scroll up and down)
5. 一小段時間後, 就可以在 Leaks tab 上看到一些 leaks 開始冒出,
如底下 blue bar 所示:
點取 "Leaked Blocks" 改選為 "Call Tree".
"Hide System Libraries" 勾選起來.
8. 然後你就可以在右邊看到: 在程式中有二個不同的方法有 memory leaks 的問題.
記憶體洩露的程式碼位置.
C-1. 針對: tableView:didSelectRowAtIndexPath
以下的程式碼是產生 Leaks 的主因, 一步一步來思考原因:
NSString *sushiString = [NSString stringWithFormat:@"%d: %@", indexPath.row, sushiName];
1. sushiString 是由 stringWithFormat 建立的, 它會回傳 retain count 為 1 的物件,
伴隨著不確定的 autorelease 機制.
2. 在此方法的最後一行: _lastSushiSelected = [sushiString retain];
對 sushiString 傳送了 retain 訊息, (使得 sushiString 的 retain count 上升為 2)
並將其儲存到 _lastSushiSelected 裡.
3. 之後, autorelease 的影響, 將 sushiString 的 retain count 下降為 1.
4. 下一次, 當 tableView:didSelectRowAtIndexPath 方法再被呼叫時,
_lastSushiSelected 利用指標指到一個新的字串(sushiString),
將自身原本的舊變數覆寫(override) 了, 而沒有 release 它.
因此舊的 _lastSushiSelected 的 retain count 仍然為 1,
並且永遠不會被 release 掉.
5. 解決方法之一是在 _lastSushiSelected = [sushiString retain] 之前,
先將 _lastSushiSelected release 掉.
[_lastSushiSelected release];
_lastSushiSelected = [sushiString retain];
C-2. 針對: tableView:cellForRowAtIndexPath
以下的程式碼是產生 Leaks 的主因, 一步一步來思考原因:
NSString *sushiString = [[NSString alloc] initWithFormat:@"%d: %@", indexPath.row, sushiName];
1. 新字串使用 alloc/init 的方式建立.
2. 它會傳回 reference count 為 1 的物件.
3. 然而, 這個 reference count 不會被減少, 所以就產生了 memory leak!
4. 解決方式有三種:
method 01:
在 cell.textLabel.text = sushiString 之後, 對 sushiString 傳送 release 訊息:
---------------------------------------------------------------------------------------------------------
NSString *sushiString = [[NSString alloc] initWithFormat:@"%d: %@", indexPath.row, sushiName];
cell.textLabel.text = sushiString;
[sushiString release]; // method 01
method 02:
於建立字串的 alloc/init 之後, 傳送 autorelease 訊息:
--------------------------------------------------------------------------------
// method 02
NSString *sushiString = [[[NSString alloc] initWithFormat:@"%d: %@", indexPath.row, sushiName] autorelease];
method 03:
使用 stringWithFormat 取代 alloc/init 來建立字串,
這樣會傳回一個標記為 autorelease 的字串:
---------------------------------------------------------------------------
NSString *sushiString = [NSString stringWithFormat:@"%d: %@", indexPath.row, sushiName];
七. 備註:
A.
當 release 正式版本時, 確認
NSZombies 的檢查
是關閉的.1. 在 didFinishLaunchingWithOptions 內加入以下的判斷式:
if (getenv("NSZombieEnabled") || getenv("NSAutoreleaseFreedObjectCheckEnabled"))
{
NSLog(@"NSZombieEnabled/NSAutoreleaseFreedObjectCheckEnabled enabled!");
}
2. 此外也可以加入以下的環境變數:
NSZombieEnabled YES
NSDeallocateZombies NO
NSAutoreleaseFreedObjectCheckEnabled YES
NSDebugEnabled YES
NSHangOnUncaughtException YES
MallocScribble YES
MallocPreScribble YES
MallocGuardEdges YES
MallocBadFreeAbort YES