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

2011年3月14日 星期一

Using RamDisk for Firefox Cache in Mac OS X

更新日期: 2011/03/14
參考資料:
1. [教學]簡單利用AJA System Test測試你的硬碟傳輸速度
   http://macuknow.com/node/360

2. 通達人驛站: 安裝MAC內的RamDisk-Esperance DV
   http://www.prudentman.idv.tw/2008/08/macramdisk-esperance-dv.html

3. 使用 Esperance DV 建立超過 2GByte 的 RAMDisk
   http://5i01.com/topicdetail.php?f=482&t=1492941

4. RamDisk on Mac OSX
   http://blog.derjohng.com/2011/02/26/ramdisk-on-mac-osx/




一. 環境:




二. 相關軟體下載:
     1. AJA System Test:
        http://www.aja.com/ajashare/AJA_System_Test_v601.zip
    
     2. Esperance DV:
        http://www.mparrot.net/index.php?page=downloads&lang=en




三. 先用 AJA System Test 來測試目前硬碟的讀取/寫入效能:

  1. Test 項目: 選擇 Disk Read/Write 來為硬碟作讀寫速度測試.
  2. Volume 項目: 選擇你要測試的硬碟.
  3. 最後將: "Disable file system cache" 勾選起來, 以避免快取影響測試結果。
  4. 按下 "Start" 即可開始測試.
  5. 使用 RamDisk 前的測試結果:
     讀: 58.8 MB/s 寫: 48.0 MB/s




四. 安裝與設定 Esperance DV:
    1. 解壓縮下載的 esperance_dv.zip 檔, 執行其中的 Assistant.app 程式.
       選擇預設的 "Only for me" 並按下 "Install and quit this assistant".
       說明: 解壓縮下的 EsperanceDV.prefPane 檔案, 執行後可以取代 系統偏好設定
                內的
Esperance DV 設定. (用來重新設定)

    2.  開啓: 系統偏好設定 > 其他, 會看到 Esperance DV 的項目.

    3. 點選 Esperance DV 並開始作設定.
     (1). Name: RamDisk (使用預設值)
     (2). Capacity (容量): 528 MB (調到接近 512)
     (3). Create on opening session: 勾選起來, 開機時就會自動設定好.
     (4). Self auto restore: 勾選起來. 由於 Ramdisk 是易失性的,
         所以除了關機前取出資料外, 也設定讓它自動還原.
         說明: 每次使用後於關機前, 可按下 "Save & restore" 下的 "Save now".

     (5). 最後按下 "Create" 即完成.
         備註: 可在終端機下, 執行: ls -al /Volumes/ , 就可看到新產生的:
          /Volumes/RamDisk
     (6). 備註: 如果將 "Hide RamDisk icon on Desktop" 勾選, Name 自動會變成:
         .RamDisk (前面多一個點)
         說明: 要卸載時, 在終端機下, 執行: umount /Volumes/.RamDisk/ 




五. Firefox 的 Cache 設定:
    1. 開啟 Firefox, 在網址列輸入: about:config 再按下 Enter.
       出現警告訊息, 按下 "我發誓, 我一定會小心的!" 按鈕.
   

    2. 先在 "篩選條件" 輸入: browser.cache.disk 並按下 enter,
       檢查是否已存在設定項目: browser.cache.disk.parent_directory

    3. 接著, 在任意空白處按一下: 滑鼠右鍵 > 新增 > 字串
       偏好設定名稱: browser.cache.disk.parent_directory
       字串值: /Volumes/RamDisk


    4. 最後, 重新啟動 Firefox.




六. 建立超過 2GByte 的 RAMDisk (僅供參考, 我沒有實際測試)
     1. 用 vi 開啓: parrotsoftwares.EsperanceDV.plist 檔案
        vi /Users/Your-Account-Name/Library/Preferences/parrotsoftwares.EsperanceDV.plist

     2. 將 <key>Size</key> 下的 <integer> 數值改成你要設定的大小(單位: MB)
       <key>Size</key>
       <integer>528</integer>

     3. 儲存後離開.

     4. 接著, 登出, 再登入.




七. 最後, 來測試下 Ramdisk 的硬碟讀取/寫入效能:
   使用 RamDisk 後的測試結果:
   讀: 806.8 MB/s 寫: 421.2 MB/s
   (之前:
讀: 58.8 MB/s 寫: 48.0 MB/s)