我的ios app崩潰了,該怎麼處理呢?(一)

跟我一起學習如何調試和修復可怕的應用程序崩潰問題吧!

要做的第一件事是:不要驚慌!

修復崩潰並不費勁。如果你已經陣腳大亂,可能是你把情況想的太嚴重了。不要指望說句咒語就能讓bug奇蹟般地消失,你需要採取有條不紊的方法,學會通過崩潰找出原因。

首先是要在你的代碼中找出發生崩潰的確切位置:在哪個文件的哪一行。Xcode調試器會幫助你,不過你也要明白如何充分地運用它,這也正是本教程將向你展示的!

本教程面向所有開發人員,從初級到高級。即使你是一個經驗豐富的iOS開發者,也可能會找到一些技巧和竅門。

入門
下載示例工程。正如你所看到的,這是一個錯誤的程序!當你在Xcode中打開該項目,顯示至少有八個編譯器警告,這通常是麻煩的前兆順便說一下,本教程我們使用Xcode4.3,當然4.2版應該工作也很好。

注:要跟隨本教程,應用程序需要運行在iOS 5的模擬器上。如果您運行應用程序在真機上,也還是會崩潰,但崩潰發生的順序可能不同。

運行這個app,看一看發生了什麼。


嗨,它崩潰了!:-(

崩潰的發生分兩種情況:SIGABRT(也稱爲EXC_CRASH),EXC_BAD_ACCESS(也可以顯示在SIGBUS或SIGSEGV的名字下)。

SIGABRT是很好解決的,因爲它是可控的。終止該app的原因是系統發現這個app做了一些未經允許的事。相反,EXC_BAD_ACCESS難解決很多,因爲它只發生在應用損壞的狀態下,通常是由內存管理問題引起的。

幸運的是,這第一次崩潰(後續還多着呢)是一個SIGABRT。SIGABRT崩潰發生時通常都會在Xcode的調試輸出窗格(窗口的右下角)輸出一個錯誤消息。(如果你看不見調試輸出窗格,點擊Xcode窗口右上角那三個視圖圖標的中間那個,就會顯示出調試區域了。如果調試輸出窗格還是看不到,你可能還得點擊調試區域右上角那三個視圖圖標(挨着搜索區域的那三個)的中間那個)。在本例中,崩潰信息是這樣的:

2013-08-15 09:54:39.538 Problems[393:c07] -[UINavigationController setList:]: unrecognized selector sent to instance 0x6ed5800

2013-08-15 09:54:39.563 Problems[393:c07] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[UINavigationController setList:]: unrecognized selector sent to instance 0x6ed5800'

*** First throw call stack:

(0x14a4052 0xea4d0a 0x14a5ced 0x140af00 0x140ace2 0x251f 0x129d6 0x138a6 0x22743 0x231f8 0x16aa9 0x138efa9 0x14781c5 0x13dd022 0x13db90a 0x13dadb4 0x13daccb 0x132a7 0x14a9b 0x23c2 0x22f5)

terminate called throwing an exception(lldb) 

學會解讀這些錯誤消息很重要,因爲它們含有能指明錯誤如何發生的重要線索。這裏,有用的部分是下面這句:

-[UINavigationController setList:]: unrecognized selector sent to instance 0x6ed5800
這條錯誤消息的意思是該app試圖調用了不存在的方法。通常這種崩潰都是由於對象調用了它自己沒有實現的方法。這裏的對象是UINavigationController(內存地址0x6ed5800),方法是 setList:。

知道崩潰的原因是一個良好的開端,但你最先要做的是修復代碼中的錯誤。你需要找到源文件的名稱和錯誤發生的行數。你可以藉助調用棧來尋找(也被稱爲堆棧跟蹤或回溯)。

app崩潰時,Xcode窗口的左窗格會切換到Debug導航。它顯示了應用程序中的活動線程,並突出崩潰的線程。通常線程1,就是app的主線程,也正是做絕大部分工作的地方。如果你的代碼使用了隊列或後臺線程,那麼app也可以在其他線程崩潰。


目前,Xcode把main.m文件的main()函數作爲問題的根源突出顯示了出來。這並不能說明多少,所以你必須深入挖掘信息。

要看到更多的調用堆棧,拖動調試導航器底部滑塊一直到右邊。這會顯示崩潰時完整的調用堆棧信息:


列表中的每一條都是app或者ios框架中的一個函數或方法。堆棧調用會展示出當前在app中活動的函數或方法。調試器已經終止了app,及時凍結了所有函數和方法。

最底部的函數是最先調用的。當它執行到某處時調用了它上面的函數。這是應用程序的起點,它總是會在底部。 main()依次調用 UIApplicationMain()。就是編輯窗口綠色箭頭指向的那行(xcode右側窗口中突出顯示那行)。

進一步追溯堆棧,UIApplicationMain()調用了UIApplication對象的方法_run,它有調用了CFRunLoopRunInMode(),接着又調用了CFRunLoopRunSpecific(),依次類推,直到調用__pthread_kill。

除了main(),堆棧調用中的其他所以函數和方法都是灰色的。這是因爲他們來自內置的iOS框架。看不到它們的源代碼。

在這個堆棧追蹤中,可以看到源代碼的只有main.m,正如xcode源碼編輯器顯示的。不過這並不是崩潰產生的真正源頭。這往往會弄暈了新的開發者,不過一會兒我會告訴你如何理解它。

幸好,點擊堆棧追蹤中的任何一個條目,都會看到一堆彙編代碼,而這些很可能對你一點也沒有。


噢,但願我們能有源代碼!:-)

