【翻譯】我的app crash了,怎么辦之一

這篇文章的作者是iOS Tutorial Team 的成員Matthijs Hollemans,他是一個(gè)經(jīng)驗(yàn)豐富的ios設(shè)計(jì)開發(fā)工作者,他的聯(lián)系方式:Google+Twitter
閱讀原文:原文鏈接

我們大多數(shù)開發(fā)人員經(jīng)常遇到這樣的情況:我們的應(yīng)用運(yùn)行的好好的,突然——“砰”一下子crash了,抓狂!
別慌!

假如此時(shí)很郁悶的你立即開始嘗試修改代碼,并期望如同尋找到了合適的咒語一樣使得bug神奇消失,那么很可能導(dǎo)致更糟糕的問題。但是如果掌握了系統(tǒng)的定位crash問題的方法的話,解決crash問題也不是很復(fù)雜的事情。

首要的事情,是找到代碼中crash出現(xiàn)的確切位置:哪個(gè)文件的哪行代碼。本文將全面的闡述如何利用Xcode的調(diào)試工具定位代碼的奔潰位置。

本文面向所有的開發(fā)者,從初級到高級。即便是高級開發(fā)者,也可能通過本文獲得一些調(diào)試技巧或者以前不曾涉及的調(diào)試知識。

一、準(zhǔn)備工作

下載示例程序.這是個(gè)有bug的程序。用Xcode打開,可以看到有至少八處編譯警告,通常編譯警告也是問題的前期表現(xiàn)。本文中用的Xcode4.3來做說明,但實(shí)際上在4.2的版本也是一樣的。

注: 本文的示例程序效果是在IOS5的模擬器上運(yùn)行的效果,如果直接在手機(jī)上運(yùn)行,同樣也會crash,但是crash出現(xiàn)的順序可能會有所不同

在模擬器上運(yùn)行看看發(fā)生了什么情況。

第一個(gè)crash
第一個(gè)crash

嗯,代碼crash了。:-)

通常crash分兩種:SIGABRT (或者 EXC_CRASH) 和 EXC_BAD_ACCESS (通常用 SIGBUS 或 SIGSEGV表示的crash)。

SIGABRT的Crash通常情況下好定位很多,因?yàn)樗鞘芸氐腸rash(系統(tǒng)讓app去執(zhí)行某個(gè)app本身并不支持的操作時(shí),應(yīng)用終端就會直接拋出該信號,讓程序crash)。

EXC_BAD_ACCESS的crash定位就要難很多。這種crash經(jīng)常發(fā)生在應(yīng)用進(jìn)入了一個(gè)損壞狀態(tài)時(shí),通常是由內(nèi)存管理問題導(dǎo)致。

幸運(yùn)的是,我們上面的第一個(gè)crash(目前暴露出來的)是SIGABRT。SIGABRT的crash發(fā)生時(shí),一般在Xcode的調(diào)試輸出窗口(窗口的右下角)會有錯(cuò)誤信息輸出。如果你看不到調(diào)試輸出窗口,通過View—》Debug Area-》Show Debug Area顯示調(diào)試輸出區(qū)域。此處,這個(gè)crash的錯(cuò)誤信息如下:

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 0xf9d6 0x108a6 0x1f743
0x201f8 0x13aa9 0x12a4fa9 0x138e1c5 0x12f3022 0x12f190a 0x12f0db4 0x12f0ccb 0x102a7
0x11a9b 0x2792 0x2705)
terminate called throwing an exception

學(xué)會解析這些錯(cuò)誤信息很關(guān)鍵。因?yàn)檫@些信息中往往包含了很重要的錯(cuò)誤原因描述。比如上面輸出中比較有趣的一行:

[UINavigationController setList:]: unrecognized selector sent to instance 0x6a33840

錯(cuò)誤信息“unrecognized selector sent to instance XXX”的意思是說應(yīng)用嘗試調(diào)用一個(gè)不存在的方法。通常是由調(diào)用方法的對象不正確導(dǎo)致。此處有問題的對象是一個(gè)UINavigationController對象(內(nèi)存地址為0x6a33840),調(diào)用的方法是setList:。

