2011年3月16日 星期三

Finding memory leaks for apps in Xcode 4

更新日期: 2011/03/17
參考資料:
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(壽司) 的清單.

     B. 點選數列後, 就會得到一個錯誤訊息: "EXC_BAD_ACCESS",
        但是卻不知道產生問題的明確地方.

    C. 解決方式:
       1. 在專案的執行設定項目裡, 設定 NSZombieEnabled 參數;
          通常可以縮小問題的範圍.
       2. 執行 Apple 的 Instruments, 例如用 Leaks 來查找記憶體的問題.
       3. 在程式碼中設置 breakpoint, 然後一步步地縮小範圍尋找造成 crash 的原因.
       4. 先將程式碼作註解(comment)直到它正常運作, 然後慢慢地反向取消註解
          (backtrack).




四. 啟用 NSZombieEnabled 方式: (圖誤)

     A. 說明: NSZombieEnabled 是一個旗標(flag), 如果在啟用後你嘗試存取一個已經被
                 deallocated 的物件, 它就會提供相關的警告訊息. 此外, 最常造成應用程
                 式 crash 的原因就是去存取 deallocated記憶體.
 
     B. 設置方式:
        1. 點取左上方 active scheme 的下拉式選單, 選擇 Edit Scheme...

        2. 在跳出的視窗中, 先在左方點選 Run your-Project-Name
           (在此為: Run PropMemFun), 然後點選右上方 "Arguments" 頁籤,
           於 "Environment Variables" 的地方按下 "+" 號

           新增: Name: NSZombieEnabled    Value: YES
           最後按下 "OK".

        3. 執行應用程式並再次點選幾列資料一直到發生 crash,
           可以在最下方的 console log 看到訊息:
           2011-03-16 16:29:39.572 PropMemFun[2946:207] ***
           -[CFString respondsToSelector:]: message sent to deallocated instance ...

        4. 同時程式也會停在發生 crash 的地方並標示出來:
           在此為: tableView:didSelectRowAtIndexPath 方法.

     C. 解決問題:
        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;
           _lastSushiSelected = [sushiString retain];




五. 使用 Build and Analyze 功能檢查記憶體洩露問題:
    A. 說明:
       1. 目前應用程式已經不會 crash 了, 接下來要確認是否有
          記憶體洩露
(memory leaks)的問題.

       2. 有一個簡單的方法用來執行掃視你的應用程式, 看看是否有記憶體洩露
          其它的問題 ------> 使用 Xcode 內建的 BuildAnalyze 功能.

       3. 這會讓 Xcode 跑遍所有的程式碼, 並且找尋它能自動偵測到的錯誤, 然後
          對任何潛在的問題發出警告. Xcode 並不會捕捉到所有的錯誤, 但是一但
          它捕捉到錯誤的地方, 的確會讓你快速且容易地發現問題所在.

    B. 使用步驟:
       1. 執行: Product > Build

       執行: Product > Analyze

    C. 執行結果:
       可以看到它偵測到了一個記憶體洩露的問題, 如下所示:
       Potential leak of an object allocated on line 144 and stored into 'alertView'
       訊息說明了有一個跟 "alertView" 有關的潛在的洩露問題.

    D. 解決方式:
       你會發現 UIAlertView 是由 alloc/init 所建立的
       (它會回傳一個 reference count1的物件),

       但是從未被 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:
   說明: 或著直接執行 Product > Profile, 會自動先 Build 再執行 Profile.


      3. 先選擇左邊 iOS Simulator 下的 Memory, 接著選擇右邊的 "Leaks",
          再按下 "Profile".

       4. 接著, 對 Simulator 作操作:
          在 Table View 點選數列, 接著將 Table View 從最上方捲到最下方再捲回來.
          (scroll up and down)

       5. 一小段時間後, 就可以在 Leaks tab 上看到一些 leaks 開始冒出,
           如底下 blue bar 所示:

       6. 按下 "stop" 按鈕, 先點選 "Leaks" bar , 然後在工具列中間的地方,
          點取 "Leaked Blocks" 改選為 "Call Tree".

       7. 在面板左下方 "Call Tree" 的部份, 將 "Invert Call Tree" 與
          "Hide System Libraries" 勾選起來.

       8. 然後你就可以在右邊看到: 在程式中有二個不同的方法有 memory leaks 的問題.

       9. 如果你對方法名稱 double click 的話, 會直接帶你到建立物件時產生
           記憶體洩露的程式碼位置.
        
    C. 解決方式:
      
       C-1. 針對: tableView:didSelectRowAtIndexPath

       以下的程式碼是產生 Leaks 的主因, 一步一步來思考原因:

       NSString *sushiString = [NSString stringWithFormat:@"%d: %@", indexPath.row, sushiName];

       1. sushiString 是由 stringWithFormat 建立的, 它會回傳 retain count1 的物件,
          伴隨著不確定的 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 它.

          因此舊的 _lastSushiSelectedretain 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 count1 的物件.
       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 訊息:
          --------------------------------------------------------------------------------
NSString *sushiString = [[NSString alloc] initWithFormat:@"%d: %@", indexPath.row, sushiName];
// method 02
NSString *sushiString = [[[NSString alloc] initWithFormat:@"%d: %@", indexPath.row, sushiName] autorelease];


          method 03:
          使用 stringWithFormat 取代 alloc/init 來建立字串,
          這樣會傳回一個標記為 autorelease 的字串:
          ---------------------------------------------------------------------------
          NSString *sushiString = [[NSString alloc] initWithFormat:@"%d: %@", indexPath.row, sushiName];
          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

4 則留言:

注意:只有此網誌的成員可以留言。