異常斷點

那麼,如何在代碼裏找到引起崩潰的那一行呢?嗯,無論何時當你看到象這樣的堆棧追蹤時,程序都會拋出一個異常。

當程序被發現正在做一些它不該做的事時,就會發生異常。 你現在看到的是這個異常的後果:應用程序做錯了什麼,異常被拋出,,Xcode顯示給你結果。 理想情況下,你想要清楚地看到該異常被拋出的地點。

所幸,你可以使用異常斷點讓xcode在那一瞬間暫停程序。斷點是一種調試工具,它可以讓你的app在某一時刻暫停。在本教程的第二部分,你會看到更多的斷點,不過此刻,你看到的這個斷點會在異常拋出前一刻暫停程序的。

要設置異常斷點,我們必須切換到斷點導航:


底部是一個小的+按鈕。單擊此按鈕並選擇“Exception Breakpoint”斷點:


一個新的斷點將被添加到列表:


單擊“完成”按鈕關閉彈窗。注意,xcode工具欄中的斷點按鈕現在已經被開啓。如果你要運行的程序沒有任何斷點,你就可以點擊這個按鈕來關閉它。但現在,保留它,並再次運行應用程序。


真好!源代碼編輯器現在指向了代碼中的一行——沒有討厭的彙編東東——注意左邊的調用堆棧(你可能需要通過調試導航切換到調用堆棧,這取決於你在xcode中的設置)看起來也不同了。

顯然,罪魁禍首就是AppDelegate文件中application:didFinishLaunchingWithOptions: 方法裏的下面這一行:

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

再次來看錯誤信息:

2013-08-15 14:42:21.684 Problems[835:c07] -[UINavigationController setList:]: unrecognized selector sent to instance 0x6cb71c0

代碼中,viewController.list=xxx實際上是在調用setList:方法,因爲list是MainViewController 類裏的一個property。然而,根據錯誤消息顯示,viewController變量不指向MainViewController對象,而是指向一個UINavigationController 對象——當然,UINavigationController沒有 “list” property!這裏把它們混淆了。

打開Storyboard文件,看一看窗口的rootViewController實際上指向什麼:


啊哈!其實storyboard的初始視圖控制器是一個導航控制器。這也就解釋了爲什麼window.rootViewController 是一個UINavigationController的對象,而不是你所期望的MainViewController。爲了修復這個bug,用以下內容替換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的一個引用,一旦有了這個引用,你就能通過詢問導航控制器的topViewController取得指向MainViewController的指針。現在,viewController變量應該指向了正確的對象。

注意:無論何時得到“unrecognized selector sent to instance XXX” 的錯誤,請檢查對象的類型是否正確以及它是否有這個名稱的方法。通常你會發現你在對一個不對的對象調用這個方法,由於指針變量所指向的值不對,它根本就不是你要的那個對象。