知道了crash的原因就好了,當(dāng)前的首要任務(wù)就是找出代碼中出錯(cuò)的地方。必須找到源文件名字以及出錯(cuò)行數(shù)。可以借助調(diào)用堆棧(also known as the stacktrace or the backtrace)。

當(dāng)一個(gè)應(yīng)用crash,Xcode窗口左側(cè)的區(qū)域會切換進(jìn)入調(diào)試導(dǎo)航頁。顯示出當(dāng)前應(yīng)用中活動狀態(tài)的線程信息,并高亮標(biāo)出crash掉的線程。 通常都是應(yīng)用的主線程Tread 1,因?yàn)榇蟛糠值臉I(yè)務(wù)工作在這個(gè)線程中完成的。如果應(yīng)用中使用了隊(duì)列或后臺線程,崩潰也會出現(xiàn)在這些線程中。

堆棧信息
堆棧信息

目前,Xcode會自動標(biāo)出出錯(cuò)點(diǎn)在main.m文件中的main()函數(shù)中。此處并不會提供更多信息,所以我們必須更深入一些尋找線索。

查看更多的堆棧信息,往右側(cè)拖動堆棧信息下方的滑塊,這樣將會完整顯示當(dāng)前crash掉的線程信息:

堆棧完整信息
堆棧完整信息

列表中的每一條都是一個(gè)應(yīng)用中或者某一個(gè)IOS frameworks的函數(shù)或方法。堆棧信息能顯示出應(yīng)用中當(dāng)前還處于活動狀態(tài)的方法和函數(shù)。調(diào)試器暫停了應(yīng)用中斷了這些函數(shù)和方法的執(zhí)行。

最后一條函數(shù)start()是入口。這個(gè)函數(shù)執(zhí)行的過程中調(diào)用了它上面的函數(shù), main()。是應(yīng)用的入口點(diǎn),經(jīng)常顯示在靠近底部的位置。main() 調(diào)用了 UIApplicationMain(),就是編輯窗口中綠色箭頭指向的代碼行。

查看更深入的堆棧信息,UIApplicationMain() 調(diào)用了 UIApplication 對象的 _run 方法。_run調(diào)用了CFRunLoopRunInMode()CFRunLoopRunInMode()又調(diào)用了CFRunLoopRunSpecific(),這樣層層調(diào)用直到 __pthread****_kill。

調(diào)用堆棧信息
調(diào)用堆棧信息

除了main(),調(diào)用堆棧中所有的函數(shù)和方法全部是灰化顯示。這是因?yàn)檫@些信息都來自編譯好的iOS庫,沒有有效的源代碼導(dǎo)致。

堆棧中的源代碼文件只有main.m,所以盡管main.m并不是真正引入crash的文件,但是Xcode文件編輯器提示崩潰的點(diǎn)還是在這個(gè)源文件里。這個(gè)經(jīng)常弄迷糊新手,所以本文中將快速給出一個(gè)方便大家理解的途徑。

點(diǎn)擊堆棧信息中其他的任何一條信息,都能看到一堆毫無頭緒的匯編代碼:

調(diào)用堆棧信息
調(diào)用堆棧信息

哦,如果有源代碼就好了! :-)

二、異常斷點(diǎn)

因此到底該如何找到crash的代碼行呢?不論何時(shí)出現(xiàn)了上面的堆棧信息的時(shí)候,都是app拋出了異常。(也可以說是堆棧上調(diào)用到了objc_exception_rethrow函數(shù))應(yīng)用做了不該做的事情就會拋出異常。目前關(guān)注的是這個(gè)異常導(dǎo)致的結(jié)果:app做了些不應(yīng)該做的事情,拋出了異常,Xcode將其呈現(xiàn)出來。我們想確切知道到底是哪里拋的異常。

幸運(yùn)的是,Xcode還可以打全局?jǐn)帱c(diǎn)暫停程序。斷點(diǎn)可以幫助開發(fā)人員在某個(gè)場景暫停程序執(zhí)行,本文的第二部分將會詳細(xì)闡述這塊。這里需要使用的是一種特殊的斷點(diǎn)——全局?jǐn)帱c(diǎn),程序crash前進(jìn)入全局?jǐn)帱c(diǎn)。

