有這樣一種情形:當我們正在快樂的致力於我們的app時,並且什麼看都是無比順利,但是突然,坑爹啊,它崩潰了。(悲傷地音樂響起)
我們需要做的第一件事就是:不要驚慌。
修復崩潰不是很困難的。假如你崩潰了,並且胡亂的改些東西,而且還在不停的念着咒語希望bug神奇的自動消失,你大多數情況下都會使情況更麻煩。相反的,你需要知道一些系統的方法,並且學習怎麼找到崩潰和他的原因。
第一件需要知道的就是在你的代碼中準確的找到crash發生的地方:在那個文件,那一行。Xcode debugger將會幫助你,但是你需要懂得怎麼樣最好的使用它,這也是這篇教程展示給你的。
這篇教程對於所有的開發者都是有利的。即使你是一個很有經驗的ios開發者,你也可能會從中學習到一些你不知道的小竅門。
準備開始
下載這個例子程序。你將會看到這是一個有bug的程序。當你打開這個項目的時候,xcode會顯示至少8個編譯警告,這個通常都是危險的信號。順便說一下,我們使用xcode4.3來做這篇教程,4.2的版本也應該沒有什麼問題。
注意:爲了跟隨這篇教程,這個編譯生成的app需要運行在ios5的模擬器上面。假如你運行這個app到你的設備上,你也會崩潰,但是他們可能不會發生和教程一樣的情況。
在模擬器上面運行你的app,你將會看到發生了什麼。
嘿,他崩潰了。
有兩種最基本的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 0xf9d60x108a6 0x1f743
- 0x201f8 0x13aa9 0x12a4fa9 0x138e1c5 0x12f3022 0x12f190a 0x12f0db40x12f0ccb 0x102a7
- 0x11a9b 0x2792 0x2705)
- terminate called throwing an exception
瞭解這些錯誤消息是非常重要的,因爲他們包含了錯誤在那裏的重要線索,一下就是需要關注的部分:
- [UINavigationController setList:]: unrecognized selector sent to instance0x6a33840
“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也可能會在其他的線程裏面崩潰。
當前xocde就高亮顯示了main.m裏面的main()函數。但是那些東西並沒有告訴你很多,所以你需要繼續的向深層次的挖掘。
爲了看到堆棧的更多信息,拖拽Debug Navigator底部的滑塊到最右邊。它將會展示出崩潰時全部的堆棧信息:
這個列表裏面的每一項都是一個來這個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。
所有在這個堆棧裏面的函數和方法都是灰色的,除了main()函數。那是因爲他們都來自內置的ios frameworks(ios內置框架)。所以沒有針對他們可見的源碼。
在這個堆棧裏面唯一的東西就是你有main.m的源碼,因此xcode的代碼編輯器就顯示了它,即使他不是這個崩潰的真正原因。但是這個經常混淆初學者,但是馬上我將展示怎麼樣來弄懂它。
開個玩笑,點擊這個堆棧裏面的任意一項,你將會看到許多的彙編代碼,這些你可能完全不理解:
加入我們得到那樣的源碼,我想很多人都會說:坑爹啊。
異常斷點
你怎麼樣找到是代碼裏面的哪一行使app崩潰的?無論什麼時候,你得到的一個想這樣的堆棧路徑,一個異常通過這個app拋出。(你多半會說因爲堆棧裏面有一個函數叫objc_exception_rethrow。)
當程序由於做了一些他不能完成的事情時,一個異常就會發生。你所看到的就是這個異常的結果:app做了一些錯的事情,異常被拋出,xcode展示異常的結果。理想情況下,你想要的準確的看到異常在那裏拋出的。
幸運的是,通過使用Exception Breakpoint(異常斷點),你可以告訴xcode在一個特定的時候暫停這個程序。斷點是一個在特定時刻暫停你的程序的調試工具。你將會第二篇教程裏面看到更多關於他們的信息,但是現在你將會使用一個特殊的斷點,它將會在拋出異常前暫停你的程序。
爲了設置異常斷點,我們不得不切換到Breakpoint Navigator(斷點導航器):
在底部有一個小的加號(“+”)按鈕。點擊它,並且選擇Add Exception Breakpoint:
一個新的斷點將會被增加到這個列表裏:
點擊Done按鈕使彈出的窗口消失。注意在xcode工具欄上面Breakpoints button(斷點按鈕)是有效的。加入你不想要帶着任何斷點運行你的app,你可以簡單的開關這個按鈕到off。但是現在,讓它打開,並且再一次運行這個app。
太好了!代碼編輯器現在停止並且指到了代碼中的其中一行,不再在令人煩躁的彙編代碼了,並且注意在在左邊的的Debug Navigatot(調試導航器)裏面顯示的堆棧信息也不一樣了。
顯然的,問題就出在AppDelegate裏面的application:didFinishLaunchingWithOptions:方法裏:
- viewController.list = [NSArray arrayWithObjects:@"One", @"Two"];
仔細再次看看這個錯誤消息:
- [UINavigationController setList:]: unrecognized selector sent to instance0x6d4ed20
在這個代碼裏面,“viewController.list = something”這種方式隱式的調用了setList:方法,也就是set方法,因爲“list”是MainViewController類的一個屬性。然而,通過這個錯誤消息,我們知道viewController這個變量沒有指向MainViewController對象,而是指向了UINavigationController,所以顯然的,UINavigationController沒有“list”屬性!所以這些變量在這裏混淆了。
打開Storyboard文件,看看window的rootViewController屬性實際上是指向那個的:
哈哈!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”錯誤,你就需要檢查這個對象是不是正確類型,並且檢查它真的是有那個名字的方法麼。你會經常發現你調用一個你認爲是這個對象的方法,因爲指針變量可能沒有包含這個正確值,所以導致很多的錯誤。
另外一個經常出現錯誤的原因就是將方法名稱拼寫錯誤。一會兒你將會看到一個這樣的例子。(譯者:我個人認爲有xcode的代碼提示功能,這種錯誤應該還是比較少吧,多數應該出現在通過selector,或者傳遞函數指針的時候,應該會多點這個錯誤)。
你的第一個內存錯誤
你可能已經修復了你的第一個問題。再一次運行這個程序。坑爹啊,在同樣的一行,又崩潰了,但是現在是EXC_BAD_ACCESS錯誤。那意味着這個app有內存管理的問題。
一個和內存相關的崩潰一般很難定位到源代碼,因爲這個惡魔可能很早就在程序中做了壞事了。假如一段有問題的代碼混亂了內存結構,這樣產生的蝴蝶效應可能會在之後很久才表現出來,並且總在不同的地方。
實際上,在你所有的測試中,這個bug可能永遠不會出現,但是卻在你的客戶的設備上展露出它醜陋的腦袋。這種是很多人都不想的。
這種特別的崩潰但是卻很容易修復。假如你看到你的代碼編輯器,xcode其實一直就在警告你這一行代碼。看到左邊靠近行號的那個黃色三角形沒有?那個指出一個編譯警告。假如你點擊那個黃色的三角形,xcode將會彈出一個“Fix-it”的建議,就像下面的一樣:
這個代碼使用了一系列的對象來初始化一個數組(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():
假如你從上到下瀏覽一下這些方法的名字,有些問題發生在NSObject和Key-Value Coding。在那之下調用了[UIRuntimeOutletConnection connect]。我不知道那個是幹什麼的,但是看起來好像它做了連接outlet的一些事情。在那之下的一些方法是從nib中加載view。因此以上那些也給你一些線索。
但是,在xcode的調試窗口,並沒有易懂的錯誤消息。那是因爲沒有異常被拋出。在xcode告訴你異常的原因之前,Exception Breakpoint已經暫停了這個程序。有些時候你會從Exception Breakpoint得到一些局部的錯誤消息,但是有些時候就得不到。
爲了得到全部的錯誤消息,點擊調試器工具欄上的“Continue Program Execution”按鈕:
你可能需要點擊好幾次纔可以,然後你將會得到錯誤消息:
- 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 0x922eeb0x93dd60 0x23091a 0x13bbe1a
- 0x1325821 0x22f46e 0xd6e2c 0xd73a9 0xd75cb 0xd6c1c 0xfd56d 0xe7d470xfe441 0xfe45d
- 0xfe4f9 0x3ed65 0x3edac 0xfbe6 0x108a6 0x1f743 0x201f8 0x13aa90x12a4fa9 0x138e1c5
- 0x12f3022 0x12f190a 0x12f0db4 0x12f0ccb 0x102a7 0x11a9b 0x28720x27e5)
- 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:
在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正常工作,或者至少說啓動的時候沒有問題。是時候來點擊這個按鈕了。
哇!這個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裏面看看,在這個按鈕的連接檢測器:
第一,斷開Touch Up Inside 事件(點擊這個小“X”),然後再次連接它到MainViewController裏,但是這次選擇這個buttonTapped:方法。注意在連接檢查器裏面看看這個方法後面是有一個冒號的。
運行這個app,再一次點擊按鈕。我們又得到了這個“unrecognized selector”消息,但是這次他正確的定位到了buttonTapped:方法裏面。
- Problems[6675:f803] -[MainViewController buttonTapped:]: unrecognized selector sent
- to instance 0x6b6c7f0
假如你仔細看的話,編譯器警告應該又給你指出解決方案。Xcode提出MainViewController的實現是不完整的。特別的,buttonTapped:方法沒有被發現。
是時候看看MainViewController.m了,在這裏確實是有buttonTapped:方法啊……………..等等,拼寫錯誤了:
- - (void)butonTapped:(id)sender
很簡單的修改,重命名這個方法:
- - (void)buttonTapped:(id)sender
提示:你沒必要聲明這個方法爲IBAction,假如你覺得這樣是很優雅的,你可以這樣做。
注意:假如你仔細注意到這些編譯器警告的話,這些問題很容易看出來的。就個人而言,我把所有的警告當成嚴重的錯誤(在xcode裏面的編譯設置(Build Settings)裏面可以設置警告作爲錯誤提示的),在運行程序以前,我會修改所有的。Xcode在指出愚蠢的錯誤表現的相當好,就像這裏這樣,並且注意到這些提示是很明智的。
Messing with Memory(混亂內存)
經過了這麼多,你知道崩潰一直在繼續從未停止過。運行這個app,點擊按鈕,然後等待崩潰。好,現在就來了:
這裏是另一種EXC_BAD_ACCESS崩潰。幸運的是,xcode已經準確給你指示出位置在那裏了,在這個buttonTapped:方法裏面:
- NSLog("You tapped on: %s", sender);
有些時候,你可能在上面花費一些時間纔會反應過來,但是xcode提供了幫助,僅僅需要點擊這個黃色的三角形來看這個錯誤是什麼:
NSLog呈現一個Objective-c類型的字符串,而不是一個c字符串,因此插入一個@符號來修改它:
- NSLog(@"You tapped on: %s", sender);
你將會注意到這個警告的黃色三角形依然沒有消失。這是因爲在這行還有另外一個bug,這個bug可能會或者可能不會使你的程序崩潰。有些時候這個代碼工作很好,或者現在看起來很好,但是有些時候他就會崩潰。(特別是有些時候只在你的客戶的設備上面,絕不會在你的設備上)。
讓我們來看看這個新的警告:
這個“%s”專門爲c語言類型的字符串。一個c類型的字符串就是把這個內存分成片段(一個老式的字節數組),通過一個所謂的”NUL character”(其實就是一個爲0的值)來終止。例如這個“Crash!”看起來就是這樣的:
無論是什麼時候,你使用一個函數或者方法來操作這個c類型的字符串,你不得不確定這個字符串是以一個0值來結尾的,否則這個函數將不知道這個字符串已經結束了。
現在來看看,當你指定了在NSlog()中用“%s”來格式化字符串,或者在NSString 的stringWithFormat裏面,這個變量將會被當做是一個c類型的字符串。假如這個“sender”指向一個包含0字節的內存,這個NSlog()將不會崩潰,但是輸出的東西就會像這樣:
- You tapped on: xËj
再一次運行這個app,點擊這個按鈕,等待它崩潰。現在在Debug窗口的左邊部分,右擊“sender”,並且選擇“view Memory of “*sender””選項(確保是選擇的是帶有星號的sender)。
Xcode將會展示出這個內存地址的內容,恰恰這個就是NSlog()打印出來的內容。
然而,這裏並不能保證這裏有空位(結束標誌位),所以你完全很容易執行到一個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是存在的,但是你忘記了設置它的標示,一個典型的錯誤:
改變這個segue的標示爲“ModalSegue”。再一次運行這個app,等待一下,點擊這個按鈕 ,這個時候不再有crash了!但是這裏只是我們下個部分的開端——-tableview不應該是空的!
何去何從?
在第二部分的教程裏面,我們將會遇到更多的bug。並且學習到更多關於調試的工具,包括NSLog()陳述,斷點和殭屍對象。