如何讓我們淡定解決 程序運行崩潰


轉來的,我覺得很不錯,精品!

有這樣一種情形:當我們正在快樂的致力於我們的app時,並且什么看都是無比順利,但是突然,坑爹啊,它崩潰了。(悲傷地音樂響起)

我們需要做的第一件事就是:不要驚慌。

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

 

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

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

准備開始

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

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

在模擬器上面運行你的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窗口,你需要點擊這個小窗口的右上角的中間那個圖標,他靠近搜索框。在這個情況下,會展示一些下面東西:

<ol class="linenums"><li class="L0" value="1"><span class="typ">Problems</span><span class="pun">[</span><span class="lit">14465</span><span class="pun">:</span><span class="pln">f803</span><span class="pun">]</span><span class="pln"> </span><span class="pun">-[</span><span class="typ">UINavigationController</span><span class="pln"> setList</span><span class="pun">:]:</span><span class="pln"> unrecognized selector sent to</span></li><li class="L1"><span class="pln">instance </span><span class="lit">0x6a33840</span></li><li class="L2"><span class="typ">Problems</span><span class="pun">[</span><span class="lit">14465</span><span class="pun">:</span><span class="pln">f803</span><span class="pun">]</span><span class="pln"> </span><span class="pun">***</span><span class="pln"> </span><span class="typ">Terminating</span><span class="pln"> app due to uncaught exception </span><span class="str">'NSInvalidArgumentException'</span><span class="pun">,</span></li><li class="L3"><span class="pln">reason</span><span class="pun">:</span><span class="pln"> </span><span class="str">'-[UINavigationController setList:]: unrecognized selector sent to instance 0x6a33840'</span></li><li class="L4"><span class="pun">***</span><span class="pln"> </span><span class="typ">First</span><span class="pln"> </span><span class="kwd">throw</span><span class="pln"> call stack</span><span class="pun">:</span></li><li class="L5"><span class="pun">(</span><span class="lit">0x13ba052</span><span class="pln"> </span><span class="lit">0x154bd0a</span><span class="pln"> </span><span class="lit">0x13bbced</span><span class="pln"> </span><span class="lit">0x1320f00</span><span class="pln"> </span><span class="lit">0x1320ce2</span><span class="pln"> </span><span class="lit">0x29ef</span><span class="pln"> </span><span class="lit">0xf9d6</span><span class="pln"> </span><span class="lit">0x108a6</span><span class="pln"> </span><span class="lit">0x1f743</span></li><li class="L6"><span class="lit">0x201f8</span><span class="pln"> </span><span class="lit">0x13aa9</span><span class="pln"> </span><span class="lit">0x12a4fa9</span><span class="pln"> </span><span class="lit">0x138e1c5</span><span class="pln"> </span><span class="lit">0x12f3022</span><span class="pln"> </span><span class="lit">0x12f190a</span><span class="pln"> </span><span class="lit">0x12f0db4</span><span class="pln"> </span><span class="lit">0x12f0ccb</span><span class="pln"> </span><span class="lit">0x102a7</span></li><li class="L7"><span class="lit">0x11a9b</span><span class="pln"> </span><span class="lit">0x2792</span><span class="pln"> </span><span class="lit">0x2705</span><span class="pun">)</span></li><li class="L8"><span class="pln">terminate called throwing an exception</span></li></ol>

了解這些錯誤消息是非常重要的,因為他們包含了錯誤在那里的重要線索,一下就是需要關注的部分:

<ol class="linenums"><li class="L0" value="1"><span class="pun">[</span><span class="typ">UINavigationController</span><span class="pln"> setList</span><span class="pun">:]:</span><span class="pln"> unrecognized selector sent to instance </span><span class="lit">0x6a33840</span></li></ol>

“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:方法里:

<ol class="linenums"><li class="L0" value="1"><span class="pln">viewController</span><span class="pun">.</span><span class="pln">list </span><span class="pun">=</span><span class="pln"> </span><span class="pun">[</span><span class="typ">NSArray</span><span class="pln"> arrayWithObjects</span><span class="pun">:@</span><span class="str">"One"</span><span class="pun">,</span><span class="pln"> </span><span class="pun">@</span><span class="str">"Two"</span><span class="pun">];</span></li></ol>