可以進(jìn)入斷點(diǎn)導(dǎo)航頁設(shè)置全局?jǐn)帱c(diǎn):

斷點(diǎn)導(dǎo)航
斷點(diǎn)導(dǎo)航

點(diǎn)擊底部的小+ 按鈕,選擇Add Exception Breakpoint

添加斷點(diǎn)
添加斷點(diǎn)

新的全局?jǐn)帱c(diǎn)就添加好了:

添加好的斷點(diǎn)
添加好的斷點(diǎn)

點(diǎn)擊確定按鈕退出彈框提醒。此時(shí)Xcode的工具欄的斷點(diǎn)按鈕現(xiàn)在變成可用狀態(tài)。如果你想程序跑起來不進(jìn)任何斷點(diǎn)的話,可以點(diǎn)擊這個(gè)斷點(diǎn)按鈕關(guān)閉斷點(diǎn),但是現(xiàn)在,打開斷點(diǎn)并運(yùn)行app。

崩潰中的高亮代碼
崩潰中的高亮代碼

好了,代碼編輯器現(xiàn)在不再是不再是匯編代碼,而是指向了源代碼,同時(shí)注意看左側(cè)的堆棧信息(是否切換出來堆棧信息,由Xcode的設(shè)置決定)也發(fā)生了變化。

明顯,問題指向AppDelegateapplication:didFinishLaunchingWithOptions:方法中下面這行代碼:

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

再來看看錯(cuò)誤信息:

[UINavigationController setList:]: unrecognized selector sent to instance 0x6d4ed20

代碼中的“viewController.list = something”調(diào)用了setList:,因?yàn)椤發(fā)ist”是MainViewController類的一個(gè)屬性。盡管在錯(cuò)誤處看viewController并不是指向MainViewController對象的實(shí)例而是指向了UINavigationController,當(dāng)然UINavigationController根本沒有一個(gè)“l(fā)ist”屬性。又混亂了!

打開Storyboard查看窗口的rootViewController屬性實(shí)際是這樣:

Storyboard-with-navigation-controller
Storyboard-with-navigation-controller

啊哈!storyboard的初始控制視圖是Navigation Controller。這樣就能解釋為什么為什么window.rootViewController指向UINavigationController對象而不是預(yù)計(jì)的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設(shè)置成MainViewController的指針?,F(xiàn)在,viewController就指向正確的對象了。

注: 一旦出現(xiàn) “unrecognized selector sent to instance XXX” 錯(cuò)誤, 首先檢查對象的類型是不是正確以及被調(diào)用的方法究竟是否存在。經(jīng)常遇到的情況是指針并沒有指向正確的值導(dǎo)致實(shí)際調(diào)用的壓根就是預(yù)期外的其他類型對象的方法。

調(diào)用堆棧信息
調(diào)用堆棧信息

另外,方法名拼寫錯(cuò)誤也會導(dǎo)致出現(xiàn)該問題。稍后將會給出一個(gè)這樣的示例程序。

三、你的第一個(gè)內(nèi)存錯(cuò)誤

第一個(gè)問題修復(fù)了,再來運(yùn)行應(yīng)用看看。 喔,又在同一行Crash了, 只不過這次是 EXC_BAD_ACCESS 錯(cuò)誤??礃幼樱搼?yīng)用還有內(nèi)存處理上的問題。

EXC_BAD_ACCESS-on-array
EXC_BAD_ACCESS-on-array

