iphone調試技巧

http://article.ityran.com/archives/1143

有這樣一種情形:當我們正在快樂的致力於我們的app時,並且什麼看都是無比順利,但是突然,坑爹啊,它崩潰了。(悲傷地音樂響起)

我們需要做的第一件事就是:不要驚慌。

修復崩潰不是很困難的。假如你崩潰了,並且胡亂的改些東西,而且還在不停的念着咒語希望bug神奇的自動消失,你大多數情況下都會使情況更麻煩。相反的,你需要知道一些系統的方法,並且學習怎麼找到崩潰和他的原因。

第一件需要知道的就是在你的代碼中準確的找到crash發生的地方:在那個文件,那一行。Xcode debugger將會幫助你,但是你需要懂得怎麼樣最好的使用它,這也是這篇教程展示給你的。

這篇教程對於所有的開發者都是有利的。即使你是一個很有經驗的ios開發者,你也可能會從中學習到一些你不知道的小竅門。

準備開始

下載這個例子程序。你將會看到這是一個有bug的程序。當你打開這個項目的時候,xcode會顯示至少8個編譯警告,這個通常都是危險的信號。順便說一下,我們使用xcode4.3來做這篇教程,4.2的版本也應該沒有什麼問題。

注意:爲了跟隨這篇教程,這個編譯生成的app需要運行在ios5的模擬器上面。假如你運行這個app到你的設備上,你也會崩潰,但是他們可能不會發生和教程一樣的情況。

在模擬器上面運行你的app,你將會看到發生了什麼。

The app crashes immediately.

嘿,他崩潰了。

有兩種最基本的crash類型常發生:SIGABRT(也叫EXC_CRASH)和EXC_BAD_ACCESS(也可能會是SIGBUS或者SIGSEGV)。

就crash而言,SIGABRT是一個比較好解決的,因爲他是一個可掌控的crash。App會在一個目的地終止,因爲系統意識到app做了一些他不能支持的事情。

EXC_BAD_ACCESS是一個比較難處理的crash了,當一個app進入一種毀壞的狀態,通常是由於內存管理問題而引起的時,就會出現出現這樣的crash。

幸運的是,第一種崩潰(也是大多數崩潰)是SIGABRT,SIGABRT通常會在xcode的Debug Output窗口(在窗口的右下角)輸出一些錯誤的信息。假如你沒有看到Debug Output窗口,在你的xcode窗口的右上角一組圖標中點擊中間那個,假如還是沒有看到Debug Output窗口,你需要點擊這個小窗口的右上角的中間那個圖標,他靠近搜索框。在這個情況下,會展示一些下面東西:

Problems[14465:f803] -[UINavigationController setList:]: unrecognized selector sent to
instance 0x6a33840
Problems[14465:f803] *** Terminating app due to uncaught exception 'NSInvalidArgumentException',
reason: '-[UINavigationController setList:]: unrecognized selector sent to instance 0x6a33840'
*** First throw call stack:
(0x13ba052 0x154bd0a 0x13bbced 0x1320f00 0x1320ce2 0x29ef 0xf9d6 0x108a6 0x1f743
0x201f8 0x13aa9 0x12a4fa9 0x138e1c5 0x12f3022 0x12f190a 0x12f0db4 0x12f0ccb 0x102a7
0x11a9b 0x2792 0x2705)
terminate called throwing an exception

瞭解這些錯誤消息是非常重要的,因爲他們包含了錯誤在那裏的重要線索,一下就是需要關注的部分:

[UINavigationController setList:]: unrecognized selector sent to instance 0x6a33840

“unrecognized selector sent to instance XXX” 這條錯誤消息意味着你的app正在試着執行一個不存在的方法。這種情況的發生,主要是都是一個方法被錯誤的對象調用了(也就是這個對象沒有這個方法,但是你調用了他,就錯了)。例如在這裏這個問題上,對象就是UINavigationController (在內存地址0x6a33840上),方法就是setList:。

知道crash的原因是很好的,但是你的第一行動目的就是指出這個錯誤的發生在代碼的那個地方。你需要找到源文件的名字和這個錯誤方法在那一行。你通過使用call stack(就像堆棧跟蹤(stacktrace)或者回溯(backtrace))就可以知道這些東西。

當你的程序crash了時,在xcode窗口的左邊小窗口會啓動Debug Navigator(調試導航)。他會展示在這個app中那個線程是活動的,並且高亮顯示crash了的線程。通常他會是線程1,這個app的主線程,這個線程也是你會做最多工作的線程。假如你的代碼裏面使用了隊列(queues)或者後臺線程(background threads),這個app也可能會在其他的線程裏面崩潰。

The call stack. It doesn't show everything yet.

 

當前xocde就高亮顯示了main.m裏面的main()函數。但是那些東西並沒有告訴你很多,所以你需要繼續的向深層次的挖掘。

爲了看到堆棧的更多信息,拖拽Debug Navigator底部的滑塊到最右邊。它將會展示出崩潰時全部的堆棧信息:

The expanded call stack.

這個列表裏面的每一項都是一個來這個app或者ios的framework裏的方法或者函數。堆棧展示了當前活躍在這個app裏面的方法或者方法。調試器(debugger)已經暫停了這個程序,並且所有的這些方法和函數在這個時候也被凍結了。

在底部的函數start(),第一個被調用。在他的執行裏面的有些地方,,main()函數在他之前。(Somewhere in its execution it called the function above it, main().)。他是應用程序的開始入口點,並且它經常在底部附近。Main()也叫UIApplicationMain()(這個針對的是ios哈,並不是其他所有程序都是這樣的)。在這個編輯窗口裏面用綠色箭頭指示的那一行(就是在這個教程最開始前面程序崩潰時停止在那個圖片上,高亮顯示的部分)。

進一步來看看這個堆棧,UIApplication()在UIApplication對象裏調用_run方法,_run方法裏面又調用CFRunLoopRunInMode()方法,CFRunLoopRunInMode()方法裏面又調用CFRunLoopSpecific()方法,就這樣一直向下調用,一直到__pthread_kill。

How a call stack works.

所有在這個堆棧裏面的函數和方法都是灰色的,除了main()函數。那是因爲他們都來自內置的ios frameworks(ios內置框架)。所以沒有針對他們可見的源碼。

在這個堆棧裏面唯一的東西就是你有main.m的源碼,因此xcode的代碼編輯器就顯示了它,即使他不是這個崩潰的真正原因。但是這個經常混淆初學者,但是馬上我將展示怎麼樣來弄懂它。

開個玩笑,點擊這個堆棧裏面的任意一項,你將會看到許多的彙編代碼,這些你可能完全不理解:

If there is no source code, Xcode shows assembly.

加入我們得到那樣的源碼,我想很多人都會說:坑爹啊。