仔細再次看看這個錯誤消息:

<ol class="linenums"><li class="L0" value="1"><span class="pun">[</span><span class="typ">UINavigationController</span><span class="pln"> setList</span><span class="pun">:]:</span><span class="pln"> unrecognized selector sent to instance </span><span class="lit">0x6d4ed20</span></li></ol>

在這個代碼里面,“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:里面的:

<ol class="linenums"><li class="L0" value="1"><span class="pun">-</span><span class="pln"> </span><span class="pun">(</span><span class="pln">BOOL</span><span class="pun">)</span><span class="pln">application</span><span class="pun">:(</span><span class="typ">UIApplication</span><span class="pln"> </span><span class="pun">*)</span><span class="pln">application didFinishLaunchingWithOptions</span><span class="pun">:(</span><span class="typ">NSDictionary</span><span class="pln"> </span><span class="pun">*)</span><span class="pln">launchOptions</span></li><li class="L1"><span class="pun">{</span></li><li class="L2"><span class="pln">	</span><span class="typ">UINavigationController</span><span class="pln"> </span><span class="pun">*</span><span class="pln">navController </span><span class="pun">=</span><span class="pln"> </span><span class="pun">(</span><span class="typ">UINavigationController</span><span class="pln"> </span><span class="pun">*)</span><span class="kwd">self</span><span class="pun">.</span><span class="pln">window</span><span class="pun">.</span><span class="pln">rootViewController</span><span class="pun">;</span></li><li class="L3"><span class="pln">	</span><span class="typ">MainViewController</span><span class="pln"> </span><span class="pun">*</span><span class="pln">viewController </span><span class="pun">=</span><span class="pln"> </span><span class="pun">(</span><span class="typ">MainViewController</span><span class="pln"> </span><span class="pun">*)</span><span class="pln">navController</span><span class="pun">.</span><span class="pln">topViewController</span><span class="pun">;</span></li><li class="L4"><span class="pln">	viewController</span><span class="pun">.</span><span class="pln">list </span><span class="pun">=</span><span class="pln"> </span><span class="pun">[</span><span class="typ">NSArray</span><span class="pln"> arrayWithObjects</span><span class="pun">:@</span><span class="str">"One"</span><span class="pun">,</span><span class="pln"> </span><span class="pun">@</span><span class="str">"Two"</span><span class="pun">];</span></li><li class="L5"><span class="pln">	</span><span class="kwd">return</span><span class="pln"> YES</span><span class="pun">;</span></li><li class="L6"><span class="pun">}</span></li></ol>

通過代碼可以看出,首先你通過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”):

<ol class="linenums"><li class="L0" value="1"><span class="pln">viewController</span><span class="pun">.</span><span class="pln">list </span><span class="pun">=</span><span class="pln"> </span><span class="pun">[</span><span class="typ">NSArray</span><span class="pln"> arrayWithObjects</span><span class="pun">:@</span><span class="str">"One"</span><span class="pun">,</span><span class="pln"> </span><span class="pun">@</span><span class="str">"Two"</span><span class="pun">,</span><span class="pln"> </span><span class="kwd">nil</span><span class="pun">];</span></li></ol>

“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.

你可能需要點擊好幾次才可以,然后你將會得到錯誤消息:

<ol class="linenums"><li class="L0" value="1"><span class="typ">Problems</span><span class="pun">[</span><span class="lit">14961</span><span class="pun">:</span><span class="pln">f803</span><span class="pun">]</span><span class="pln"> </span><span class="pun">***</span><span class="pln"> </span><span class="typ">Terminating</span><span class="pln"> app due to uncaught exception </span><span class="str">'NSUnknownKeyException'</span><span class="pun">,</span></li><li class="L1"><span class="pln">reason</span><span class="pun">:</span><span class="pln"> </span><span class="str">'[ setValue:forUndefinedKey:]: this class is not</span></li><li class="L2"><span class="str">key value coding-compliant for the key button.'</span></li><li class="L3"><span class="pun">***</span><span class="pln"> </span><span class="typ">First</span><span class="pln"> </span><span class="kwd">throw</span><span class="pln"> call stack</span><span class="pun">:</span></li><li class="L4"><span class="pun">(</span><span class="lit">0x13ba052</span><span class="pln"> </span><span class="lit">0x154bd0a</span><span class="pln"> </span><span class="lit">0x13b9f11</span><span class="pln"> </span><span class="lit">0x9b1032</span><span class="pln"> </span><span class="lit">0x922f7b</span><span class="pln"> </span><span class="lit">0x922eeb</span><span class="pln"> </span><span class="lit">0x93dd60</span><span class="pln"> </span><span class="lit">0x23091a</span><span class="pln"> </span><span class="lit">0x13bbe1a</span></li><li class="L5"><span class="lit">0x1325821</span><span class="pln"> </span><span class="lit">0x22f46e</span><span class="pln"> </span><span class="lit">0xd6e2c</span><span class="pln"> </span><span class="lit">0xd73a9</span><span class="pln"> </span><span class="lit">0xd75cb</span><span class="pln"> </span><span class="lit">0xd6c1c</span><span class="pln"> </span><span class="lit">0xfd56d</span><span class="pln"> </span><span class="lit">0xe7d47</span><span class="pln"> </span><span class="lit">0xfe441</span><span class="pln"> </span><span class="lit">0xfe45d</span></li><li class="L6"><span class="lit">0xfe4f9</span><span class="pln"> </span><span class="lit">0x3ed65</span><span class="pln"> </span><span class="lit">0x3edac</span><span class="pln"> </span><span class="lit">0xfbe6</span><span class="pln"> </span><span class="lit">0x108a6</span><span class="pln"> </span><span class="lit">0x1f743</span><span class="pln"> </span><span class="lit">0x201f8</span><span class="pln"> </span><span class="lit">0x13aa9</span><span class="pln"> </span><span class="lit">0x12a4fa9</span><span class="pln"> </span><span class="lit">0x138e1c5</span></li><li class="L7"><span class="lit">0x12f3022</span><span class="pln"> </span><span class="lit">0x12f190a</span><span class="pln"> </span><span class="lit">0x12f0db4</span><span class="pln"> </span><span class="lit">0x12f0ccb</span><span class="pln"> </span><span class="lit">0x102a7</span><span class="pln"> </span><span class="lit">0x11a9b</span><span class="pln"> </span><span class="lit">0x2872</span><span class="pln"> </span><span class="lit">0x27e5</span><span class="pun">)</span></li><li class="L8"><span class="pln">terminate called throwing an exception</span></li></ol>

就像之前的一樣,你可以忽略下面的那些數字。他們展示了調用堆棧,但是在調試導航器的左邊有更加直觀的堆棧調用展示。

有趣的部分是:

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:

<ol class="linenums"><li class="L0" value="1"><span class="lit">@interface</span><span class="pln"> </span><span class="typ">MainViewController</span><span class="pln"> </span><span class="pun">:</span><span class="pln"> </span><span class="typ">UIViewController</span></li><li class="L1"><span class="pln"> </span></li><li class="L2"><span class="lit">@property</span><span class="pln"> </span><span class="pun">(</span><span class="pln">nonatomic</span><span class="pun">,</span><span class="pln"> retain</span><span class="pun">)</span><span class="pln"> </span><span class="typ">NSArray</span><span class="pln"> </span><span class="pun">*</span><span class="pln">list</span><span class="pun">;</span></li><li class="L3"><span class="lit">@property</span><span class="pln"> </span><span class="pun">(</span><span class="pln">nonatomic</span><span class="pun">,</span><span class="pln"> retain</span><span class="pun">)</span><span class="pln"> </span><span class="typ">IBOutlet</span><span class="pln"> </span><span class="typ">UIButton</span><span class="pln"> </span><span class="pun">*</span><span class="pln">button</span><span class="pun">;</span></li><li class="L4"><span class="pln"> </span></li><li class="L5"><span class="pun">-</span><span class="pln"> </span><span class="pun">(</span><span class="typ">IBAction</span><span class="pun">)</span><span class="pln">buttonTapped</span><span class="pun">:(</span><span class="pln">id</span><span class="pun">)</span><span class="pln">sender</span><span class="pun">;</span></li><li class="L6"><span class="pln"> </span></li><li class="L7"><span class="lit">@end</span></li></ol>