定位內(nèi)存相關(guān)的crash相對來說會比較困難,因?yàn)殡[患可能出現(xiàn)在崩潰前已經(jīng)運(yùn)行很久的其他代碼中。有問題的代碼破壞了內(nèi)存結(jié)構(gòu),并不會立即在程序中體現(xiàn)出來,而是到一段時(shí)間后,其他地方再訪問這段內(nèi)存有問題了,才會爆出crash出來。

而且事實(shí)上,可能測試的時(shí)候這種bug根本就不會暴露出來,最終往往暴露在用戶的手機(jī)上。誰也不想出現(xiàn)這種情況。然而這種類型的crash也是比較容易處理的。如果仔細(xì)查看代碼編輯框就會發(fā)現(xiàn),Xcode原來早就警告我們這些存在內(nèi)存處理不妥當(dāng)?shù)拇a行了??匆姶a左側(cè)行標(biāo)上的黃色三角形了么?那就是一個(gè)編譯警告。點(diǎn)擊黃色的三角形,Xcode會自動彈出一個(gè)“Fix-it”的修復(fù)建議,如下圖:

Missing-sentinel
Missing-sentinel

此處用一種對象的序列來初始化NSArray對象,這種序列需要以nil結(jié)尾,但是此處并沒有以nil結(jié)尾,編譯器不知道該何處是序列的結(jié)尾,所以就報(bào)了這個(gè)警告出來。運(yùn)行時(shí),因?yàn)闆]有明顯的結(jié)尾標(biāo)志,所以系統(tǒng)會在讀完了所有的參數(shù)后,還嘗試獲取并不存在的對象,添加到序列中,所以就崩潰了。

這種錯(cuò)誤實(shí)在不應(yīng)該犯,特別是Xcode已經(jīng)給出警告后。修復(fù)這個(gè)bug可以像下面代碼一樣,給序列添加nil結(jié)尾項(xiàng)(或者,直接點(diǎn)擊“Fix-it”):

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

四、“這個(gè)類不符合鍵值編碼”

在運(yùn)行代碼,看還有哪些其他有趣的bug。但是你是怎么知道的?它又一次崩潰在了main.m中。盡管全局?jǐn)帱c(diǎn)還有效我們卻看不到任何高亮的代碼提示,這次代碼的crash實(shí)實(shí)在在沒有發(fā)生在我們應(yīng)用的源代碼中。堆棧信息證實(shí),除了main(),沒有一個(gè)方法屬于我們的應(yīng)用:

調(diào)用堆棧信息
調(diào)用堆棧信息

自上而下查看方法名,可以發(fā)現(xiàn)有些事情發(fā)生跟NSObject鍵值編碼(Key-Value Coding)有關(guān)系。其下,是對[UIRuntimeOutletConnection connect]的調(diào)用。不知道該怎么辦,但是看上去好像跟綁定(connect outlets)有關(guān)系.再往下調(diào)用的方法是從nib中加載view。這已經(jīng)給出了線索。然而Xcode的調(diào)試窗還沒有很方便定位的錯(cuò)誤信息,因?yàn)橄到y(tǒng)尚未拋出異常。全局?jǐn)帱c(diǎn)僅僅會在程序告訴你異常原因前中斷程序。有時(shí)候你會獲取到一個(gè)明確的錯(cuò)誤信息,有時(shí)候并不能獲取到。

想看到完整的錯(cuò)誤信息,點(diǎn)擊調(diào)試窗口工具欄的“Continue Program Execution”按鈕:

調(diào)用堆棧信息
調(diào)用堆棧信息

有時(shí)候可能需要多點(diǎn)幾下,才能看到打印出來的錯(cuò)誤信息:

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

和之前一樣忽略下方的數(shù)字。它們表示的是調(diào)用棧信息,但是我們已經(jīng)有關(guān)于它們的更方便和可讀的信息了!就是左側(cè)的調(diào)試導(dǎo)航頁面。

有趣的信息如下:

  • NSUnknownKeyException
  • MainViewController
  • “this class is not key value coding-compliant for the key button”

異常的名字NSUnknownKeyException通常能很好的指示出問題原因。比如此處,就告訴我們代碼某處使用了系統(tǒng)不知道的“鍵值(unknown key)”。這里的某處很明顯是MainViewController,而且鍵值名應(yīng)該就是“button”。

可以確定,問題就發(fā)生在加載nib的時(shí)候。雖然應(yīng)用直接使用的是storyboard,但是更深入些storyboard實(shí)際就是所有nib的集合,所以問題應(yīng)該就處在storyboard中。

檢查MainViewController中的所有outlet

調(diào)用堆棧信息
調(diào)用堆棧信息

在鏈接監(jiān)測區(qū),可以看到試圖控制器中心的UIButton被鏈接到MainViewController“button”了。所以storyboard/nib有一個(gè)出口叫“button”,但是根據(jù)錯(cuò)誤信息看的話,實(shí)際根本沒有這個(gè)出口。

看看MainViewController.h:

@interface MainViewController : UIViewController

@property (nonatomic, retain) NSArray *list;
@property (nonatomic, retain) IBOutlet UIButton *button;

- (IBAction)buttonTapped:(id)sender;

@end

此處@property 定義了名為 “button” 出口, 所以到底是怎么回事? 如果你注意到編譯警告,或許就不難找到問題癥結(jié)了。

即使沒有,檢查下MainViewController.m文件中的@synthesiz列表。現(xiàn)在找到問題了么?

代碼并沒有準(zhǔn)確的@synthesize按鈕屬性。它告訴MainViewController有一個(gè)名字叫“button”的屬性,但是卻并沒有提供實(shí)例變量以及存取方法(這些都是由@synthesize完成的)

MainViewController.m中的@synthesize之下添加如下代碼來處理這個(gè)問題:

@synthesize button = _button;

現(xiàn)在運(yùn)行程序?qū)⒉辉賑rash了!

注: “this class is not key value coding-compliant for the key XXX” 錯(cuò)誤經(jīng)常出現(xiàn)在加載聲明但是并未實(shí)現(xiàn)的屬性的nib時(shí)。一般當(dāng)從源文件中刪除了一個(gè)outlet屬性,但是并沒有從nib去掉隊(duì)形鏈接時(shí),就會出現(xiàn)這中錯(cuò)誤。

五、點(diǎn)擊按鈕

現(xiàn)在應(yīng)用可以運(yùn)行了,或者說至少啟動沒問題了,來,點(diǎn)擊按鈕試試。

調(diào)用堆棧信息
調(diào)用堆棧信息

哇哦,應(yīng)用崩潰在main.m中,還報(bào)了個(gè)SIGABRT錯(cuò)誤信息。調(diào)試窗口的錯(cuò)誤信息如下:

Problems[6579:f803] -[MainViewController buttonTapped]: unrecognized selector sent
to instance 0x6e44850

堆棧信息并不很明了,它列出了所有的可能通過這樣那樣途徑發(fā)消息或執(zhí)行操作的方法,但是已經(jīng)可以知道哪個(gè)操作有問題。畢竟這里是點(diǎn)擊了UIButton,調(diào)用IBAction method時(shí)出的問題。

當(dāng)然,之前已經(jīng)遇到過類似問題了。因?yàn)檎{(diào)用了一個(gè)并沒有實(shí)現(xiàn)的方法導(dǎo)致。不過這次目標(biāo)對象MainViewController似乎沒有問題,因?yàn)榛顒臃椒ň褪窃谶@個(gè)有按鈕的view controller中。而且頭文件MainViewController.h中也確實(shí)存在IBAction方法:

- (IBAction)buttonTapped:(id)sender;

或者是不是這樣?錯(cuò)誤信息是想告訴我們方法名是buttonTapped,但是MainViewController的方法名卻是以冒號結(jié)尾的buttonTapped:,因?yàn)樗试S傳入一個(gè)參數(shù)(名字叫“sender”)。反過來說,錯(cuò)誤信息中的方法名并不包含冒號,因?yàn)椴恍枰獋魅雲(yún)?shù)。所以正確格式的方法應(yīng)該是這樣:

- (IBAction)buttonTapped;

這里到底是怎么回事呢?方法初始化的時(shí)候是沒有參數(shù)的格式(有些情況允許沒有參數(shù)的響應(yīng)方法),同時(shí),storyboard將該方法關(guān)聯(lián)成了按鈕的點(diǎn)擊(Touch Up Inside)事件響應(yīng)。但是,后來方法變成了包含一個(gè)“sender”參數(shù)的格式,但是storyboard的關(guān)聯(lián)沒有實(shí)時(shí)更新。