異常斷點

你怎麼樣找到是代碼裏面的哪一行使app崩潰的?無論什麼時候,你得到的一個想這樣的堆棧路徑,一個異常通過這個app拋出。(你多半會說因爲堆棧裏面有一個函數叫objc_exception_rethrow。)

當程序由於做了一些他不能完成的事情時,一個異常就會發生。你所看到的就是這個異常的結果:app做了一些錯的事情,異常被拋出,xcode展示異常的結果。理想情況下,你想要的準確的看到異常在那裏拋出的。

幸運的是,通過使用Exception Breakpoint(異常斷點),你可以告訴xcode在一個特定的時候暫停這個程序。斷點是一個在特定時刻暫停你的程序的調試工具。你將會第二篇教程裏面看到更多關於他們的信息,但是現在你將會使用一個特殊的斷點,它將會在拋出異常前暫停你的程序。

爲了設置異常斷點,我們不得不切換到Breakpoint Navigator(斷點導航器):

The Breakpoint Navigator

在底部有一個小的加號(“+”)按鈕。點擊它,並且選擇Add Exception Breakpoint:

Adding the Exception Breakpoint

一個新的斷點將會被增加到這個列表裏:

After the Exception Breakpoint has been added

點擊Done按鈕使彈出的窗口消失。注意在xcode工具欄上面Breakpoints button(斷點按鈕)是有效的。加入你不想要帶着任何斷點運行你的app,你可以簡單的開關這個按鈕到off。但是現在,讓它打開,並且再一次運行這個app。

After the crash, the problematic source line is now highlighted.

太好了!代碼編輯器現在停止並且指到了代碼中的其中一行,不再在令人煩躁的彙編代碼了,並且注意在在左邊的的Debug Navigatot(調試導航器)裏面顯示的堆棧信息也不一樣了。

顯然的,問題就出在AppDelegate裏面的application:didFinishLaunchingWithOptions:方法裏:

viewController.list = [NSArray arrayWithObjects:@"One", @"Two"];

仔細再次看看這個錯誤消息:

[UINavigationController setList:]: unrecognized selector sent to instance 0x6d4ed20

在這個代碼裏面,“viewController.list = something”這種方式隱式的調用了setList:方法,也就是set方法,因爲“list”是MainViewController類的一個屬性。然而,通過這個錯誤消息,我們知道viewController這個變量沒有指向MainViewController對象,而是指向了UINavigationController,所以顯然的,UINavigationController沒有“list”屬性!所以這些變量在這裏混淆了。

打開Storyboard文件,看看window的rootViewController屬性實際上是指向那個的:

The storyboard has a navigation controller.

哈哈!Storyboard的最初的view controller是一個Navigation controller。這就是爲什麼window.rootViewController是一個UINavigationController對象,而不是你自認爲的MainViewController。爲了修改這裏,使用下面的代碼來替代application:didFinishLaunchingWithOptions:裏面的:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
	UINavigationController *navController = (UINavigationController *)self.window.rootViewController;
	MainViewController *viewController = (MainViewController *)navController.topViewController;
	viewController.list = [NSArray arrayWithObjects:@"One", @"Two"];
	return YES;
}

通過代碼可以看出,首先你通過self.window.rootViewController得到UINavigationController,一旦你得到了上面的。你就可以通過請求navigation controller來得到topViewController,進而得到MainViewController。現在viewController變量就是指向了正確的對象了。

注意:一旦你得到“unrecognized selector sent to instance XXX”錯誤,你就需要檢查這個對象是不是正確類型,並且檢查它真的是有那個名字的方法麼。你會經常發現你調用一個你認爲是這個對象的方法,因爲指針變量可能沒有包含這個正確值,所以導致很多的錯誤。

The different parts of the "Unrecognized selector" error message.

另外一個經常出現錯誤的原因就是將方法名稱拼寫錯誤。一會兒你將會看到一個這樣的例子。(譯者:我個人認爲有xcode的代碼提示功能,這種錯誤應該還是比較少吧,多數應該出現在通過selector,或者傳遞函數指針的時候,應該會多點這個錯誤)。

你的第一個內存錯誤

你可能已經修復了你的第一個問題。再一次運行這個程序。坑爹啊,在同樣的一行,又崩潰了,但是現在是EXC_BAD_ACCESS錯誤。那意味着這個app有內存管理的問題。

Our first EXC_BAD_ACCESS error.

一個和內存相關的崩潰一般很難定位到源代碼,因爲這個惡魔可能很早就在程序中做了壞事了。假如一段有問題的代碼混亂了內存結構,這樣產生的蝴蝶效應可能會在之後很久才表現出來,並且總在不同的地方。

實際上,在你所有的測試中,這個bug可能永遠不會出現,但是卻在你的客戶的設備上展露出它醜陋的腦袋。這種是很多人都不想的。

這種特別的崩潰但是卻很容易修復。假如你看到你的代碼編輯器,xcode其實一直就在警告你這一行代碼。看到左邊靠近行號的那個黃色三角形沒有?那個指出一個編譯警告。假如你點擊那個黃色的三角形,xcode將會彈出一個“Fix-it”的建議,就像下面的一樣:

Xcode warns about a missing sentinel.

這個代碼使用了一系列的對象來初始化一個數組(NSArray),並且像那樣的一系列的對象應該使用nil來終止,這個警告的標記就是想要表達一個這樣的意思。但是代碼卻沒有那樣做,所以NSArray就很困惑,很迷茫。它試着讀取一個不存在的對象,最後這個app艱難的崩潰了。

這種錯誤,你真的不應該犯,特別是xcode已經警告了你。修復這個錯誤,通過像下面一樣增加一個nil(或者你可以簡單的選擇剛剛彈出來的菜單裏面“Fix-it”):

viewController.list = [NSArray arrayWithObjects:@"One", @"Two", nil];

“This class is not key value coding-compliant”

重新運行這個程序,看看爲你準備的其他有趣的bug。信不信由你?它又在main.m裏面崩潰了。雖然Exception Breakpoint任然起作用了,但是我們沒有看見任何高亮的程序代碼,這次的崩潰真的沒有發生在任何程序代碼裏。這個調用堆棧證實了這點:這裏面的方法沒有一個屬於的程序的,除了main():

The call stack for the "key value coding" crash.

假如你從上到下瀏覽一下這些方法的名字,有些問題發生在NSObject和Key-Value Coding。在那之下調用了[UIRuntimeOutletConnection connect]。我不知道那個是幹什麼的,但是看起來好像它做了連接outlet的一些事情。在那之下的一些方法是從nib中加載view。因此以上那些也給你一些線索。

