什麼程式都會有問題,而在寫 Cocoa/iPhone 程式的時候要 debug,最好還是先看一下蘋果的Xcode Debugging Guide這篇文件,這裡就只就一些最常見問題簡單寫一寫。
在開發 Cocoa/iPhone 應用程式的時候,如果你的程式 crash,QA Team 的成員很高興地把問題列成p1 bug 的時候,就個人遇到的狀況來說,有高達八成的機率(台灣電視記者的口氣),Xcode 會告訴你是以下三種問題-Out of Bound、Bad Access、Unsupported Selector。說起來都是很基本的錯誤,但是基本的錯誤並不代表是不會犯、或不常犯的錯誤,而且機率有高達八成之多。
Cocoa 應用程式有的時候就只會告訴你程式有 exception,接下來的 code 頂多就是不跑,至於 iPhone 上面就是直接炸掉給你看。而在這八成之外的另外兩成,則是各種稀奇古怪的問題了。
常見錯誤
Out of Bound
超過了 Array 的邊界範圍-比方說,你的 Array 裡頭只有三筆資料,但是你卻跟 Array 要第四筆資料。像這樣:
NSArray *a = [NSArray array]; // 空 Array [a objectAtIndex:0]; // 去要第一筆資料,這筆資料當然不存在
Bad Access
嘗試讀取不正確的記憶體位置。每一個 Objective C 物件(NSObject)都是一個指標,而如果你的變數指到了一個什麼東西都不存在的記憶體位置,然後你又對這個變數下指令(或是用 ObjC 的術語來說,送一個 message 過去),就會產生 Bad Access 錯誤。以下面這個例子來說:
NSObject *obj; [obj description];
因為完全沒有指定 obj 這個變數要指到哪裡,於是就有問題。而更常見的會是下面這樣的狀況:
NSObject *obj = [[NSObject alloc] init]; [obj release]; [obj description];
我們產生了一個物件,在用完之後,因為不需要了,所以我們釋放掉 obj 變數原本指向的記憶體。在釋放掉之後,我們又對 obj 下指令,就出問題了。而避免產生問題,在呼叫了 [obj release] 之後,就要將 obj 指到一個空指標上(加一行「obj = nil;」)。
產生 Bad Access 錯誤的常見狀況是,當我們在產生一個 class 的成員變數的時候,把成員變數產生成 autorelease 物件,結果這個變數在下一輪 runloop 時被釋放了,然後我們卻又去呼叫這個變數。另一個狀況則是,在寫一個 class 的 getter 的時候,忘記要把外面傳進來的參數 retain 或 copy 一份起來使用,而這個參數卻是 autorelease 物件…。
這種錯誤通常稱為 over-release:不應該 release 的東西,卻被 release 掉了。
Unsupported Selector
就是-你的某個 class 明明沒有某個 method,你卻對那個 class 呼叫了那個 method。這種狀況通常是在 refactor 程式之後發生的-你改了某個 class 的某個 method 的名稱(像是要多傳一個參數之類的),而別的地方已經用到了這個 method,你卻忘記修改這個地方。
如果只是要修改 method 的名稱,建議使用 Xcode 裡頭的 refactor 功能,Xcode 會幫你搜尋你整個專案當中用到這個 method 的全部地方,包括程式碼以及 nib 裡頭的內容。但是如果程式裡頭包含了 C++,那 Xcode 就沒有辦法 refactor 了,另外,Xcode 也不會修改 @selector(aMethod:)
裡頭的內容,所以,當你經常使用 performSelector:
的話,在 refactor 某個 method 名稱的時候,也要特別小心。
不過,如果你程式中呼叫了 performSelector:
這類的語法,而你看到 @selector(aMethod:)
括弧裡頭的字是黑色的,就應該要注意。Xcode 本身的 syntax highlighting 功能會將它找到的 selector 的名稱加上顏色,如果有找到(如果你用預設的配色),就會以綠色顯示,如果沒有加上 highlight 顏色,往往代表這個 selector 不存在。
另外一個更常發生這個錯誤的狀況是-你打算用 10.4 SDK 編譯發行,然後你卻不小心用了某個 10.5 或 10.6 SDK 才有的 API…。
Xcode 的 debugger
在 Xcode 寫了一段程式之後,要開始 debug,記得在執行的時候,Active Configuration 要選 Debug,然後從選單或是工具列上,也是選 Build and Debug 這個選項(快速鍵是 command + Y),而不是選 Build and Run。
在開始 debug 之前,也在 objc_exception_throw 這裡加上一個 break point;加上這個 break point 之後,當你在執行程式時發生問題,就會馬上停在發生錯誤的那一行程式上。方法是,在選單上找「Run->Show->Break Points」,這時候就會出現 Break Point 列表的視窗。
要加入新的 break point,直接在列表中最後一筆點兩下(就是上面說「Double-Click for Symbol」的地方,點兩下之後就變成可以輸入文字的輸入框,我們在這裡輸入 objc_exception_throw。
在這邊我們先來寫一個一定會 crash 的程式,就叫做 CrashApp,裡頭有 out of bound 錯誤,然後按下 Build and Debug 執行。一執行,自然馬上出問題,這時候因為設了 break point,於是立刻停止。現在,我們便可以叫出 debugger 視窗(選單上的「Run->Debugger」),看看到底哪裡出了問題。
在視窗的工具列正下方,首先是一個下拉選單,可以讓你切換要顯示哪一個 thread 裡頭的內容。繼續往下看,就是 call stack 列表,在表格裡頭可以看到是怎樣一路呼叫到了 objc_exception_throw,如果裡頭的文字是灰色的,代表呼叫到了系統 API,無法查看,但如果是黑色的,就代表是你自己寫的程式,點下去之後,有問題的程式內容就會出現在下方的文字編輯區中-也就是,你可以馬上看到你的程式到底錯在哪一行上。
我們知道錯誤發生在哪一行之後,不見得就馬上知道發生的是哪一種錯誤,這時候我們可以叫出 Console 出來(Run->Console)出來,裡頭會有 crash report,來看看會不會告訴我們什麼有用的訊息。
在這個範例中,告訴我們 “-[NSArray objectAtIndex:]: index (0) beyond bounds (0)” 這樣的訊息,我們就可以知道,要去檢查我們所使用到的 NSArray 物件了。而如果不是在 Xcode 裡頭執行,crash report 也會存一份到 ~/Library/Logs/CrashReporter 裡頭,至於 iPhone 程式在 release mode 下的 crash report,請參考蘋果的 Technical Note TN2151,或是在網路上搜尋 symbolicatecrash 關鍵字的相關文章。
http://developer.apple.com/iphone/library/technotes/tn2008/tn2151.html
當程式停在 break point 的時候,在 Debugger 視窗當中,如果將滑鼠指標移動到程式碼當中的某個變數名稱上面,就會有一個黃色的 tooltip 視窗,告訴你這個變數所代表的物件的內容,裡頭有什麼成員變數也可以一層一層展開。如果你遇到的是 over-release 的問題,就要看看 tooltip 視窗中有沒有什麼紅色的字。當一個物件會用紅色的字代表,就是有問題的物件。
另外,在 Console 視窗中,現在你也可以下 gdb 指令,有哪些指令可以用,打個 help 就知道了。而如果是要查看某個物件的狀態,最常使用的指令是 po(也就是 print-object 這個指令的縮寫)。
幾個稍微要注意的事情
- 按下 Build and Debug 按鈕後,Xcode 就會 attach 到你正在開發中的程式的執行 proccess 上,而這個程式在被 Xcode attach 的狀況下又回去影響 Xcode,就會發生問題。一般來說不太會有這種狀況,但是呢,如果你寫的程式是 IMK 輸入法,你把 Xcode attach 上去,你又在 Xcode 的編輯區裡頭不小心切到你正在開發的輸入法,那 Xcode 就會當場 crash 給你看。
- 如果你寫了一個用到瀏覽器元件 WebView 的應用程式,在 debug 之前,請務必把選單上的「Run->Stop on Debugger()/DebugStr()」這個選項關掉,原因是,有某個東西會一直隨便亂呼叫 Debugger()…。
Pingback: iOS Programming Tips (1) | Grey Lee