這種錯誤的另一常見原因是 方法名稱的拼寫錯誤。稍後你會看到一個這樣的例子。

你的第一個內存錯誤

此刻應該已經修復了第一個問題。再次運行該應用程序。哎呀,它在同一行又崩潰了,只是這次是一個EXC_BAD_ACCESS錯誤。這意味着應用程序有一個內存管理問題。


與內存相關的崩潰源頭是很難定位到的,因爲罪魁禍首很可能是程序之前已經執行過的。如果發生故障的代碼片段破壞了內存結構,直到很久以後此結果纔會出現在一個完全不同的地方。

事實上,測試時這個bug可能永遠都不會出現,只會在你的用戶們的設備上張牙舞爪。你不希望這樣的事情發生!

然而,這種特殊的崩潰是很容易修復的。如果你看看源代碼編輯器,Xcode已經在這一行警告過你了。看到緊挨着行標左側的黃色三角了嗎?這是編譯器警告。如果你點擊黃色三角形,Xcode應該會彈出一個這樣的修復建議:


在警告中提及的地點,代碼通過賦值一個對象列表初始化了一個NSArray 對象,而此種列表應該是以nil結尾的。不過代碼並沒有這麼寫,所以NSArray被弄迷糊l

了。它試圖讀取不存在的對象,所以app崩潰了。

這是一個不該犯的錯誤,特別是xcode都已經警告過你了。通過下面的方法爲列表添加nil來修復代碼(或者,你可以簡單地選擇修復菜單選項):

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

這個類不支持鍵值編碼兼容

再次運行應用程序,看看這個項目裏還爲你藏了哪些其他有趣的bug。你是怎麼知道的?它再次在main.m裏崩潰。異常斷點仍處於啓用狀態,我們看不到應用程序的任何源代碼處於高亮顯示,這次崩潰真的沒有發生在app的源代碼中。調用堆棧證實了這一點:除main():以外,其餘方法都不是app的。


如果從上往下瀏覽這些方法名,你會發現好多都是關於NSObject的和鍵 -值編碼的。其下方有一個 [UIRuntimeOutletConnection connect]的調用。我不知道那是什麼,不過看起來是和連接outlets有關的。其下方是一些關於從nib文件加載views的方法。以上這些就已經給我們提供了線索了。

不過Xcode的調試窗格中並沒有直觀的錯誤信息。這是因爲還沒有異常被拋出。在調試窗格告知我們異常發生原因之前,異常斷點已經暫停了程序。在啓用異常斷點的情況下,有時可以獲得部分錯誤信息,有時則不能。

要看到完整的錯誤信息,點擊“調試”工具欄上的“繼續執行程序”按鈕:


你可能還需要再次點擊它,然後會獲得完整的錯誤信息:

Problems[14961:f803] *** Terminating app due to uncaught exception 'NSUnknownKeyException', 
reason: '[<MainViewController 0x6b3f590> 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

像之前一樣,你可以忽略底部的那些數字。它們代表了調用棧,但在左側調試導航欄裏,你已經擁有了一個更方便 - 可讀的格式。

有意義的點是:

  • NSUnknownKeyException
  • MainViewController
  • “this class is not key value coding-compliant for the key button”
異常的名稱,NSUnknownKeyException,通常是錯誤的很好說明。它告訴你在某處有一個 “unknown key” 。顯然,某處就是MainViewController,the key就是 “button”。

正如我們已經建立的,這一切都發生在加載nib文件時。雖然這個app使用的是storyboard,不是nibs,但就內部實現而言,storyboard就是nibs的一個集合,故一定是storyboard那裏出了問題。

檢查MainViewController中的outlets:


在連接檢查器裏,可以看到試圖管理器中心的按鈕是與MainViewController的按鈕出口連接着的。storyboard/nib連接了出口button,但是錯誤信息指出不能找到此出口。

看看MainViewController.h:


button的@property 定義是存在的,那麼問題在哪呢?如果你有注意到編譯警告的話,可能已經找到了。

要是還沒有,就去查看一下MainViewController.m裏的@synthesize list.現在看到問題所在了嗎?

代碼實際上並沒有@synthesize button。

未完待續!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章