轉自:http://www.ityran.com/archives/1143編程
------------------------------------------------數組
歡迎回到當程序崩潰的時候怎麼辦 教程!xcode
在這個教程的第一部分,咱們介紹了SIGABRT和EXC_BAD_ACCESS錯誤,而且舉例說明了一些使用xcode調試器(Xcode debugger)和異常斷點(Exception Breakpoints)解決問題的策略。安全
可是咱們的app仍然有一些問題!就像咱們看到的,他工做的並非很好,而且這裏仍然有許多潛在的可能崩潰的問題。多線程
幸運的是,在這個教程的第二部分,也是最後一部分,咱們能夠學習更多的技術來處理這些問題。app
因此咱們就不在囉嗦了,讓咱們回到繼續修正這個充滿bug的app中吧!框架
Getting Started: When What’s Supposed to Happen, Doesn’tiphone
在第一部分咱們中止的地方,通過許多的調試工做以後,咱們運行這個程序他是不會崩潰的。可是他卻展示了一個沒有預料到的空的table,就像下面同樣:ide
當你以爲一些事情應該發生,可是卻沒有發生的時候,這裏有些你可使用一些技巧來排除問題。在這個教程裏面,咱們首先是學習使用NSlog來解決這個問題。函數
這個table view controller的類是ListViewController。在一系列的任務執行以後,這個app應該裝載ListViewController,而且在屏幕上面顯示出來。你能夠作一個測試,來肯定view controller的方法是執行了的。因此viewDidLoad這個方法看起來應該是一個好地方來作測試。
在ListViewController.m,增長一個NSLog()到viewDidload,就像下面同樣:
當你運行這個app時,你應該指望當咱們點擊了「Tap Me」按鈕後在調試窗口看到「viewDidLoad is called」這樣文字。如今就來試試,點都不驚訝,在調試窗口什麼也沒有出現。那就意味着ListViewController類根本沒有被使用!
這個多半意味着,你可能忘記了告訴storyboard你想要爲table view controller場景使用ListViewController類。
由上圖咱們能夠看出,在身份檢查器(Identity Inspector)的類屬性區域是設置的默認值UITableViewController。改變這個Custom Class下面的class爲ListViewController,而後再一次運行這個app。如今在調試窗口應該就會出現「viewDidLoad is called」文字:
可是此次app將會再一次崩潰,可是倒是一個新的問題。
注意:一旦你的代碼好像沒起什麼什麼做用的話,放置一些NSLog()在確切的地方,來看看是否這個方法是被執行了的和cpu經過怎麼樣路徑執行這個方法。使用NSLog()來測試你假設將會執行的代碼。
Assertion Failures
這個新的有趣的崩潰。它是一個SIGABRT,而且在調試窗口打印出來的是如下消息:
咱們獲得的是一個執行UITableView的一些方法的一個「斷言錯誤(assertion failure)」。當某些東西出錯了以後,一個斷言是一個內部相容性的檢查器,而且會拋出一個異常。你也能夠放置斷言在你的代碼裏。例如:
在上面的方法裏面,咱們讓一個NSString對象做爲這個函數的變量,可是代碼卻不容許調用者傳遞一個nil或者長度小於3的字符串。假如這些條件中的一個不匹配的話,這個app將會終止,而且拋出一個異常。
你可使用斷言來做爲一個防護性編程技術,所以你應該肯定這個就是咱們想要的代碼行爲。斷言一般只在調試編譯下有用的,所以他們對發佈到app store的最終的app是沒有運行時的影響的。
在這個狀況下,某些狀況觸發了一個UITableView的斷言錯誤,可是你並無徹底肯定在那個地方。App也是中止在main.m裏面,而且在執行堆棧裏面只包含了框架(framework)的方法。
從這些方法的名字,咱們能夠猜想這個錯誤發生在重畫這個tableview的某些地方。例如,咱們能夠看到layoutSubviews和_updateVisibleCellsNow:這些名字的方法。
繼續運行這個app來看看是否能夠獲得一些比較好的錯誤消息—–記住,如今只是在拋出異常的時候暫停了程序,並無崩潰。點擊繼續程序按鈕,或者在調試窗口鍵入下面的命令:
你可能不得很少點擊幾回繼續按鈕,「c」命令也是一個簡短的繼續指令,和點擊繼續按鈕一個效果,並非就直接執行到最後。
如今這個調試窗口噴發出一些比較有用的信息:
太好了,這是一個至關好的一個線索。顯然這個UITableView的數據源沒有從tableView:cellForRowAtIndexPath:方法返回一個有效的cell,所以在ListViewController.m方法裏面增長一些調試輸出信息來看看:
你增長一個NSLog()標記。再一次運行這個app,看看輸出了什麼:
從以上信息咱們能夠看出,調用dequeueReusableCellwithIdentifier:返回的倒是nil,這就意味着使用「Cell」做爲標識符的cell可能不存在(由於這個app使用的是標準的cell的storyboard)。
固然,這也是愚蠢的bug,而且毫無疑問的是,在之前解決這個須要很長的時間,可是如今卻不是了,由於xcode已經經過靜態編譯警告了你:「Prototype cells must have reuse identities。(標準的cell必須有重用的標識)」。這個是不能忽視的警告:
打開storyboard,選擇這個標準的cell(在tableview的頂端,而且顯示的是「Title」的單獨的一個cell),而且設置cell的標識符爲「Cell」:
將那個修復了以後,因此的編譯警告應該沒有了。運行這個app,如今這個調試窗口應該會打印出來:
Verify Your Assumptions
你的NSLog()打印出來的消息,已經告訴咱們6個table view cell被建立了,可是在table上面什麼都看不見。怎麼回事呢?假如你在模擬器裏面處處點擊一下,你將會注意到tableview中6個cell中的第一個卻可以被選中。因此,顯然cells都是存在的,只是他們都是空的:
是時候須要更多的調試記錄了。將先前的NSLog()標記改變一下:
如今你打印出來就是你的數據模塊的內容。運行這個app,看看顯示出來的是什麼:
上面的很好的解釋了爲何在cell裏面什麼都沒有看到的緣由:由於這個文字(text)始終是nil。然而,假如你檢查你的代碼,而且在initWithStyle:方法裏面顯示的添加了不少的字符串到list array裏面:
就像上面那樣,這是測試你的假設是否是正確的一個很好的方法。可能你還想更準確的看看這個array裏面到底有什麼東西。改變先前在tableView:cellForRowAtIndexPath:裏面的NSLog()爲這樣:
至少這樣能夠給你展現一些東西。運行這個app。假如你還沒準備好猜想會發生什麼狀況,調試窗口已經給你打印出來了:
哈哈,你的臉色瞬間陰沉下來。上面的代碼竟然沒有起做用,由於你可能忘了在首先爲這個array對象申請內存空間。這個「list」因此一直爲nil,所以調用addObject: 和objectAtIndex:不會起任何的做用。
你應該在你的view controller被裝載的時候爲這個list對象分配空間,所以在initWithStyle:方法裏面應該是一個不錯的選擇。修改那個方法爲:
試一試。我暈,依然什麼都沒有!調試窗口輸出依然是:
通過了這麼多假設和修改,可是仍是什麼都沒有,這些真的是很是使人沮喪啊,可是請記住你可能會一直繼續到最後,直到你弄清楚了全部的假設。因此如今的問題就是難道initWithStyle:沒有被調用?
Working With Breakpoints
你可能又會在代碼裏面放置另一個NSLog()標誌,可是其實你徹底可使用另外的工具:斷點( breakpoints)。你已經看到過不管何時只要有異常拋出的時候,程序就會終止的異常斷點(Exception Breakpoint)了。你其實也能夠增長其餘的斷點,而且能夠放置到代碼的任何地方。一旦你的程序運行到斷點的地方,這個斷點就會被觸發,而且程序就會進入調試模式。
你能夠經過點擊代碼編輯區前面的行號來放置特殊的斷點:
這個藍色的箭頭所指示的那一行就有一個斷點了。你也能夠在斷點導航器(Breakpoint Navigator)裏面看到這個新的斷點:
再一次運行這個app。假如initWithStyle:確實是會被調用的話,那麼你點擊了「Tap Me!」按鈕以後,當這個ListViewController被裝載的時候,這個app將會暫停,而且會進入調試器。
可能正如你所料的,什麼事情也沒有發生。initWithStyle:沒有被調用。其實這個是能夠講得通的,由於view controller是從storyboard(或者xib)中裝載的,因此使用的應該是initWithCoder:方法。
將以前initWithStyle:方法替換爲initWithCoder::
而且保持斷點在這個方法上面,來看看它是怎麼工做的:
一旦你點擊了那個按鈕,這個app將會進入調試器:
以上的狀況並非意味着這個app崩潰了!它只是在這個斷點處暫停了。在左邊的執行堆棧裏面(假如你沒有看到執行堆棧的話,你可能須要切換到調試導航器),你能夠看到你是從buttonTapped:到這裏的。這個調試導航器裏面,咱們看到執行了一系列的UIKit的方法,而且裝載了一個新的view controller。(順便說句,斷點是一個很是好的工具來指出這個系統是怎麼工做的。)
若是想要離開你以前停留的地方,繼續運行這個程序,簡單的就是點擊繼續程序運行按鈕,或者在調試控制檯中輸入「c」。
顯然的是,一切並無如咱們料想的同樣,這個app又奔潰了。我告訴過你,它有不少bug的。
注意:在你繼續以前,在initWithCoder:移除斷點或者使斷點無效。由於他已經展示了他的目的,因此如今它能夠離開了。
你能夠在顯示行號的的地方右擊斷點,而且在彈出的菜單中選擇刪除斷點。你也能夠拖出這個斷點離開窗口,或者在斷點調試器裏面移除。
假如你並不想移除這個斷點,你能夠簡單的使斷點無效。爲了達到這個目的,你可使用右擊彈出菜單,或者左擊一次這個斷點。判斷這個斷點是否有效,你能夠看看這個斷點的顏色,當爲淺藍色了就是無效了,深藍色就是有效的。
Zombies!
回到這個崩潰。它是一個EXC_BAD_ACCESS,幸運的是調試器指到了他發生在那裏,在tableView:cellForRowAtIndexPath:
這是一個EXC_BAD_ACCESS崩潰,意味着在你的內存管理裏面有bug。不像SIGABRT,你將不會獲得很明朗的錯誤消息。然而你可使用一個讓你看到曙光的調試工具:Zombies!
打開這個項目的scheme editor:
選擇Run 選項,而後選擇Diagnosics標籤。勾上Enable Zombie Objects選項:
如今運行這個app。這個app仍然崩潰,可是如今你將會獲得下面的錯誤消息:
上面這個就是zombie enable 工具所作的,作個小歸納:不管何時你建立了一個新對象(經過發送「alloc」消息),一塊內存將會爲這個對象的實例變量保留。當這個對象被釋放,他的保留計數(retain count)變成0,這塊內存將會被釋放。
可是,你可能仍然有許多的指針指向這個已經失效的內存,這些都是創建在假設這裏有一個有效的對象存在的狀況下。假如你程序的某些部分試着使用這個野指針,這個app將會伴隨着EXC_BAD_ACCESS的錯誤崩潰掉。
(假如你是很幸運的話,這個程序將會崩潰。假如你沒那麼幸運的哈,這個app將會使用這個死亡的對象,各類各樣的破壞可能相繼發生,特別是某個指針所指向的這個內存區域已經被一個新的對象從新分配了。)
當這個zombie工具被啓用以後,即便這個對象被釋放了,這個對象的內存也不會被清理。因此,那塊內存將會被標記爲「長生不死的」。假如你試着以後又去使用這塊內存,這個app可以意識到你的錯誤操做,而且app將會拋出「message sent to daellocated instance」錯誤而且終止運行。
所以這就是以前發生的事。這行就是使用了不死的對象:
這個cell對象和他的textLabel應該是好的,那麼indexPath也應該是正確的,所以我猜想在這個問題下,這個不死的對象應該是「list」。
你多半其實已經有個很好的線索來懷疑這個「list」,由於這個錯誤消息說:
這個不死的對象的類是__NSArrayM。假如你已經有一段時間的cocoa編程經驗,你應該就會知道一些基本的類,就像NSString和NSArray其實是「class clusters」,這就意味着就像NSString或者NSArray這些原始的類在一些底層的地方會被特殊的類代替。因此在這裏你能夠看到一些NSArray類型的對象,也就是這個「list」其實應該是一個NSMutableArray。
假如你倒是想要確認一下,你能夠增長一個NSLog()在分配了「list」數組那行代碼以後:
這裏將會打印出和錯誤消息同樣的內存地址(在我這裏的狀況下是0x6d84980,可是你本身測試的時候,地址就會不同的)。
你也能夠在調試器裏面使用「p」的命令來打印出這個「list」變量的地址(和這個相對的命令就是「po」,這個命令將會打印出這個實際的對象,而不是地址)。這樣方便的地方就是你能夠省略不少額外增長NSLog()的步驟和重新編譯這個app、
注意:很是不幸的是,上面這些命令在xcode4.3裏面並無執行的很好。因爲一些緣由,這個地址一直都是展現的0×00000001,多是由於這個class cluster吧。
在GDB調試器下面,那些命令就執行的很好,在調試器的變量窗口展現出「list」都是zombie。所以我以爲這個是LLDB的bug。
爲這個list 數組分配空間的地方就在initWithCoder:,就是下面這樣:
因爲這裏不是ARC(Automatic Reference Counting)(自動引用計數)項目,因此是人工管理內存,因此這裏你須要retain這個變量:
爲了不內存泄露,你也不得不在dealloc函數中釋放這個對象,就像下面這個:
再一次運行這個app。它又崩潰在這一樣的一行,可是注意這個調試窗口輸出的東西改變了:
由上面信息能夠知道這個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(單步調試)
使用斷點來解決這個新的問題。將斷點放置在剛剛崩潰那一行:
從新運行這個程序,點擊按鈕。你將會在第一次執行tableView:cellForRowAtIndexPath:的時候進入調試器。注意啊,這個時候,app只是由於斷點暫停了,並無崩潰。
你想要準確的知道這個程序崩潰時的一些細節。請點擊繼續執行按鈕,或者在(lldb)的提示後輸入「c」來繼續執行。程序將會從暫停的地方繼續執行。
什麼事情也沒有發生,你仍然暫停在tableView:cellForRowAtIndexPath:這個函數的斷點處。可是在調試窗口卻顯示:
這就意味着tableView:cellForRowAtIndexPath:在第一次執行的時候沒有任何問題,由於NSLog()在斷點以後執行了。所以這個app可以很好地建立第一個cell。
假如你鍵入如下的到調試提示以後:
在調試窗口應該能夠輸出下面的:
以上重要的部分是[0, 1]。就是這個NSIndexPath對象爲section 0和row 1。換句話說,這個tableview如今就在請求第二行。從這裏咱們能夠推測這個app在第一次建立cell的時候沒有任何問題,正如剛剛這裏就沒有發生崩潰。
多點幾回這個繼續按鈕。在某一個特定的時候,這個程序崩潰了,而且輸出一下錯誤消息:
假如你檢查這個indexpath對象的話,你能夠看到:
Section依然是0,可是這個row的索引是5。注意哦,這個錯誤的消息也是說「index 5」。由於計數是從0開始的,當到5的時候實際上意味着已是6的位置了。可是這裏只有5項。顯然這個tableview認爲這裏實際上有更多的行。
因此這個犯人就是下面的方法:
這個方法其實應該被寫成這樣的:
刪除斷點或者使斷點無效,而後再次運行這個程序。終於這個tableview顯示出來了,而且沒有了崩潰!
注意:這個「po」命令對於檢查你的對象是很是有用的。你能夠在程序暫停在調試器的時候,或者在設置一個斷點的時候,或者在崩潰的時候,使用這個命令。你須要肯定的是這個方法當前在調用堆棧裏面是高亮的,不然這個調試器將找不到這個變量。
你也能夠在調試窗口的左邊看到這些變量,可是就算看到了也不是很方便就能知道細節的:
Once more, with feeling
我剛剛說了沒有崩潰的現象了?好,如今咱們來試試滑動刪除。這個app又終止了在tableView:commitEditingStyle:forRowAtIndexPath:
錯誤消息是:
這個錯誤看起來像是來自UIKit,並非來自app的代碼。屢次輸入幾回「c」來讓系統拋出異常,這樣能夠你能夠獲得更多有用的信息:
通過這些,上面給你一個很是漂亮的解釋。這個app告訴這個tableview裏面一行要刪除,可是某人卻忘記從數據源裏面移除這行的數據。所以這個table view看起來沒有什麼改變。修改這個這方法:
太好了,看起來這樣作起效了,你終於有一個不會崩潰的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界面上爲你的工程設置:
調試愉快吧!