但是,在xcode的調試窗口,並沒有易懂的錯誤消息。那是因爲沒有異常被拋出。在xcode告訴你異常的原因之前,Exception Breakpoint已經暫停了這個程序。有些時候你會從Exception Breakpoint得到一些局部的錯誤消息,但是有些時候就得不到。

爲了得到全部的錯誤消息,點擊調試器工具欄上的“Continue Program Execution”按鈕:

The Continue Program Execution button.

你可能需要點擊好幾次纔可以,然後你將會得到錯誤消息:

Problems[14961:f803] *** Terminating app due to uncaught exception 'NSUnknownKeyException',
reason: '[ setValue:forUndefinedKey:]: this class is not
key value coding-compliant for the key button.'
*** First throw call stack:
(0x13ba052 0x154bd0a 0x13b9f11 0x9b1032 0x922f7b 0x922eeb 0x93dd60 0x23091a 0x13bbe1a
0x1325821 0x22f46e 0xd6e2c 0xd73a9 0xd75cb 0xd6c1c 0xfd56d 0xe7d47 0xfe441 0xfe45d
0xfe4f9 0x3ed65 0x3edac 0xfbe6 0x108a6 0x1f743 0x201f8 0x13aa9 0x12a4fa9 0x138e1c5
0x12f3022 0x12f190a 0x12f0db4 0x12f0ccb 0x102a7 0x11a9b 0x2872 0x27e5)
terminate called throwing an exception

就像之前的一樣,你可以忽略下面的那些數字。他們展示了調用堆棧,但是在調試導航器的左邊有更加直觀的堆棧調用展示。

有趣的部分是:

NSUnknowKeyException

MainViewController

“this class is not key value coding-compliant for the key button”

這個異常的名字爲NSUnknownKeyException,它是這個錯誤很好的指示器。它告訴你在某個地方有一個“unknown key”。這個某一個地方通常就是MainViewController,並且這個key就是“button”。

既然我們已經確定了,所有這些都是發生在裝載nib的時候。這個應用使用的是storyboard,而不是nib文件,但是其實storyboard內部就是nib的集合(也就是可以有很多的nib),因此這個錯誤就在這個storyboard中。

檢查一下MainViewController的outlets:

The button outlet in the storyboard.

在Connections Inspector(連接檢測器)裏,你可以看見在viewcontroller中間的UIButton是連接到MainViewController的“button”outlet上的。因此storyboard引用了一個名叫“button”的outlet,但是通過這個錯誤消息說明它找不到這個outlet。

讓我們來看看MainViewController.h:

@interface MainViewController : UIViewController

@property (nonatomic, retain) NSArray *list;
@property (nonatomic, retain) IBOutlet UIButton *button;

- (IBAction)buttonTapped:(id)sender;

@end

這裏是爲這個“button”定義了外部連接屬性的(@property),因此這個問題是什麼呢?假如你仔細觀察了編譯警告的話,你可以已經知道是什麼地方的問題了。

假如還不知道的話,檢查一下MainViewController.m的@synthesize的內容的話。你現在看出問題沒有啊?

這個代碼其實沒有@synthesize這個button的屬性。它(@synthesize)其實是告訴MainVIewController他自己有個“button”的屬性,提供一個後臺實例變量,並且提供getter和setter方法(這就是@synthesize所做的)。

把下面的增加到MainViewController.m裏面已經存在的@synthesize行的下面來修復這個問題:

@synthesize button = _button;

現在這個app應該不會在你運行的時候崩潰了!

注意:“this class is not key value coding-compliant for the key XXX”的錯誤經常都是由於你裝載這個nib,但是裏面引用的一些熟悉可能不存在。特別是當你在代碼中移除了outlet屬性後,但是你卻沒有在nib中移除這個連接。

Push the Button

現在這個app正常工作,或者至少說啓動的時候沒有問題。是時候來點擊這個按鈕了。

The app finally starts up.

哇!這個app崩潰在main.m裏面,並且伴隨着SIGABRT。在調試窗口打印出的錯誤消息是:

Problems[6579:f803] -[MainViewController buttonTapped]: unrecognized selector sent
to instance 0x6e44850

堆棧跟蹤也不是很有啓發。只是列出了一些和一個方法相關的或者發送了事件並且執行了動作的方法,但是你已經知道到了被涉及的動作了。畢竟,你點擊了一個按鈕,這個按鈕的IBAction方法應該被調用。

當然你之前應該已經看過了這個錯誤消息。一個被調用的方法不存在。這個時候目標對象應該是MainViewController,由於動作方法經常存在於一個包含按鈕的view controller裏面,所以這個看起來是正確。並且你看MainViewController.h文件,這個IBAction方法確實在裏面:

- (IBAction)buttonTapped:(id)sender;

是這樣的嗎?錯誤消息顯示這個方法的名字是buttonTapped,但是MainViewController的方法卻是buttonTapped:(注意冒號),由於這個方法需要接受一個參數(名字是sender),所以在方法名字後面有個冒號。從這個錯誤消息看出,這個方法沒有冒號,因此不需要參數。所以這個方法看其實應該是這樣的:

- (IBAction)buttonTapped;

這個裏發生了什麼?這個方法最初的時候不需要參數(有些時候這樣動作方法是被允許的),並且在那個時候,他爲這個按鈕在storyboard裏面連接了Touch Up Inside的時間方法。然而,在那之後某個時候,這個方法的形式被修改爲包含了一個“sender”參數,但是,卻沒有去更新storyboard。

你可以在storyboard裏面看看,在這個按鈕的連接檢測器:

The button's connections in the storyboard.

第一,斷開Touch Up Inside 事件(點擊這個小“X”),然後再次連接它到MainViewController裏,但是這次選擇這個buttonTapped:方法。注意在連接檢查器裏面看看這個方法後面是有一個冒號的。

運行這個app,再一次點擊按鈕。我們又得到了這個“unrecognized selector”消息,但是這次他正確的定位到了buttonTapped:方法裏面。

Problems[6675:f803] -[MainViewController buttonTapped:]: unrecognized selector sent
to instance 0x6b6c7f0

假如你仔細看的話,編譯器警告應該又給你指出解決方案。Xcode提出MainViewController的實現是不完整的。特別的,buttonTapped:方法沒有被發現。

Xcode shows an incomplete implementation warning.

是時候看看MainViewController.m了,在這裏確實是有buttonTapped:方法啊……………..等等,拼寫錯誤了:

- (void)butonTapped:(id)sender

很簡單的修改,重命名這個方法:

- (void)buttonTapped:(id)sender

提示:你沒必要聲明這個方法爲IBAction,假如你覺得這樣是很優雅的,你可以這樣做。