這里是為這個“button”定義了外部連接屬性的(@property),因此這個問題是什么呢?假如你仔細觀察了編譯警告的話,你可以已經知道是什么地方的問題了。

假如還不知道的話,檢查一下MainViewController.m的@synthesize的內容的話。你現在看出問題沒有啊?

這個代碼其實沒有@synthesize這個button的屬性。它(@synthesize)其實是告訴MainVIewController他自己有個“button”的屬性,提供一個后台實例變量,並且提供getter和setter方法(這就是@synthesize所做的)。

把下面的增加到MainViewController.m里面已經存在的@synthesize行的下面來修復這個問題:

<ol class="linenums"><li class="L0" value="1"><span class="lit">@synthesize</span><span class="pln"> button </span><span class="pun">=</span><span class="pln"> _button</span><span class="pun">;</span></li></ol>

現在這個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。在調試窗口打印出的錯誤消息是:

<ol class="linenums"><li class="L0" value="1"><span class="typ">Problems</span><span class="pun">[</span><span class="lit">6579</span><span class="pun">:</span><span class="pln">f803</span><span class="pun">]</span><span class="pln"> </span><span class="pun">-[</span><span class="typ">MainViewController</span><span class="pln"> buttonTapped</span><span class="pun">]:</span><span class="pln"> unrecognized selector sent</span></li><li class="L1"><span class="pln">to instance </span><span class="lit">0x6e44850</span></li></ol>

堆棧跟蹤也不是很有啟發。只是列出了一些和一個方法相關的或者發送了事件並且執行了動作的方法,但是你已經知道到了被涉及的動作了。畢竟,你點擊了一個按鈕,這個按鈕的IBAction方法應該被調用。

當然你之前應該已經看過了這個錯誤消息。一個被調用的方法不存在。這個時候目標對象應該是MainViewController,由於動作方法經常存在於一個包含按鈕的view controller里面,所以這個看起來是正確。並且你看MainViewController.h文件,這個IBAction方法確實在里面:

<ol class="linenums"><li class="L0" value="1"><span class="pun">-</span><span class="pln"> </span><span class="pun">(</span><span class="typ">IBAction</span><span class="pun">)</span><span class="pln">buttonTapped</span><span class="pun">:(</span><span class="pln">id</span><span class="pun">)</span><span class="pln">sender</span><span class="pun">;</span></li></ol>

是這樣的嗎?錯誤消息顯示這個方法的名字是buttonTapped,但是MainViewController的方法卻是buttonTapped:(注意冒號),由於這個方法需要接受一個參數(名字是sender),所以在方法名字后面有個冒號。從這個錯誤消息看出,這個方法沒有冒號,因此不需要參數。所以這個方法看其實應該是這樣的:

<ol class="linenums"><li class="L0" value="1"><span class="pun">-</span><span class="pln"> </span><span class="pun">(</span><span class="typ">IBAction</span><span class="pun">)</span><span class="pln">buttonTapped</span><span class="pun">;</span></li></ol>

這個里發生了什么?這個方法最初的時候不需要參數(有些時候這樣動作方法是被允許的),並且在那個時候,他為這個按鈕在storyboard里面連接了Touch Up Inside的時間方法。然而,在那之后某個時候,這個方法的形式被修改為包含了一個“sender”參數,但是,卻沒有去更新storyboard。

你可以在storyboard里面看看,在這個按鈕的連接檢測器:

The button's connections in the storyboard.

第一,斷開Touch Up Inside 事件(點擊這個小“X”),然后再次連接它到MainViewController里,但是這次選擇這個buttonTapped:方法。注意在連接檢查器里面看看這個方法后面是有一個冒號的。

運行這個app,再一次點擊按鈕。我們又得到了這個“unrecognized selector”消息,但是這次他正確的定位到了buttonTapped:方法里面。