我們可以在storyboard的按鈕鏈接窗口看到如下場景:

調(diào)用堆棧信息
調(diào)用堆棧信息

先斷開點(diǎn)擊(Touch Up Inside)的鏈接(點(diǎn)擊小的X按鈕),接著將其再一次鏈接到主視圖控制器,不過這次選擇buttonTapped:方法。注意,這時(shí)鏈接窗口中的方法名末尾是包含了冒號的。

再運(yùn)行程序后點(diǎn)擊按鈕。什么鬼?盡管現(xiàn)在已經(jīng)使用了有冒號的正確格式的點(diǎn)擊方法buttonTapped:,還是報(bào)“unrecognized selector”錯(cuò)誤信息,

Problems[6675:f803] -[MainViewController buttonTapped:]: unrecognized selector sent
to instance 0x6b6c7f0

如果仔細(xì)查看的話,就會發(fā)現(xiàn)編譯警告又是跟上面類似的場景。Xcode在抱怨MainViewController實(shí)現(xiàn)文件不完整。特別是buttonTapped:沒有實(shí)現(xiàn)。

調(diào)用堆棧信息
調(diào)用堆棧信息

該看看MainViewController.m文件了。這里面明明有buttonTapped:方法的,呃,等等,拼寫好像不對:

- (void)butonTapped:(id)sender

好了這個(gè)很好修復(fù),修改下名字:

- (void)buttonTapped:(id)sender

請注意,盡管將方法定義成IBAction會使代碼看上去整潔,但是也沒有必要非將方法定義成IBAction。

注:如果留神編譯器的警告的話,這章節(jié)的問題都是比較容易定位的。就個(gè)人來說,我是將所有的警告都當(dāng)做錯(cuò)誤來處理 (在Xcode的編譯設(shè)置頁:Build Settings screen有一個(gè)設(shè)置項(xiàng),將警告當(dāng)做error) ,所以我會在程序運(yùn)行前處理修復(fù)掉所有的警告信息。 Xcode能很好的指出如上的低級錯(cuò)誤,留神這些警告信息能達(dá)到事半功倍的效果。

六、內(nèi)存信息

繼續(xù)之前的操作:運(yùn)行程序,點(diǎn)擊按鈕,等待崩潰。是呢,不負(fù)所望:

調(diào)用堆棧信息
調(diào)用堆棧信息

好驚訝,這次是EXC_BAD_ACCESS錯(cuò)誤中的另一種。幸運(yùn)的是,Xcode告訴我們崩潰發(fā)生的位置在buttonTapped:方法中這一行:

NSLog("You tapped on: %s", sender);

有時(shí)候,這種問題會讓我們反應(yīng)不過來發(fā)生了什么,同樣不用擔(dān)心,Xcode提供了幫手,點(diǎn)擊黃色的三角形看看有什么錯(cuò)誤:

調(diào)用堆棧信息
調(diào)用堆棧信息

NSLog()使用的是Objective-C類型的字符串,并不是C類型的字符串,所以插入一個(gè)@來修復(fù):

NSLog(@"You tapped on: %s", sender);

仔細(xì)看發(fā)現(xiàn)黃色警告信息并沒有消除。因?yàn)檫@行還有其他不知道會不會導(dǎo)致崩潰的問題存在。這同樣很有趣,有時(shí)候代碼運(yùn)行的好好的,或者說看上去執(zhí)行的好好的,但是其他不知道什么時(shí)候它就崩潰了(當(dāng)然此類型的崩潰經(jīng)常發(fā)生在客戶手機(jī)上)。

讓我們來看看具體的警告信息:

調(diào)用堆棧信息
調(diào)用堆棧信息

%s表示的是C類型的字符串。C類型的字符串實(shí)際只是一串連續(xù)的以空字符(NULL character,實(shí)際值是0)結(jié)尾的byte類型數(shù)組。比如,C類型的字符串“Crash!”實(shí)際在內(nèi)存中的存儲如下:

調(diào)用堆棧信息
調(diào)用堆棧信息

如果你的方法或函數(shù)中用到了C類型的字符串,那必須先確認(rèn)字符串是以0結(jié)尾的,否則函數(shù)處理時(shí)沒辦法識別出字符串的結(jié)尾。

現(xiàn)在,當(dāng)你在NSLog()格式的字符串或者NSStringstringWithFormat的字符串中使用%s,參數(shù)就會被當(dāng)做C類型的字符串解析。這種情況下,作為參數(shù)傳入的“sender”實(shí)際是一個(gè)UIButton的對象,并不是個(gè)C類型的字符串。甚至當(dāng)“sender”指向的內(nèi)存中包含字節(jié)0,NSLog()將不會崩潰,而是輸出類似如下的信息:

You tapped on: x?j

可以準(zhǔn)確的看到這些信息來自何處。再一次運(yùn)行程序,點(diǎn)擊按鈕等待崩潰。在左側(cè)的調(diào)試區(qū),右擊“sender”選擇“View Memory of *sender”項(xiàng)(確認(rèn)點(diǎn)擊的是帶*號的)。

調(diào)用堆棧信息
調(diào)用堆棧信息

Xcode這時(shí)會這塊內(nèi)存地址存儲的內(nèi)容,也就是剛剛NSLog()輸出的信息。

調(diào)用堆棧信息
調(diào)用堆棧信息

然而并不能保證這塊內(nèi)存中一定有NULL字節(jié),所以EXC_BAD_ACCESS錯(cuò)誤并不是能輕易報(bào)出的。如果一直在模擬器上運(yùn)行程序,可能跑很長時(shí)間都出不來這個(gè)錯(cuò)誤,因?yàn)槟阕约旱臏y試環(huán)境總是你自己最喜歡的狀態(tài)。這也導(dǎo)致此類型的bug很難浮現(xiàn)。

當(dāng)然,這種情況發(fā)生時(shí)Xcode已經(jīng)給你了類型錯(cuò)誤的警告了,所以這種特殊的bug也是很好查找的。但是不論何時(shí),只要使用了C類型的字符串或者直接操作了內(nèi)存,都要特別小心是不是影響了其他地方的內(nèi)存,導(dǎo)致它們出了問題。

如果應(yīng)用總是奔潰,那么恭喜你這個(gè)bug定位起來實(shí)際很容易,但是比較常見的是,應(yīng)用程序時(shí)而崩潰時(shí)而不崩潰,這將是問題復(fù)現(xiàn)非常困難!這種情況下定位這個(gè)bug也將變成史詩級的難題。

修復(fù)此處NSLog()語句的問題,用下面的方法:

NSLog(@"You tapped on: %@", sender);

運(yùn)行程序再一次點(diǎn)擊按鈕。NSLog()做了我們期望它做的事情,但是似乎我們還沒有處理好buttonTapped:的崩潰。

七、和調(diào)試器做朋友

Xcode告訴我們這個(gè)最新的crash在這一行:

[self performSegueWithIdentifier:@"ModalSegue" sender:sender];

調(diào)試窗口沒有信息輸出。你可以像之前一樣一直點(diǎn)擊繼續(xù)運(yùn)行按鈕,或者你也可以在調(diào)試器中輸入一個(gè)命令去獲取錯(cuò)誤信息。這樣做的好處是,程序還是中斷在之前的位置。
如果是在模擬器上運(yùn)行,可以輸入如下的(lldb)命令:

(lldb) po $eax

LLDB是Xcode4.3及以上版本中默認(rèn)調(diào)試器。如果使用的是早期的Xcode版本,可以使用GDB調(diào)試器。他們倆的基本命令一致,所以即便你的Xcode編譯命令前面的標(biāo)記是(gdb)而不是上面的(lldb),也一樣可以繼續(xù)(順便補(bǔ)充一下,you can switch between debuggers in the Scheme editor in Xcode, under the Run action. And you can access the Scheme editor by Alt-tapping the Run icon at the top left corner of your Xcode window.)。po命令代表打印對象(print object)。參數(shù)$eax指向一個(gè)CPU寄存器。出現(xiàn)異常的情況下,這個(gè)寄存器中的數(shù)據(jù)將包含一個(gè)指向NSException對象的指針。注意:$eax僅在模擬器環(huán)境下有效,如果你使用真機(jī)調(diào)試,那么要訪問的寄存器是$r0。