注意:假如你仔細注意到這些編譯器警告的話,這些問題很容易看出來的。就個人而言,我把所有的警告當成嚴重的錯誤(在xcode裏面的編譯設置(Build Settings)裏面可以設置警告作爲錯誤提示的),在運行程序以前,我會修改所有的。Xcode在指出愚蠢的錯誤表現的相當好,就像這裏這樣,並且注意到這些提示是很明智的。

Messing with Memory(混亂內存)

經過了這麼多,你知道崩潰一直在繼續從未停止過。運行這個app,點擊按鈕,然後等待崩潰。好,現在就來了:

The app crashes on NSLog().

這裏是另一種EXC_BAD_ACCESS崩潰。幸運的是,xcode已經準確給你指示出位置在那裏了,在這個buttonTapped:方法裏面:

NSLog("You tapped on: %s", sender);

有些時候,你可能在上面花費一些時間纔會反應過來,但是xcode提供了幫助,僅僅需要點擊這個黃色的三角形來看這個錯誤是什麼:

Objective-C strings must have a @ prefix.

NSLog呈現一個Objective-c類型的字符串,而不是一個c字符串,因此插入一個@符號來修改它:

NSLog(@"You tapped on: %s", sender);

你將會注意到這個警告的黃色三角形依然沒有消失。這是因爲在這行還有另外一個bug,這個bug可能會或者可能不會使你的程序崩潰。有些時候這個代碼工作很好,或者現在看起來很好,但是有些時候他就會崩潰。(特別是有些時候只在你的客戶的設備上面,絕不會在你的設備上)。

讓我們來看看這個新的警告:

Xcode warns about a format string issue.

這個“%s”專門爲c語言類型的字符串。一個c類型的字符串就是把這個內存分成片段(一個老式的字節數組),通過一個所謂的”NUL character”(其實就是一個爲0的值)來終止。例如這個“Crash!”看起來就是這樣的:

What a C-string looks like in memory.

無論是什麼時候,你使用一個函數或者方法來操作這個c類型的字符串,你不得不確定這個字符串是以一個0值來結尾的,否則這個函數將不知道這個字符串已經結束了。

現在來看看,當你指定了在NSlog()中用“%s”來格式化字符串,或者在NSString 的stringWithFormat裏面,這個變量將會被當做是一個c類型的字符串。假如這個“sender”指向一個包含0字節的內存,這個NSlog()將不會崩潰,但是輸出的東西就會像這樣:

You tapped on: xËj

再一次運行這個app,點擊這個按鈕,等待它崩潰。現在在Debug窗口的左邊部分,右擊“sender”,並且選擇“view Memory of “*sender””選項(確保是選擇的是帶有星號的sender)。

The view memory menu option.

Xcode將會展示出這個內存地址的內容,恰恰這個就是NSlog()打印出來的內容。

Xcode shows the contents of memory.

然而,這裏並不能保證這裏有空位(結束標誌位),所以你完全很容易執行到一個EXC_BAD_ACCESS的錯誤。 假如你經常在模擬器上面測試的話,這個很長時間都可能不會發生,然而這種情況一般都是在很特殊的情況環境下就可能發生。所以這種類型的bug很難跟蹤。

當然,在這種情況下,xcode已經警告你這個錯誤的格式化字符串,因此這個特別的bug是很容易發現的。但是無論什麼時候,你使用c類型的字符串或者手動直接操作內存的,都應該非常的小心的不要混亂了其他的內存。

假如你非常的幸運,這個app將會經常崩潰,這個bug很容易找到,但是通常情況是這個app會崩潰在某個時候,而且這個問題很難重現!之後尋找這個bug將會是一個史詩般的工程,十分麻煩。

修復這個NSLog()的形式,就像下面的一樣:

NSLog(@"You tapped on: %@", sender);

運行這個app,並且再一次點擊這個按鈕。現在這個NSLog()做了他能做的了,並且看起來好像不會崩潰在buttonTapped:這個函數裏面了。

和調試器交朋友(Making Friends With the Debugger)

看看這最近的崩潰,xcode指示到了這一行:

[self performSegueWithIdentifier:@"ModalSegue" sender:sender];

在Debug窗口裏面沒有消息打印出來。你可以點擊這個繼續執行這個程序的按鈕,就像前面一樣,但是你也可以在調試器裏面輸入一個命令來得到這個錯誤消息。這樣做的好處就是,這個app可以保持暫停在這個同樣的地方。

假如你準備在模擬器裏面運行這個,你可以在“(lldb)”提示的後面輸入下面的:

(lldb) po $eax

LLDB在xcode4.3或者之後的版本里面是默認的調試器。假如你正在使用老一點版本的xcode的話,你又GDB調試器。他們有一些基本的相同的命令,因此假如你的xcode使用的是“(gdb)”提示,而不是“(lldb)”提示的話,你也能夠更隨一起做,而沒有問題。

“po”命令是“print object”(打印對象)的簡寫。“$eax”是cup的一個寄存器。在一個異常的情況下,這個寄存器將會包含一個異常對象的指針。注意:$eax只會在模擬器裏面工作,假如你在設備上調試,你將需要使用”$r0″寄存器。

例如,假如你輸入:

(lldb) po [$eax class]

你將會看像這樣的東西:

(id) $2 = 0x01446e84 NSException

這些數字不重要,但是很明顯的是你正在處理的NSException對象在這裏。

你可以對這個對象調用任何方法。例如:

(lldb) po [$eax name]

這個將會輸出這個異常的名字,在這裏是NSInvalidArgumentException,並且:

(lldb) po [$eax reason]

這個將會輸出錯誤消息:

(unsigned int) $4 = 114784400 Receiver () has no
segue with identifier 'ModalSegue'

注意:當你僅僅使用了“po $eax”,這個命令將會對這個對象調用“description”方法和打印出來,在這個情況下,你也會得到錯誤的消息。

因此解釋下那是什麼情況:你正在嘗試執行一個名叫“ModalSegue”的segue,但是很顯然,在MainViewController裏面並沒有這樣的的segue。

Storyboard展示出來這個segue是存在的,但是你忘記了設置它的標示,一個典型的錯誤:

Giving the segue an identifier.

改變這個segue的標示爲“ModalSegue”。再一次運行這個app,等待一下,點擊這個按鈕 ,這個時候不再有crash了!但是這裏只是我們下個部分的開端——-tableview不應該是空的!

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

在這個教程的第一部分,我們介紹了SIGABRT和EXC_BAD_ACCESS錯誤,並且舉例說明了一些使用xcode調試器(Xcode debugger)和異常斷點(Exception Breakpoints)解決問題的策略。

但是我們的app仍然有一些問題!就像我們看到的,他工作的並不是很好,並且這裏仍然有許多潛在的可能崩潰的問題。

幸運的是,在這個教程的第二部分,也是最後一部分,我們可以學習更多的技術來處理這些問題。