<ol class="linenums"><li class="L0" value="1"><span class="typ">Problems</span><span class="pun">[</span><span class="lit">6675</span><span class="pun">:</span><span class="pln">f803</span><span class="pun">]</span><span class="pln"> </span><span class="pun">-[</span><span class="typ">MainViewController</span><span class="pln"> buttonTapped</span><span class="pun">:]:</span><span class="pln"> unrecognized selector sent</span></li><li class="L1"><span class="pln">to instance </span><span class="lit">0x6b6c7f0</span></li></ol>

假如你仔細看的話,編譯器警告應該又給你指出解決方案。Xcode提出MainViewController的實現是不完整的。特別的,buttonTapped:方法沒有被發現。

Xcode shows an incomplete implementation warning.

是時候看看MainViewController.m了,在這里確實是有buttonTapped:方法啊……………..等等,拼寫錯誤了:

<ol class="linenums"><li class="L0" value="1"><span class="pun">-</span><span class="pln"> </span><span class="pun">(</span><span class="kwd">void</span><span class="pun">)</span><span class="pln">butonTapped</span><span class="pun">:(</span><span class="pln">id</span><span class="pun">)</span><span class="pln">sender</span></li></ol>

很簡單的修改,重命名這個方法:

<ol class="linenums"><li class="L0" value="1"><span class="pun">-</span><span class="pln"> </span><span class="pun">(</span><span class="kwd">void</span><span class="pun">)</span><span class="pln">buttonTapped</span><span class="pun">:(</span><span class="pln">id</span><span class="pun">)</span><span class="pln">sender</span></li></ol>

提示:你沒必要聲明這個方法為IBAction,假如你覺得這樣是很優雅的,你可以這樣做。

注意:假如你仔細注意到這些編譯器警告的話,這些問題很容易看出來的。就個人而言,我把所有的警告當成嚴重的錯誤(在xcode里面的編譯設置(Build Settings)里面可以設置警告作為錯誤提示的),在運行程序以前,我會修改所有的。Xcode在指出愚蠢的錯誤表現的相當好,就像這里這樣,並且注意到這些提示是很明智的。

Messing with Memory(混亂內存)

經過了這么多,你知道崩潰一直在繼續從未停止過。運行這個app,點擊按鈕,然后等待崩潰。好,現在就來了:

The app crashes on NSLog().

這里是另一種EXC_BAD_ACCESS崩潰。幸運的是,xcode已經准確給你指示出位置在那里了,在這個buttonTapped:方法里面:

<ol class="linenums"><li class="L0" value="1"><span class="typ">NSLog</span><span class="pun">(</span><span class="str">"You tapped on: %s"</span><span class="pun">,</span><span class="pln"> sender</span><span class="pun">);</span></li></ol>

有些時候,你可能在上面花費一些時間才會反應過來,但是xcode提供了幫助,僅僅需要點擊這個黃色的三角形來看這個錯誤是什么:

Objective-C strings must have a @ prefix.

NSLog呈現一個Objective-c類型的字符串,而不是一個c字符串,因此插入一個@符號來修改它:

<ol class="linenums"><li class="L0" value="1"><span class="typ">NSLog</span><span class="pun">(@</span><span class="str">"You tapped on: %s"</span><span class="pun">,</span><span class="pln"> sender</span><span class="pun">);</span></li></ol>

你將會注意到這個警告的黃色三角形依然沒有消失。這是因為在這行還有另外一個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()將不會崩潰,但是輸出的東西就會像這樣:

<ol class="linenums"><li class="L0" value="1"><span class="typ">You</span><span class="pln"> tapped on</span><span class="pun">:</span><span class="pln"> x</span><span class="pun">Ë</span><span class="pln">j</span></li></ol>

再一次運行這個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()的形式,就像下面的一樣:

<ol class="linenums"><li class="L0" value="1"><span class="typ">NSLog</span><span class="pun">(@</span><span class="str">"You tapped on: %@"</span><span class="pun">,</span><span class="pln"> sender</span><span class="pun">);</span></li></ol>

運行這個app,並且再一次點擊這個按鈕。現在這個NSLog()做了他能做的了,並且看起來好像不會崩潰在buttonTapped:這個函數里面了。

和調試器交朋友(Making Friends With the Debugger)

看看這最近的崩潰,xcode指示到了這一行:

<ol class="linenums"><li class="L0" value="1"><span class="pun">[</span><span class="kwd">self</span><span class="pln"> performSegueWithIdentifier</span><span class="pun">:@</span><span class="str">"ModalSegue"</span><span class="pln"> sender</span><span class="pun">:</span><span class="pln">sender</span><span class="pun">];</span></li></ol>

在Debug窗口里面沒有消息打印出來。你可以點擊這個繼續執行這個程序的按鈕,就像前面一樣,但是你也可以在調試器里面輸入一個命令來得到這個錯誤消息。這樣做的好處就是,這個app可以保持暫停在這個同樣的地方。

假如你准備在模擬器里面運行這個,你可以在“(lldb)”提示的后面輸入下面的:

<ol class="linenums"><li class="L0" value="1"><span class="pun">(</span><span class="pln">lldb</span><span class="pun">)</span><span class="pln"> po $eax</span></li></ol>

LLDB在xcode4.3或者之后的版本里面是默認的調試器。假如你正在使用老一點版本的xcode的話,你又GDB調試器。他們有一些基本的相同的命令,因此假如你的xcode使用的是“(gdb)”提示,而不是“(lldb)”提示的話,你也能夠更隨一起做,而沒有問題。

“po”命令是“print object”(打印對象)的簡寫。“$eax”是cup的一個寄存器。在一個異常的情況下,這個寄存器將會包含一個異常對象的指針。注意:$eax只會在模擬器里面工作,假如你在設備上調試,你將需要使用”$r0″寄存器。

例如,假如你輸入:

<ol class="linenums"><li class="L0" value="1"><span class="pun">(</span><span class="pln">lldb</span><span class="pun">)</span><span class="pln"> po </span><span class="pun">[</span><span class="pln">$eax </span><span class="kwd">class</span><span class="pun">]</span></li></ol>

你將會看像這樣的東西:

<ol class="linenums"><li class="L0" value="1"><span class="pun">(</span><span class="pln">id</span><span class="pun">)</span><span class="pln"> $2 </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x01446e84</span><span class="pln"> </span><span class="typ">NSException</span></li></ol>

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

你可以對這個對象調用任何方法。例如:

<ol class="linenums"><li class="L0" value="1"><span class="pun">(</span><span class="pln">lldb</span><span class="pun">)</span><span class="pln"> po </span><span class="pun">[</span><span class="pln">$eax name</span><span class="pun">]</span></li></ol>

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

<ol class="linenums"><li class="L0" value="1"><span class="pun">(</span><span class="pln">lldb</span><span class="pun">)</span><span class="pln"> po </span><span class="pun">[</span><span class="pln">$eax reason</span><span class="pun">]</span></li></ol>

這個將會輸出錯誤消息:

<ol class="linenums"><li class="L0" value="1"><span class="pun">(</span><span class="kwd">unsigned</span><span class="pln"> </span><span class="kwd">int</span><span class="pun">)</span><span class="pln"> $4 </span><span class="pun">=</span><span class="pln"> </span><span class="lit">114784400</span><span class="pln"> </span><span class="typ">Receiver</span><span class="pln"> </span><span class="pun">()</span><span class="pln"> has </span><span class="kwd">no</span></li><li class="L1"><span class="pln">segue </span><span class="kwd">with</span><span class="pln"> identifier </span><span class="str">'ModalSegue'</span></li></ol>

注意:當你僅僅使用了“po $eax”,這個命令將會對這個對象調用“description”方法和打印出來,在這個情況下,你也會得到錯誤的消息。

因此解釋下那是什么情況:你正在嘗試執行一個名叫“ModalSegue”的segue,但是很顯然,在MainViewController里面並沒有這樣的的segue。

Storyboard展示出來這個segue是存在的,但是你忘記了設置它的標示,一個典型的錯誤:

Giving the segue an identifier.

改變這個segue的標示為“ModalSegue”。再一次運行這個app,等待一下,點擊這個按鈕 ,這個時候不再有crash了!但是這里只是我們下個部分的開端——-tableview不應該是空的!

何去何從?

 

在第二部分的教程里面,我們將會遇到更多的bug。並且學習到更多關於調試的工具,包括NSLog()陳述,斷點和僵屍對象。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM