[ios-必看] IOS調試技巧:當程序崩潰的時候怎麼辦 iphone IOS

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


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

咱們須要作的第一件事就是:不要驚慌。數組

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

 

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

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

準備開始app

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

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

在模擬器上面運行你的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 0x6a33840Problems[14465:f803]***Terminating app due to uncaught exception 'NSInvalidArgumentException',
reason:'-[UINavigationController setList:]: unrecognized selector sent to instance 0x6a33840'***Firstthrow call stack:(0x13ba0520x154bd0a0x13bbced0x1320f000x1320ce20x29ef0xf9d60x108a60x1f7430x201f80x13aa90x12a4fa90x138e1c50x12f30220x12f190a0x12f0db40x12f0ccb0x102a70x11a9b0x27920x2705)
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.'***Firstthrow call stack:(0x13ba0520x154bd0a0x13b9f110x9b10320x922f7b0x922eeb0x93dd600x23091a0x13bbe1a0x13258210x22f46e0xd6e2c0xd73a90xd75cb0xd6c1c0xfd56d0xe7d470xfe4410xfe45d0xfe4f90x3ed650x3edac0xfbe60x108a60x1f7430x201f80x13aa90x12a4fa90x138e1c50x12f30220x12f190a0x12f0db40x12f0ccb0x102a70x11a9b0x28720x27e5)
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:

@interfaceMainViewController:UIViewController@property(nonatomic, retain)NSArray*list;@property(nonatomic, retain)IBOutletUIButton*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 =0x01446e84NSException

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

你能夠對這個對象調用任何方法。例如:

(lldb) po [$eax name]

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

(lldb) po [$eax reason]

這個將會輸出錯誤消息:

(unsignedint) $4 =114784400Receiver() 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 =(119189;8237);
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:'***Firstthrow call stack:(0x13ba0520x154bd0a0x1362a780x99a2db0xaaee30xab5890x96dfd0xa58510x503010x13bbe720x1d6492d0x1d6e8270x1cf4fa70x1cf6ea60x1cf65800x138e9ce0x13256700x12f14f60x12f0db40x12f0ccb0x12a38790x12a393e0x11a9b0x27220x2695)
terminate called throwing an exception

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

-(UITableViewCell*)tableView:(UITableView*)tableView cellForRowAtIndexPath:(NSIndexPath*)indexPath
{staticNSString*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 =(00;32044); text ='Title'; layer =<CALayer:0x6a6d240>>Problems[7880:f803] the cell is<UITableViewCell:0x6877620; frame =(00;32044); text ='Title'; layer =<CALayer:0x6867140>>Problems[7880:f803] the cell is<UITableViewCell:0x6da1e80; frame =(00;32044); text ='Title'; layer =<CALayer:0x6d9fae0>>Problems[7880:f803] the cell is<UITableViewCell:0x6878c40; frame =(00;32044); text ='Title'; layer =<CALayer:0x6878f60>>Problems[7880:f803] the cell is<UITableViewCell:0x6da10c0; frame =(00;32044); text ='Title'; layer =<CALayer:0x6d9f240>>Problems[7880:f803] the cell is<UITableViewCell:0x6879640; frame =(00;32044); 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
{staticNSString*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"];}returnself;}

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

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"];}returnself;}

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

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<NSIndexPath0x6895680>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]'***Firstthrow call stack:...and so on ...

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

(lldb) po indexPath

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

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

因此這個犯人就是下面的方法:

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

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

-(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).'***Firstthrow 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.

調試愉快吧!

相關文章
相關標籤/搜索