所以我們就不在囉嗦了,讓我們回到繼續修正這個充滿bug的app中吧!

Learn how to debug and fix dreaded app crashes!

Getting Started: When What’s Supposed to Happen, Doesn’t

 

在第一部分我們停止的地方,經過許多的調試工作之後,我們運行這個程序他是不會崩潰的。但是他卻展現了一個沒有預料到的空的table,就像下面一樣:

The table view doesn't show any rows.

當你覺得一些事情應該發生,但是卻沒有發生的時候,這裏有些你可以使用一些技巧來排除問題。在這個教程裏面,我們首先是學習使用NSlog來解決這個問題。

這個table view controller的類是ListViewController。在一系列的任務執行之後,這個app應該裝載ListViewController,並且在屏幕上面顯示出來。你可以做一個測試,來確定view controller的方法是執行了的。所以viewDidLoad這個方法看起來應該是一個好地方來做測試。

在ListViewController.m,增加一個NSLog()到viewDidload,就像下面一樣:

- (void)viewDidLoad
{
	[super viewDidLoad];
	NSLog(@"viewDidLoad is called");
}

當你運行這個app時,你應該期望當我們點擊了“Tap Me”按鈕後在調試窗口看到“viewDidLoad is called”這樣文字。現在就來試試,點都不驚訝,在調試窗口什麼也沒有出現。那就意味着ListViewController類根本沒有被使用!

這個多半意味着,你可能忘記了告訴storyboard你想要爲table view controller場景使用ListViewController類。

Setting the class for the table view controller.

由上圖我們可以看出,在身份檢查器(Identity Inspector)的類屬性區域是設置的默認值UITableViewController。改變這個Custom Class下面的class爲ListViewController,然後再一次運行這個app。現在在調試窗口應該就會出現“viewDidLoad is called”文字:

PProblems[18375:f803] You tapped on: <UIRoundedRectButton: 0x6894800; frame = (119 189; 82 37);
opaque = NO; autoresize = RM+BM; layer = <CALayer: 0x68948f0>>
Problems[18375:f803] viewDidLoad is called

但是這次app將會再一次崩潰,但是卻是一個新的問題。

注意:一旦你的代碼好像沒起什麼什麼作用的話,放置一些NSLog()在確切的地方,來看看是否這個方法是被執行了的和cpu通過怎麼樣路徑執行這個方法。使用NSLog()來測試你假設將會執行的代碼。

Assertion Failures

這個新的有趣的崩潰。它是一個SIGABRT,並且在調試窗口打印出來的是以下消息:

Problems[18375:f803] *** Assertion failure in -[UITableView _createPreparedCellForGlobalRow:
withIndexPath:], /SourceCache/UIKit_Sim/UIKit-1912.3/UITableView.m:6072

我們得到的是一個執行UITableView的一些方法的一個“斷言錯誤(assertion failure)”。當某些東西出錯了之後,一個斷言是一個內部相容性的檢查器,並且會拋出一個異常。你也可以放置斷言在你的代碼裏。例如:

- (void)doSomethingWithAString:(NSString *)theString
{
	NSAssert(string != nil, @"String cannot be nil");
	NSAssert([string length] >= 3, @"String is too short");
	. . .
}

在上面的方法裏面,我們讓一個NSString對象作爲這個函數的變量,但是代碼卻不允許調用者傳遞一個nil或者長度小於3的字符串。假如這些條件中的一個不匹配的話,這個app將會終止,並且拋出一個異常。

你可以使用斷言來作爲一個防禦性編程技術,因此你應該確定這個就是我們想要的代碼行爲。斷言通常只在調試編譯下有用的,因此他們對發佈到app store的最終的app是沒有運行時的影響的。

在這個情況下,某些情況觸發了一個UITableView的斷言錯誤,但是你並沒有完全確定在那個地方。App也是停止在main.m裏面,並且在執行堆棧裏面只包含了框架(framework)的方法。

從這些方法的名字,我們可以猜測這個錯誤發生在重畫這個tableview的某些地方。例如,我們可以看到layoutSubviews和_updateVisibleCellsNow:這些名字的方法。

The call stack for the assertion failure on the table view.

繼續運行這個app來看看是否可以得到一些比較好的錯誤消息—–記住,現在只是在拋出異常的時候暫停了程序,並沒有崩潰。點擊繼續程序按鈕,或者在調試窗口鍵入下面的命令:

(lldb) c

你可能不得不多點擊幾次繼續按鈕,“c”命令也是一個簡短的繼續指令,和點擊繼續按鈕一個效果,並不是就直接執行到最後。

現在這個調試窗口噴發出一些比較有用的信息:

*** Terminating app due to uncaught exception 'NSInternalInconsistencyException',
reason: 'UITableView dataSource must return a cell from tableView:cellForRowAtIndexPath:'
*** First throw call stack:
(0x13ba052 0x154bd0a 0x1362a78 0x99a2db 0xaaee3 0xab589 0x96dfd 0xa5851 0x50301
0x13bbe72 0x1d6492d 0x1d6e827 0x1cf4fa7 0x1cf6ea6 0x1cf6580 0x138e9ce 0x1325670
0x12f14f6 0x12f0db4 0x12f0ccb 0x12a3879 0x12a393e 0x11a9b 0x2722 0x2695)
terminate called throwing an exception

太好了,這是一個相當好的一個線索。顯然這個UITableView的數據源沒有從tableView:cellForRowAtIndexPath:方法返回一個有效的cell,因此在ListViewController.m方法裏面增加一些調試輸出信息來看看:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
	static NSString *CellIdentifier = @"Cell";
	UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];

	NSLog(@"the cell is %@", cell);

	cell.textLabel.text = [list objectAtIndex:indexPath.row];

	return cell;
}

你增加一個NSLog()標記。再一次運行這個app,看看輸出了什麼:

Problems[18420:f803] the cell is (null)

從以上信息我們可以看出,調用dequeueReusableCellwithIdentifier:返回的卻是nil,這就意味着使用“Cell”作爲標識符的cell可能不存在(因爲這個app使用的是標準的cell的storyboard)。

當然,這也是愚蠢的bug,並且毫無疑問的是,在以前解決這個需要很長的時間,但是現在卻不是了,因爲xcode已經通過靜態編譯警告了你:“Prototype cells must have reuse identities。(標準的cell必須有重用的標識)”。這個是不能忽視的警告:

Xcode warns about a missing prototype cell identifier.

打開storyboard,選擇這個標準的cell(在tableview的頂端,並且顯示的是“Title”的單獨的一個cell),並且設置cell的標識符爲“Cell”:

Giving the prototype cell a reuse identifier.

將那個修復了之後,所以的編譯警告應該沒有了。運行這個app,現在這個調試窗口應該會打印出來:

Problems[7880:f803] the cell is <UITableViewCell: 0x6a6d120; frame = (0 0; 320 44); text = 'Title'; layer = <CALayer: 0x6a6d240>>
Problems[7880:f803] the cell is <UITableViewCell: 0x6877620; frame = (0 0; 320 44); text = 'Title'; layer = <CALayer: 0x6867140>>
Problems[7880:f803] the cell is <UITableViewCell: 0x6da1e80; frame = (0 0; 320 44); text = 'Title'; layer = <CALayer: 0x6d9fae0>>
Problems[7880:f803] the cell is <UITableViewCell: 0x6878c40; frame = (0 0; 320 44); text = 'Title'; layer = <CALayer: 0x6878f60>>
Problems[7880:f803] the cell is <UITableViewCell: 0x6da10c0; frame = (0 0; 320 44); text = 'Title'; layer = <CALayer: 0x6d9f240>>
Problems[7880:f803] the cell is <UITableViewCell: 0x6879640; frame = (0 0; 320 44); text = 'Title'; layer = <CALayer: 0x6878380>>

Verify Your Assumptions

你的NSLog()打印出來的消息,已經告訴我們6個table view cell被創建了,但是在table上面什麼都看不見。怎麼回事呢?假如你在模擬器裏面到處點擊一下,你將會注意到tableview中6個cell中的第一個卻能夠被選中。所以,顯然cells都是存在的,只是他們都是空的:

The table appears empty but cells can actually be selected.

是時候需要更多的調試記錄了。將先前的NSLog()標記改變一下:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
	static NSString *CellIdentifier = @"Cell";
	UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];

	cell.textLabel.text = [list objectAtIndex:indexPath.row];

	NSLog(@"the text is %@", [list objectAtIndex:indexPath.row]);

	return cell;
}

現在你打印出來就是你的數據模塊的內容。運行這個app,看看顯示出來的是什麼:

Problems[7914:f803] the text is (null)
Problems[7914:f803] the text is (null)
Problems[7914:f803] the text is (null)
Problems[7914:f803] the text is (null)
Problems[7914:f803] the text is (null)
Problems[7914:f803] the text is (null)

上面的很好的解釋了爲什麼在cell裏面什麼都沒有看到的原因:因爲這個文字(text)始終是nil。然而,假如你檢查你的代碼,並且在initWithStyle:方法裏面顯示的添加了很多的字符串到list array裏面:

		[list addObject:@"One"];
		[list addObject:@"Two"];
		[list addObject:@"Three"];
		[list addObject:@"Four"];
		[list addObject:@"Five"];

就像上面那樣,這是測試你的假設是不是正確的一個很好的方法。可能你還想更準確的看看這個array裏面到底有什麼東西。改變先前在tableView:cellForRowAtIndexPath:裏面的NSLog()爲這樣:

NSLog(@"array contents: %@", list);

至少這樣可以給你展示一些東西。運行這個app。假如你還沒準備好猜測會發生什麼情況,調試窗口已經給你打印出來了:

Problems[7942:f803] array contents: (null)
Problems[7942:f803] array contents: (null)
Problems[7942:f803] array contents: (null)
Problems[7942:f803] array contents: (null)
Problems[7942:f803] array contents: (null)
Problems[7942:f803] array contents: (null)

哈哈,你的臉色瞬間陰沉下來。上面的代碼居然沒有起作用,因爲你可能忘了在首先爲這個array對象申請內存空間。這個“list”所以一直爲nil,因此調用addObject: 和objectAtIndex:不會起任何的作用。

你應該在你的view controller被裝載的時候爲這個list對象分配空間,因此在initWithStyle:方法裏面應該是一個不錯的選擇。修改那個方法爲:

- (id)initWithStyle:(UITableViewStyle)style
{
	if (self == [super initWithStyle:style])
	{
		list = [NSMutableArray arrayWithCapacity:10];

		[list addObject:@"One"];
		[list addObject:@"Two"];
		[list addObject:@"Three"];
		[list addObject:@"Four"];
		[list addObject:@"Five"];
	}
	return self;
}

試一試。我暈,依然什麼都沒有!調試窗口輸出依然是:

Problems[7971:f803] array contents: (null)
. . . and so on . . .

經過了這麼多假設和修改,但是還是什麼都沒有,這些真的是非常令人沮喪啊,但是請記住你可能會一直繼續到最後,直到你弄清楚了所有的假設。所以現在的問題就是難道initWithStyle:沒有被調用?

Working With Breakpoints

你可能又會在代碼裏面放置另外一個NSLog()標誌,但是其實你完全可以使用另外的工具:斷點( breakpoints)。你已經看到過無論什麼時候只要有異常拋出的時候,程序就會終止的異常斷點(Exception Breakpoint)了。你其實也可以增加其他的斷點,並且可以放置到代碼的任何地方。一旦你的程序運行到斷點的地方,這個斷點就會被觸發,並且程序就會進入調試模式。

你可以通過點擊代碼編輯區前面的行號來放置特殊的斷點:

Setting a breakpoint on a line of code.

這個藍色的箭頭所指示的那一行就有一個斷點了。你也可以在斷點導航器(Breakpoint Navigator)裏面看到這個新的斷點:

The new breakpoint in the Breakpoint Navigator.

再一次運行這個app。假如initWithStyle:確實是會被調用的話,那麼你點擊了“Tap Me!”按鈕之後,當這個ListViewController被裝載的時候,這個app將會暫停,並且會進入調試器。

可能正如你所料的,什麼事情也沒有發生。initWithStyle:沒有被調用。其實這個是可以講得通的,因爲view controller是從storyboard(或者xib)中裝載的,所以使用的應該是initWithCoder:方法。

將之前initWithStyle:方法替換爲initWithCoder::

- (id)initWithCoder:(NSCoder *)aDecoder
{
	if (self == [super initWithCoder:aDecoder])
	{
		list = [NSMutableArray arrayWithCapacity:10];

		[list addObject:@"One"];
		[list addObject:@"Two"];
		[list addObject:@"Three"];
		[list addObject:@"Four"];
		[list addObject:@"Five"];
	}
	return self;
}

並且保持斷點在這個方法上面,來看看它是怎麼工作的:

Setting the breakpoint on initWithCoder.

一旦你點擊了那個按鈕,這個app將會進入調試器:

The debugger is paused on the breakpoint.

以上的情況並不是意味着這個app崩潰了!它只是在這個斷點處暫停了。在左邊的執行堆棧裏面(假如你沒有看到執行堆棧的話,你可能需要切換到調試導航器),你可以看到你是從buttonTapped:到這裏的。這個調試導航器裏面,我們看到執行了一系列的UIKit的方法,並且裝載了一個新的view controller。(順便說句,斷點是一個非常好的工具來指出這個系統是怎麼工作的。)