例如,這樣輸入:

(lldb) po [$eax class]

將看到這樣的信息:

(id) $2 = 0x01446e84 NSException

數(shù)字并不重要,但是明顯你正在處理的NSException對象是存儲在這里的。

你可以通過這個(gè)對象調(diào)用任何NSException的方法。比如:

(lldb) po [$eax name]

這行命令將輸出該異常的名稱,比如這里是NSInvalidArgumentException,另外輸出如下命令:

(lldb) po [$eax reason]

將告訴我們異常的原因:

(unsigned int) $4 = 114784400 Receiver (<MainViewController: 0x6b60620>) has no
segue with identifier 'ModalSegue'

: 調(diào)用“po $eax”的時(shí)候, 通常也會調(diào)用到對象的“description”方法并且輸出, 這種情況下一般就已經(jīng)給出了錯(cuò)誤消息。

正好就解釋了剛剛發(fā)生了什么:你的本意是執(zhí)行一個(gè)名叫“ModalSegue”segue,但是顯然MainViewController中不存在這個(gè)東東。

storyboard顯示一個(gè)segue使用的是模態(tài)方式,但是你忘記設(shè)置它的標(biāo)識,這是個(gè)典型的錯(cuò)誤:

調(diào)用堆棧信息
調(diào)用堆棧信息

修改segue的標(biāo)識為“ModalSegue”。再一次運(yùn)行程序,點(diǎn)擊按鈕,等待crash。這次不再崩潰了!但是呢,這里遺留了我們下一篇要討論的問題:顯示的tableview不應(yīng)該為空!

八、相關(guān)篇章

所以這個(gè)空白的tableview到底是關(guān)于什么的? 這是本文的一個(gè)懸念,下一篇中會詳細(xì)解釋,同時(shí)也會解決一些很有趣的在你編程生涯中曾經(jīng)遇到過bug。當(dāng)然通過第二部分學(xué)習(xí),你還可以更加充實(shí)自己的調(diào)試技能,比如NSLog()語句,斷點(diǎn)以及僵尸對象(Zombie Objects)。

當(dāng)所有的這些做完講完,我承諾程序一定會按照我們期待的那樣運(yùn)行!最重要的是,你已經(jīng)掌握了技能當(dāng)你的程序出現(xiàn)這些令人挫敗的問題時(shí),你也必將能妥善處理它們。

有任何意見和建議請至論壇找我!

調(diào)用堆棧信息
調(diào)用堆棧信息
這篇文章的作者是iOS Tutorial Team 的成員Matthijs Hollemans,他是一個(gè)經(jīng)驗(yàn)豐富的ios設(shè)計(jì)開發(fā)工作者,他的聯(lián)系方式:Google+Twitter

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 178,917評論 25 709
  • 轉(zhuǎn)載 與調(diào)試器共舞 - LLDB 的華爾茲: https://objccn.io/issue-19-2/ 推薦:i...
    F麥子閱讀 3,456評論 0 10
  • WebSocket-Swift Starscream的使用 WebSocket 是 HTML5 一種新的協(xié)議。它實(shí)...
    香橙柚子閱讀 24,726評論 8 183
  • 遠(yuǎn)上寒山霧,飛鳥影無蹤。 裊裊炊煙淡,行行素鱗藏。 羨云隨日落,徒使淚稀松。 長歌伴閑庭,晴陽鬧遠(yuǎn)空。
    鳳回閱讀 180評論 0 2
  • 我是日記星球238號星寶寶,我正在參加日記星球第五期21天蛻變之旅,這是我的第23篇原創(chuàng)日記,我相信堅(jiān)持的力量! ...
    Ms娟子閱讀 225評論 0 0

友情鏈接更多精彩內(nèi)容