IOS調試技巧:當程序崩潰的時候怎麼辦 xcode調試

轉自:http://www.ityran.com/archives/1143

------------------------------------------------

歡迎回到當程序崩潰的時候怎麼辦 教程!

在這個教程的第一部分,我們介紹了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,就像下面一樣:

  1. -(void)viewDidLoad
  2. {
  3. [super viewDidLoad];
  4. NSLog(@"viewDidLoad is called");
  5. }

當你運行這個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”文字:

  1. PProblems[18375:f803]You tapped on:<UIRoundedRectButton:0x6894800; frame =(119189;8237);
  2. opaque = NO; autoresize = RM+BM; layer =<CALayer:0x68948f0>>
  3. Problems[18375:f803] viewDidLoad is called

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

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

Assertion Failures

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

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

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

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

在上面的方法裏面,我們讓一個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來看看是否可以得到一些比較好的錯誤消息—–記住,現在只是在拋出異常的時候暫停了程序,並沒有崩潰。點擊繼續程序按鈕,或者在調試窗口鍵入下面的命令:

  1. (lldb) c

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

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

  1. ***Terminating app due to uncaught exception 'NSInternalInconsistencyException',
  2. reason:'UITableView dataSource must return a cell from tableView:cellForRowAtIndexPath:'
  3. ***Firstthrow call stack:
  4. (0x13ba0520x154bd0a0x1362a780x99a2db0xaaee30xab5890x96dfd0xa58510x50301
  5. 0x13bbe720x1d6492d0x1d6e8270x1cf4fa70x1cf6ea60x1cf65800x138e9ce0x1325670
  6. 0x12f14f60x12f0db40x12f0ccb0x12a38790x12a393e0x11a9b0x27220x2695)
  7. terminate called throwing an exception

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

  1. -(UITableViewCell*)tableView:(UITableView*)tableView cellForRowAtIndexPath:(NSIndexPath*)indexPath
  2. {
  3. staticNSString*CellIdentifier=@"Cell";
  4. UITableViewCell*cell =[tableView dequeueReusableCellWithIdentifier:CellIdentifier];
  5. NSLog(@"the cell is %@", cell);
  6. cell.textLabel.text =[list objectAtIndex:indexPath.row];
  7. return cell;
  8. }

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

  1. 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,現在這個調試窗口應該會打印出來:

  1. Problems[7880:f803] the cell is<UITableViewCell:0x6a6d120; frame =(00;32044); text ='Title'; layer =<CALayer:0x6a6d240>>
  2. Problems[7880:f803] the cell is<UITableViewCell:0x6877620; frame =(00;32044); text ='Title'; layer =<CALayer:0x6867140>>
  3. Problems[7880:f803] the cell is<UITableViewCell:0x6da1e80; frame =(00;32044); text ='Title'; layer =<CALayer:0x6d9fae0>>
  4. Problems[7880:f803] the cell is<UITableViewCell:0x6878c40; frame =(00;32044); text ='Title'; layer =<CALayer:0x6878f60>>
  5. Problems[7880:f803] the cell is<UITableViewCell:0x6da10c0; frame =(00;32044); text ='Title'; layer =<CALayer:0x6d9f240>>
  6. 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()標記改變一下:

  1. -(UITableViewCell*)tableView:(UITableView*)tableView cellForRowAtIndexPath:(NSIndexPath*)indexPath
  2. {
  3. staticNSString*CellIdentifier=@"Cell";
  4. UITableViewCell*cell =[tableView dequeueReusableCellWithIdentifier:CellIdentifier];
  5. cell.textLabel.text =[list objectAtIndex:indexPath.row];
  6. NSLog(@"the text is %@",[list objectAtIndex:indexPath.row]);
  7. return cell;
  8. }

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

  1. Problems[7914:f803] the text is(null)
  2. Problems[7914:f803] the text is(null)
  3. Problems[7914:f803] the text is(null)
  4. Problems[7914:f803] the text is(null)
  5. Problems[7914:f803] the text is(null)
  6. Problems[7914:f803] the text is(null)

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

  1. [list addObject:@"One"];
  2. [list addObject:@"Two"];
  3. [list addObject:@"Three"];
  4. [list addObject:@"Four"];
  5. [list addObject:@"Five"];

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

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

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

  1. Problems[7942:f803] array contents:(null)
  2. Problems[7942:f803] array contents:(null)
  3. Problems[7942:f803] array contents:(null)
  4. Problems[7942:f803] array contents:(null)
  5. Problems[7942:f803] array contents:(null)
  6. Problems[7942:f803] array contents:(null)

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

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

  1. -(id)initWithStyle:(UITableViewStyle)style
  2. {
  3. if(self==[super initWithStyle:style])
  4. {
  5. list =[NSMutableArray arrayWithCapacity:10];
  6. [list addObject:@"One"];
  7. [list addObject:@"Two"];
  8. [list addObject:@"Three"];
  9. [list addObject:@"Four"];
  10. [list addObject:@"Five"];
  11. }
  12. returnself;
  13. }

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

  1. Problems[7971:f803] array contents:(null)
  2. ...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::

  1. -(id)initWithCoder:(NSCoder*)aDecoder
  2. {
  3. if(self==[super initWithCoder:aDecoder])
  4. {
  5. list =[NSMutableArray arrayWithCapacity:10];
  6. [list addObject:@"One"];
  7. [list addObject:@"Two"];
  8. [list addObject:@"Three"];
  9. [list addObject:@"Four"];
  10. [list addObject:@"Five"];
  11. }
  12. returnself;
  13. }

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

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仍然崩潰,但是現在你將會得到下面的錯誤消息:

  1. 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”錯誤並且終止運行。

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

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

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

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

  1. -[__NSArrayM objectAtIndex:]

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

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

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

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

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

  1. (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:,就是下面這樣:

  1. list =[NSMutableArray arrayWithCapacity:10];

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

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

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

  1. -(void)dealloc
  2. {
  3. [list release];
  4. [super dealloc];
  5. }

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

  1. Problems[8266:f803] array contents:(
  2. One,
  3. Two,
  4. Three,
  5. Four,
  6. Five
  7. )

由上面信息可以知道這個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:這個函數的斷點處。但是在調試窗口卻顯示:

  1. Problems[12540:f803] array contents:(
  2. One,
  3. Two,
  4. Three,
  5. Four,
  6. Five
  7. )

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

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

  1. (lldb) po indexPath

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

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

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

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

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

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

  1. (lldb) po indexPath
  2. (NSIndexPath*) $11 =0x06a8a6c0<NSIndexPath0x6a8a6c0>2 indexes [0,5]

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

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

  1. -(NSInteger)tableView:(UITableView*)tableView numberOfRowsInSection:(NSInteger)section
  2. {
  3. return6;
  4. }

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

  1. -(NSInteger)tableView:(UITableView*)tableView numberOfRowsInSection:(NSInteger)section
  2. {
  3. return[list count];
  4. }

刪除斷點或者使斷點無效,然後再次運行這個程序。終於這個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.

錯誤消息是:

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

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

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

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

  1. -(void)tableView:(UITableView*)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath*)indexPath
  2. {
  3. if(editingStyle ==UITableViewCellEditingStyleDelete)
  4. {
  5. [list removeObjectAtIndex:indexPath.row];
  6. [tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
  7. }
  8. }

太好了,看起來這樣做起效了,你終於有一個不會崩潰的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.

調試愉快吧!

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