如果想要離開你之前停留的地方,繼續運行這個程序,簡單的就是點擊繼續程序運行按鈕,或者在調試控制檯中輸入“c”。

顯然的是,一切並沒有如我們料想的一樣,這個app又奔潰了。我告訴過你,它有很多bug的。

注意:在你繼續之前,在initWithCoder:移除斷點或者使斷點無效。因爲他已經展現了他的目的,所以現在它可以離開了。

你可以在顯示行號的的地方右擊斷點,並且在彈出的菜單中選擇刪除斷點。你也可以拖出這個斷點離開窗口,或者在斷點調試器裏面移除。

假如你並不想移除這個斷點,你可以簡單的使斷點無效。爲了達到這個目的,你可以使用右擊彈出菜單,或者左擊一次這個斷點。判斷這個斷點是否有效,你可以看看這個斷點的顏色,當爲淺藍色了就是無效了,深藍色就是有效的。

Zombies!

回到這個崩潰。它是一個EXC_BAD_ACCESS,幸運的是調試器指到了他發生在那裏,在tableView:cellForRowAtIndexPath:

EXC_BAD_ACCESS error on cellForRowAtIndexPath.

這是一個EXC_BAD_ACCESS崩潰,意味着在你的內存管理裏面有bug。不像SIGABRT,你將不會得到很明朗的錯誤消息。然而你可以使用一個讓你看到曙光的調試工具:Zombies!

打開這個項目的scheme editor:

The Edit Scheme menu option.

選擇Run 選項,然後選擇Diagnosics標籤。勾上Enable Zombie Objects選項:

Enabling the Zombie Objects diagnostic option.

現在運行這個app。這個app仍然崩潰,但是現在你將會得到下面的錯誤消息:

Problems[18702:f803] *** -[__NSArrayM objectAtIndex:]: message sent to deallocated instance 0x6d84980

上面這個就是zombie enable 工具所做的,做個小概括:無論什麼時候你創建了一個新對象(通過發送“alloc”消息),一塊內存將會爲這個對象的實例變量保留。當這個對象被釋放,他的保留計數(retain count)變成0,這塊內存將會被釋放。

但是,你可能仍然有許多的指針指向這個已經失效的內存,這些都是建立在假設這裏有一個有效的對象存在的情況下。假如你程序的某些部分試着使用這個野指針,這個app將會伴隨着EXC_BAD_ACCESS的錯誤崩潰掉。

(假如你是很幸運的話,這個程序將會崩潰。假如你沒那麼幸運的哈,這個app將會使用這個死亡的對象,各種各樣的破壞可能相繼發生,特別是某個指針所指向的這個內存區域已經被一個新的對象重新分配了。)

當這個zombie工具被啓用之後,即使這個對象被釋放了,這個對象的內存也不會被清理。所以,那塊內存將會被標記爲“長生不死的”。假如你試着之後又去使用這塊內存,這個app能夠意識到你的錯誤操作,並且app將會拋出“message sent to daellocated instance”錯誤並且終止運行。

因此這就是之前發生的事。這行就是使用了不死的對象:

	cell.textLabel.text = [list objectAtIndex:indexPath.row];

這個cell對象和他的textLabel應該是好的,那麼indexPath也應該是正確的,因此我猜測在這個問題下,這個不死的對象應該是“list”。

你多半其實已經有個很好的線索來懷疑這個“list”,因爲這個錯誤消息說:

-[__NSArrayM objectAtIndex:]

這個不死的對象的類是__NSArrayM。假如你已經有一段時間的cocoa編程經驗,你應該就會知道一些基本的類,就像NSString和NSArray實際上是“class clusters”,這就意味着就像NSString或者NSArray這些原始的類在一些底層的地方會被特殊的類代替。所以在這裏你可以看到一些NSArray類型的對象,也就是這個“list”其實應該是一個NSMutableArray。

假如你卻是想要確認一下,你可以增加一個NSLog()在分配了“list”數組那行代碼之後:

NSLog(@"list is %p", list);

這裏將會打印出和錯誤消息一樣的內存地址(在我這裏的情況下是0x6d84980,但是你自己測試的時候,地址就會不一樣的)。

你也可以在調試器裏面使用“p”的命令來打印出這個“list”變量的地址(和這個相對的命令就是“po”,這個命令將會打印出這個實際的對象,而不是地址)。這樣方便的地方就是你可以省略很多額外增加NSLog()的步驟和從新編譯這個app、

(lldb) p list

注意:非常不幸的是,上面這些命令在xcode4.3裏面並沒有執行的很好。由於一些原因,這個地址一直都是展示的0×00000001,可能是因爲這個class cluster吧。

在GDB調試器下面,那些命令就執行的很好,在調試器的變量窗口展示出“list”都是zombie。因此我覺得這個是LLDB的bug。

The GDB debugger points out which object is the zombie.

爲這個list 數組分配空間的地方就在initWithCoder:,就是下面這樣:

		list = [NSMutableArray arrayWithCapacity:10];

由於這裏不是ARC(Automatic Reference Counting)(自動引用計數)項目,所以是人工管理內存,所以這裏你需要retain這個變量:

// in initWithCoder:
		list = [[NSMutableArray arrayWithCapacity:10] retain];

爲了避免內存泄露,你也不得不在dealloc函數中釋放這個對象,就像下面這個:

- (void)dealloc
{
	[list release];
	[super dealloc];
}

再一次運行這個app。它又崩潰在這同樣的一行,但是注意這個調試窗口輸出的東西改變了:

Problems[8266:f803] array contents: (
    One,
    Two,
    Three,
    Four,
    Five
)

由上面信息可以知道這個array已經分配了內存空間和包含了字符串的。這個崩潰的提示不再是EXC_BAD_ACCESS,而是SIGABRT,所以你需要再一次設置這個Exception Breakpoint。將這個解決了,繼續找其他的bug!

注意:即使你使用了ARC,在這樣的內存管理錯誤下也是一個非常大的事,你也會崩潰,得到一個EXC_BAD_ACCESS的錯誤,特別是假如你使用了不安全保留屬性。

我的小提議:無論你什麼時候得到一個EXC_BAD_ACCESS錯誤,你都可以開啓zombie objects,然後再試試。

注意一點:你不應該一直啓用zombie objects。因爲這個工具將永遠不會釋放內存,只是簡單標記一下這個內存是不死的,你最終將會在某個時候耗盡所有的內存。因此你應該在排查內存相關的錯誤的時候纔開啓zombie objects,其他時候應該關閉它。

Stepping Through the App(單步調試)

使用斷點來解決這個新的問題。將斷點放置在剛剛崩潰那一行:

Setting the breakpoint on cellForRowAtIndexPath.

重新運行這個程序,點擊按鈕。你將會在第一次執行tableView:cellForRowAtIndexPath:的時候進入調試器。注意啊,這個時候,app只是因爲斷點暫停了,並沒有崩潰。

你想要準確的知道這個程序崩潰時的一些細節。請點擊繼續執行按鈕,或者在(lldb)的提示後輸入“c”來繼續執行。程序將會從暫停的地方繼續執行。

什麼事情也沒有發生,你仍然暫停在tableView:cellForRowAtIndexPath:這個函數的斷點處。但是在調試窗口卻顯示:

Problems[12540:f803] array contents: (
    One,
    Two,
    Three,
    Four,
    Five
)

這就意味着tableView:cellForRowAtIndexPath:在第一次執行的時候沒有任何問題,因爲NSLog()在斷點之後執行了。因此這個app能夠很好地創建第一個cell。

假如你鍵入以下的到調試提示之後:

(lldb) po indexPath

在調試窗口應該可以輸出下面的:

(NSIndexPath *) $3 = 0x06895680 <NSIndexPath 0x6895680> 2 indexes [0, 1]

以上重要的部分是[0, 1]。就是這個NSIndexPath對象爲section 0和row 1。換句話說,這個tableview現在就在請求第二行。從這裏我們可以推測這個app在第一次創建cell的時候沒有任何問題,正如剛剛這裏就沒有發生崩潰。

多點幾次這個繼續按鈕。在某一個特定的時候,這個程序崩潰了,並且輸出一下錯誤消息:

Problems[12540:f803] *** Terminating app due to uncaught exception 'NSRangeException',
reason: '*** -[__NSArrayM objectAtIndex:]: index 5 beyond bounds [0 .. 4]'
*** First throw call stack:
. . . and so on . . .

假如你檢查這個indexpath對象的話,你可以看到:

(lldb) po indexPath

(NSIndexPath *) $11 = 0x06a8a6c0 <NSIndexPath 0x6a8a6c0> 2 indexes [0, 5]

Section依然是0,但是這個row的索引是5。注意哦,這個錯誤的消息也是說“index 5”。因爲計數是從0開始的,當到5的時候實際上意味着已經是6的位置了。但是這裏只有5項。顯然這個tableview認爲這裏實際上有更多的行。

所以這個犯人就是下面的方法:

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
	return 6;
}

這個方法其實應該被寫成這樣的:

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
	return [list count];
}

刪除斷點或者使斷點無效,然後再次運行這個程序。終於這個tableview顯示出來了,並且沒有了崩潰!

注意:這個“po”命令對於檢查你的對象是非常有用的。你可以在程序暫停在調試器的時候,或者在設置一個斷點的時候,或者在崩潰的時候,使用這個命令。你需要確定的是這個方法當前在調用堆棧裏面是高亮的,否則這個調試器將找不到這個變量。

你也可以在調試窗口的左邊看到這些變量,但是就算看到了也不是很方便就能知道細節的:

The debugger shows the content of your variables.

Once more, with feeling

我剛剛說了沒有崩潰的現象了?好,現在我們來試試滑動刪除。這個app又終止了在tableView:commitEditingStyle:forRowAtIndexPath:

Swipe-to-delete will make the app crash.

錯誤消息是:

Problems[18835:f803] *** Assertion failure in -[UITableView _endCellAnimationsWithContext:],
/SourceCache/UIKit_Sim/UIKit-1912.3/UITableView.m:1046

這個錯誤看起來像是來自UIKit,並不是來自app的代碼。多次輸入幾次“c”來讓系統拋出異常,這樣可以你可以得到更多有用的信息:

*** Terminating app due to uncaught exception 'NSInternalInconsistencyException',
reason: 'Invalid update: invalid number of rows in section 0.  The number of rows
contained in an existing section after the update (5) must be equal to the number
of rows contained in that section before the update (5), plus or minus the number
of rows inserted or deleted from that section (0 inserted, 1 deleted) and plus or
minus the number of rows moved into or out of that section (0 moved in, 0 moved out).'
*** First throw call stack: . . .

經過這些,上面給你一個非常漂亮的解釋。這個app告訴這個tableview裏面一行要刪除,但是某人卻忘記從數據源裏面移除這行的數據。因此這個table view看起來沒有什麼改變。修改這個這方法:

- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath
{
	if (editingStyle == UITableViewCellEditingStyleDelete)
	{
		[list removeObjectAtIndex:indexPath.row];
		[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
	}
}

太好了,看起來這樣做起效了,你終於有一個不會崩潰的app了。

Where to go from here?(何去何從)

記住下面幾點:

假如你的app崩潰了,第一件事就是找到是哪裏崩潰了,爲什麼崩潰了。一旦你知道了這兩點,修復這個崩潰就很簡單了。調試器可以幫助你,但是你需要知道怎麼樣讓他幫助你。

有些崩潰可能是隨機出現的,這個也是最困難的一個,特別是當你正在使用多線程。但是大多數,你可以試試,會發現一些固定的方法來讓你的程序每次崩潰。

你可以想出怎麼使用最少的步驟來減少崩潰的現象,這樣你將找到一個好的方法來修復這個bug(也就是說他將不會發生)。但是假如你沒有確定不會再生了這個錯誤,你就絕不能確定你的修改已經修復了這個bug。

祕訣:

1.假如崩潰在main.m裏面,就可以設置全局異常斷點(Exception Breakpoint)。

2.在異常斷點開啓的狀態下,你也沒有得到得到有用的信息。在這種情況下,多繼續幾次運行這個app,或者在調試提示後面輸入“po $eax”命令。

3.大多數崩潰的一般原因和一些bug都是在你的xib中或者storyboard中的連接丟失了或者是錯誤的連接。這些情況不會在編譯錯誤裏面顯示,因此你一般不知道。

4.不要忽略編譯警告。假如你有編譯警告,就說明你有些東西可能會出錯。假如你不知道爲什麼你會到一個編譯警告,最好去搞明白它. 這些都是安全的做法!

5.在設備上調試可能會和在模擬器上面有些微的不同。這兩個環境不是完全一樣,你將會得到不同的結果。

例如,當你運行一個有問題的程序在iphone4上的時候,這第一個崩潰就會發生在NSArray初始化的時候,因爲你缺少一個nil標記,而不是會因爲當這個app執行setList:的時候的時候崩潰。所以說上面那個原則方法就可以幫你找到崩潰問題的根源本質。

不要忘記靜態分析工具(static analyzer tool),這個工具將會捕獲更多的錯誤。假如你是一個初學者,推薦你開啓它。你可以在Build Settings界面上爲你的工程設置:

Setting the static analyzer to run on each build.

調試愉快吧!

發佈了55 篇原創文章 · 獲贊 5 · 訪問量 14萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章