
前言
Bebug調(diào)試程序是開發(fā)中最常見的問題,對(duì)于一些簡(jiǎn)單有效的調(diào)試技巧的了解是很有必要的。這篇文章就列舉Debug中用到的一些簡(jiǎn)單的技巧。
一.打印
相信在調(diào)試程序時(shí),打印有時(shí)候一定是少不了的,當(dāng)然你也可以用LLDB命令完全代替,但是打印技巧依然是比較實(shí)用的一種調(diào)試技巧。
#ifdef DEBUG
#define NSLog(...) NSLog(@"%s %@", __PRETTY_FUNCTION__, [NSString stringWithFormat:__VA_ARGS__])
#else
#define NSLog(...) do { } while (0)
#endif
#endif
這是一個(gè)使用了條件編譯和宏重寫的 NSLog,在打印出信息的時(shí)候,會(huì)打印出當(dāng)前類名稱和方法名稱。
二.斷點(diǎn)
(1)普通斷點(diǎn)
普通斷點(diǎn)是調(diào)試中最常使用的。當(dāng)程序運(yùn)行到斷點(diǎn)處就會(huì)暫停運(yùn)行。
用于在某些重要的操作前查看關(guān)鍵參數(shù)的值。
(2)條件斷點(diǎn)
點(diǎn)擊Xcode editor的‘gutter’來添加斷點(diǎn),右鍵點(diǎn)擊斷點(diǎn),然后選擇“edit breakpoint”來設(shè)置特定條件。

你還可以添加能根據(jù)斷點(diǎn)自動(dòng)發(fā)生的動(dòng)作,例如一個(gè)debugger command---打印一個(gè)值,
以下是親測(cè)可用的條件斷點(diǎn)語(yǔ)句:
dic.count == 3 ? ? ? ? ?[dic count] == 4
cmd == "login" ? 不是 cmd ==@"login" ? ? ?[cmd isEqualToString:@"login"]
i==1
[[dic objectForKey:@"errorCode"]isEqualToString:@"10"]
!dic
在Edit Breakpoint...窗口中有下面四個(gè)輸入項(xiàng):
Condition?設(shè)置的條件。
Ignore ? ?表示忽略多少次之后斷點(diǎn)有效。例如:不設(shè)置條件,Ignore設(shè)置為4,則i=4時(shí)程序在斷點(diǎn)處停止。
Action ? ?在程序斷點(diǎn)處執(zhí)行的操作(執(zhí)行完這個(gè)操作后代碼停止運(yùn)行,此時(shí)設(shè)置斷點(diǎn)處的代碼還沒有執(zhí)行)。這里的操作是LLDB語(yǔ)句,關(guān)于LLDB會(huì)在下面介紹。
Options ?當(dāng)選中時(shí),執(zhí)行完Action的操作后代碼不會(huì)停止,就像沒有設(shè)置斷點(diǎn)一樣。
(3)異常斷點(diǎn)Exception BreakPoint
在設(shè)置異常斷點(diǎn)的情況下 當(dāng)程序crash時(shí) Xcode會(huì)幫我們定位到crash產(chǎn)生的位置。
設(shè)置異常斷點(diǎn)步驟:
(4)符號(hào)斷點(diǎn)Symbolic Breakpoint
符號(hào)斷點(diǎn)能夠?yàn)槟骋粋€(gè)方法或者 某一個(gè)類的某一個(gè)方法設(shè)置斷點(diǎn)。實(shí)現(xiàn)的功能如下圖:
某一個(gè)類的某一個(gè)方法設(shè)置斷點(diǎn)
在某個(gè)方法中執(zhí)行斷點(diǎn)
設(shè)置步驟如下:
如果你的Symbol只寫了一個(gè)函數(shù)名,那么就會(huì)在出現(xiàn)該函數(shù)名的地方就中斷執(zhí)行。如下,就會(huì)在運(yùn)行到doAnimation的時(shí)候中斷。是不是很強(qiáng)大呢?

三.LLDB命令
LLDB的Xcode默認(rèn)的調(diào)試器, 我們通過執(zhí)行LLDB命令使調(diào)試過程更加的靈活。并且可以通過指令立刻看到一些我們需要修改完代碼再次運(yùn)行才能看到的效果或者結(jié)果,超級(jí)實(shí)用。Xcode內(nèi)嵌LLDB調(diào)試窗口。在程序執(zhí)行到斷點(diǎn)后你可以輸入LLDB命令操作調(diào)試過程。
LLDB常用命令如下:
1, p (print)用于輸出基本類型, 如 p (int)[[[self view] subviews] count] 輸出子視圖個(gè)數(shù)。(而且還能輸出字符串的地址,很實(shí)用,想看字符串指針指向的地址就不用 NSLog啦。)
2, po (print object)輸出對(duì)象, 如 po [self view]; (相當(dāng)于普通的NSLog)
3, expr (expression) 可以在調(diào)試時(shí)動(dòng)態(tài)執(zhí)行指定表達(dá)式,并將結(jié)果打印出來。常用于在調(diào)試過程中修改變量的值。例如上圖所示,程序第一次執(zhí)行到斷點(diǎn)時(shí)
執(zhí)行下面的指令:expr i=4
你會(huì)看到如下的輸出: (int) $0 = 4
繼續(xù)運(yùn)行程序,程序輸出的信息是:value:4 i==4 ? (這個(gè)功能相當(dāng)于,不修改代碼再次運(yùn)行的情況下,動(dòng)態(tài)修改參數(shù)值看程序的執(zhí)行結(jié)果,很強(qiáng)大。)
4, call ? ? ?call即是調(diào)用的意思。其實(shí)上述的po和p也有調(diào)用的功能。因此一般只在不需要顯示輸出,或是方法無返回值時(shí)使用call。我們可以在viewDidLoad:里面設(shè)置斷點(diǎn),然后在程序中斷的時(shí)候輸入下面的命令: call [self.view setBackgroundColor:[UIColor redColor]] 此時(shí)view的背景顏色變?yōu)榧t色。(這個(gè)相當(dāng)于,動(dòng)態(tài)往程序中加入新的代碼,不用修改代碼再次運(yùn)行即可看到一些你想看到的效果,超級(jí)強(qiáng)大。)
5, bt ? ??打印當(dāng)前線程的調(diào)用堆棧,加all可打印所有thread的堆棧。不詳細(xì)舉例說明,感興趣的朋友可以自己試試。
6, fr v -R 命令來打印出變量的未加工過時(shí)的信息,
7.help ?最簡(jiǎn)單命令是 help,它會(huì)列舉出所有的命令。如果你忘記了一個(gè)命令是做什么的,或者想知道更多的話,你可以通過 help來了解更多細(xì)節(jié),例如 help print或者 help thread如果你甚至忘記了help命令是做什么的,你可以試試help help不過你如果知道這么做,那就說明你大概還沒忘光這個(gè)命令。
如果想了解更詳細(xì)的內(nèi)容,戳這里。
控制臺(tái)左側(cè)的調(diào)試區(qū):
在左側(cè)調(diào)試區(qū)? 右鍵 選擇“Add Expression” 輸入你想要顯示的變量名稱,即可立即顯示(注意這個(gè)變量不可以是不直觀的)。這種方法一般用于你在斷點(diǎn) debug時(shí),鼠標(biāo)光標(biāo)放上去的時(shí)候不顯式某個(gè)你想要的值,可以這樣讓其顯示

這種方法更加的強(qiáng)大,在斷點(diǎn)debug的時(shí)候,完全代替了“想看某一個(gè)隱形值,左邊調(diào)試區(qū)又看不到,自己加一個(gè) ?NSlog ,關(guān)閉程序,再次運(yùn)行”的尷尬,再次運(yùn)行一個(gè)龐大項(xiàng)目是很耗時(shí)間的,就為了加一個(gè) ?NSLOG???,
po 命令:為 print object 的縮寫,顯示對(duì)象的文本描述(顯示從對(duì)象的 description 消息獲得的字符串信息)。
(它甚至可以打印一些通過方法才能得到的值,如下,很強(qiáng)大,有人說左邊不是也可以看嗎,左邊是可以看,但是需要一層一層打開,不夠直觀)

四. Scheme中run狀態(tài)下的 Diagnostics(診斷)
野指針分析方法(Enable Malloc Scribble)
因?yàn)橐爸羔樀脑虬l(fā)生崩潰是常常出現(xiàn)的事,而且比較隨機(jī)。關(guān)于一些原因及概念后面我們會(huì)講到。所以我們要提高野指針的崩潰率好來幫我們快速找到有問題的代碼。對(duì)象釋放后只有出現(xiàn)被隨機(jī)填入的數(shù)據(jù)是不可訪問的時(shí)候才會(huì)必現(xiàn)Crash。
這個(gè)地方我們可以做一下手腳,把這一隨機(jī)的過程變成不隨機(jī)的過程。對(duì)象釋放后在內(nèi)存上填上不可訪問的數(shù)據(jù),其實(shí)這種技術(shù)其實(shí)一直都有,xcode的Enable Scribble就是這個(gè)作用。

enter image description here
更加詳細(xì)的介紹可以參考:如何定位Obj-C野指針隨機(jī)Crash。
DSCrashDemo這個(gè)demo里有上面這篇文章里寫的關(guān)于提高野指針崩潰率的例子。
僵尸模式(NSZombieEnabled)
啟用了NSZombieEnabled的話,它會(huì)用一個(gè)僵尸來替換默認(rèn)的dealloc實(shí)現(xiàn),也就是在引用計(jì)數(shù)降到0時(shí),該僵尸實(shí)現(xiàn)會(huì)將該對(duì)象轉(zhuǎn)換成僵尸對(duì)象。僵尸對(duì)象的作用是在你向它發(fā)送消息時(shí),它會(huì)顯示一段日志并自動(dòng)跳入調(diào)試器。
所以當(dāng)啟用NSZombieEnabled時(shí),一個(gè)錯(cuò)誤的內(nèi)存訪問就會(huì)變成一條無法識(shí)別的消息發(fā)送給僵尸對(duì)象。僵尸對(duì)象會(huì)顯示接受到得信息,然后跳入調(diào)試器,這樣你就可以查看到底是哪里出了問題。
所以這時(shí)一般崩潰的原因是:調(diào)用了已經(jīng)釋放的內(nèi)存空間,或者說重復(fù)釋放了某個(gè)地址空間。
如何找出問題
1.NSZombieEnabled
EXC_BAD_ACCESS 可以這么說,90%的錯(cuò)誤來源在于對(duì)一個(gè)已經(jīng)釋放的對(duì)象進(jìn)行release操作。
最后提醒NSZombieEnabled只能在調(diào)試的時(shí)候使用,千萬不要忘記在產(chǎn)品發(fā)布的時(shí)候去掉,因?yàn)镹SZombieEnabled不會(huì)真正去釋放dealloc對(duì)象的內(nèi)存,一直開啟后果可想而知。
就是當(dāng)設(shè)置NSZombieEnabled環(huán)境變量后,一個(gè)對(duì)象銷毀時(shí)會(huì)被轉(zhuǎn)化為_NSZombie,設(shè)置NSZombieEnabled后,當(dāng)你向一個(gè)已經(jīng)釋放的對(duì)象發(fā)送消息,這個(gè)對(duì)象就不會(huì)向之前那樣Crash或者產(chǎn)生一個(gè)難以理解的行為,而是放出一個(gè)錯(cuò)誤消息,然后以一種可預(yù)測(cè)的可以產(chǎn)生debug斷點(diǎn)的方式消失, 因此我們就可以找到具體或者大概是哪個(gè)對(duì)象被錯(cuò)誤的釋放了。
對(duì) Xcode 設(shè)置了NSZombieEnabled 之后,Xcode 會(huì)明確定位在行[array addObject:@"Hello"],然后控制臺(tái)下報(bào)的錯(cuò)誤信息是:
*** -[__NSArray addObject:]:message sent to deallocated instance 0x6557370
2.MallocStackLoggingNoCompact
如果崩潰是發(fā)生在當(dāng)前調(diào)用棧,通過上面的做法,系統(tǒng)就會(huì)把崩潰原因定位到具體代碼中。但是,如果崩潰不在當(dāng)前調(diào)用棧,系統(tǒng)就僅僅只能把崩潰地址告訴我們,而沒辦法定位到具體代碼,這樣我們也沒法去修改錯(cuò)誤。這時(shí)就可以修改scheme,讓xcode記錄每個(gè)地址alloc的歷史,這樣我們就可以用命令把這個(gè)地址還原出來。
如圖:(跟設(shè)置NSZombieEnabled一樣,添加MallocStackLoggingNoCompact,并且設(shè)置為YES)

這樣,當(dāng)出現(xiàn)崩潰原因是message sent to deallocated instance 0x7179910,我們可以使用以下命令,把內(nèi)存地址還原:
(gdb) nfo malloc-history 0x7179910
也可以使用下面的命令
(gdb) shell malloc_history {pid/partial-process-name} {address}
這篇文章中有介紹MallocStackLoggingNoCompact的使用。
還有官方文檔Enabling the Malloc Debugging Features介紹了類似NSZombieEnabled和MallocStackLoggingNoCompact這類的環(huán)境變量的作用。
TODO:翻譯Enabling the Malloc Debugging Features這篇文章,寫對(duì)應(yīng)的demo測(cè)試這類變量設(shè)置后如何找出內(nèi)存出錯(cuò)問題。
Enable Address Sanitizer(地址消毒劑)

設(shè)置這個(gè)參數(shù)后就能看到一些更詳細(xì)的錯(cuò)誤信息提示,甚至?xí)袃?nèi)存使用情況的展示。

C語(yǔ)言是一門危險(xiǎn)的語(yǔ)言,內(nèi)存安全是一個(gè)主要的問題。C語(yǔ)言中根本沒有內(nèi)存安全可言。像下面的代碼,會(huì)被正常的編譯,而且可能正常運(yùn)行:
char *ptr = malloc(5);
ptr[12] = 0;
對(duì)于內(nèi)存安全的驗(yàn)證已經(jīng)有一些解決方案了。如Clang的靜態(tài)代碼分析,可以從代碼中查找特定類型的內(nèi)存安全問題。如Valgrind之類的程序可以在運(yùn)行時(shí)檢測(cè)到不安全的內(nèi)存訪問。
Address Sanitizer是另外一種解決方案。它使用了一種新的方法,有利有弊。但仍不失為一個(gè)查找代碼問題的有力工具。
這類工具的理論依據(jù)是:訪問內(nèi)存時(shí),通過比較訪問的內(nèi)存和程序?qū)嶋H分配的內(nèi)存,驗(yàn)證內(nèi)存訪問的有效性,從而在bug發(fā)生時(shí)就檢測(cè)到它們,而不會(huì)等到副作用產(chǎn)生時(shí)才有所察覺。
malloc函數(shù)總是最少分配16個(gè)字節(jié)。為了儲(chǔ)存針對(duì)標(biāo)準(zhǔn)malloc的內(nèi)存的保護(hù),需要分配內(nèi)存到16字節(jié)的范圍內(nèi),因此,若分配的內(nèi)存大小不是16字節(jié)的整數(shù)倍,余出的幾個(gè)字節(jié)將不受保護(hù)。
Address Sanitizer會(huì)追蹤受限內(nèi)存,使用了一種簡(jiǎn)單但是很巧妙的方法:它在進(jìn)程的內(nèi)存空間上保存了一個(gè)固定的區(qū)域,叫做“影子內(nèi)存區(qū)”。用內(nèi)存消毒劑的術(shù)語(yǔ)來說,一個(gè)被標(biāo)記為受限的內(nèi)存被稱作“中毒”內(nèi)存?!坝白觾?nèi)存區(qū)”會(huì)記錄哪些內(nèi)存字節(jié)是中毒的。通過一個(gè)簡(jiǎn)單的公式,可以將進(jìn)程中的內(nèi)存空間映射到“影子內(nèi)存區(qū)”中,即:每8字節(jié)的正常內(nèi)存塊映射到一個(gè)字節(jié)的影子內(nèi)存上。在影子內(nèi)存上,會(huì)跟蹤這8字節(jié)的“中毒狀態(tài)”。
Address Sanitizer這篇文章詳細(xì)介紹了Enable Address Sanitizer,對(duì)應(yīng)的中文翻譯在Xcode 7上直接使用Clang Address Sanitizer
Signal和EXC_BAD_ACCESS錯(cuò)誤分析
什么是Signal
在計(jì)算機(jī)科學(xué)中,信號(hào)(英語(yǔ):Signals)是Unix、類Unix以及其他POSIX兼容的操作系統(tǒng)中進(jìn)程間通訊的一種有限制的方式。它是一種異步的通知機(jī)制,用來提醒進(jìn)程一個(gè)事件已經(jīng)發(fā)生。當(dāng)一個(gè)信號(hào)發(fā)送給一個(gè)進(jìn)程,操作系統(tǒng)中斷了進(jìn)程正常的控制流程,此時(shí),任何非原子操作都將被中斷。如果進(jìn)程定義了信號(hào)的處理函數(shù),那么它將被執(zhí)行,否則就執(zhí)行默認(rèn)的處理函數(shù)。
在iOS中就是未被捕獲的Objective-C異常(NSException),導(dǎo)致程序向自身發(fā)送了SIGABRT信號(hào)而崩潰。
Signal信號(hào)的類型
SIGABRT–程序中止命令中止信號(hào)
SIGALRM–程序超時(shí)信號(hào)
SIGFPE–程序浮點(diǎn)異常信號(hào)
SIGILL–程序非法指令信號(hào)
SIGHUP–程序終端中止信號(hào)
SIGINT–程序鍵盤中斷信號(hào)
SIGKILL–程序結(jié)束接收中止信號(hào)
SIGTERM–程序kill中止信號(hào)
SIGSTOP–程序鍵盤中止信號(hào)
SIGSEGV–程序無效內(nèi)存中止信號(hào)
SIGBUS–程序內(nèi)存字節(jié)未對(duì)齊中止信號(hào)
SIGPIPE–程序Socket發(fā)送失敗中止信號(hào)
iOS異常捕獲這篇文章中有對(duì)各種信號(hào)的解釋。
EXC_BAD_ACCESS
EXC_BAD_ACCESS是一個(gè)比較難處理的crash了,當(dāng)一個(gè)app進(jìn)入一種毀壞的狀態(tài),通常是由于內(nèi)存管理問題而引起的時(shí),就會(huì)出現(xiàn)出現(xiàn)這樣的crash。通常1.7.1中的Signal信號(hào)錯(cuò)誤都會(huì)提醒EXC_BAD_ACCESS。
五.Static Analyzer(靜態(tài)分析)
Static Analyzer是一個(gè)非常好的工具去發(fā)現(xiàn)編譯器警告不會(huì)提示的問題和一些個(gè)人的內(nèi)錯(cuò)泄露和死存儲(chǔ)(不會(huì)用到的賦了值的變量)錯(cuò)誤。這個(gè)方法可能大大的提高內(nèi)存使用和性能,以及提升應(yīng)用的整體穩(wěn)定性和代碼質(zhì)量。
打開方式:Xcode->Product-Analyze
然后我們就能看到如下藍(lán)色箭頭所示的一些有問題的代碼。

使用Xcode來分析你的項(xiàng)目,從Xcode的 Product菜單選擇 Analyze或按 Shift-Command-B.Xcode的將需要片刻的時(shí)間,但是當(dāng)它完成的時(shí)候你會(huì)在左邊的 Issue Navigator看到問題列表。由Analyze發(fā)現(xiàn)的問題用藍(lán)色高亮顯示。

當(dāng)你點(diǎn)擊一個(gè)問題,Xcode的會(huì)指向問題代碼塊,這些正是你要的注意的地方。注意,Xcode僅僅是建議。在某些情況下,這是可能的,問題是不相關(guān)的,不固定。如果你找不到造成EXC_BAD_ACCESS的錯(cuò)誤,那就需要你仔細(xì)審視Xcode項(xiàng)目,分析其中發(fā)現(xiàn)的每一個(gè)問題。
六.Instruments檢查器
提起檢查器,我們有時(shí)會(huì)忽略Xcode本身自帶的顯示CPU,內(nèi)存,網(wǎng)絡(luò)的測(cè)試界面的使用,尤其是對(duì)流量的統(tǒng)計(jì)是比較實(shí)用的。

Instruments是一個(gè)強(qiáng)大而靈活的性能分析和測(cè)試工具,它是Xcode工具集的一部分。它旨在幫助您分析您的OS X和iOS應(yīng)用程序,過程和設(shè)備,以便更好地了解和優(yōu)化其行為和性能。從開發(fā)應(yīng)用程序開發(fā)流程到將工具集成到您的工作流程中,可以幫助您在開發(fā)周期的早期找到問題,從而節(jié)省您的時(shí)間。

七. 調(diào)試工具集:FLEX
FLEX是Flipboard開源的一系列在應(yīng)用中調(diào)試的工具集。FLEX以第三方庫(kù)的形式集成在應(yīng)用中,使用時(shí)將類庫(kù)加到工程中,然后 通過調(diào)用[[FLEXManager sharedManager] showExplorer];就可顯示出用于調(diào)試的工具欄進(jìn)行調(diào)試。

它提供的功能如下:
查看、修改views
查看任何對(duì)象的屬性
動(dòng)態(tài)的修改屬性
動(dòng)態(tài)的調(diào)用實(shí)例方法和類方法
查看網(wǎng)絡(luò)請(qǐng)求過程
添加模擬的鍵盤快捷鍵
查看系統(tǒng)日志
從堆中獲取任何對(duì)象
查看沙盒中的文件
查看文件系統(tǒng)中的SQLite/Realm數(shù)據(jù)庫(kù)
在模擬器中觸發(fā)3D touch
查看你應(yīng)用中所有的類
快速獲取常用的類,例如[UIApplication sharedApplication], the app delegate, the root view controller on the key window, and more.
動(dòng)態(tài)的查看NSUserDefaults里面的值
簡(jiǎn)直吊炸天。當(dāng)你將FLEX集成到你項(xiàng)目中時(shí)就會(huì)認(rèn)識(shí)到它的威力。看起來確實(shí)很強(qiáng)大,可是我并沒有使用過,不做評(píng)價(jià),以后使用后再更新相關(guān)信息。
小結(jié)
程序的調(diào)試包括很多方面,我這篇文章可能并不全面,但是也算是一個(gè)小結(jié),后續(xù)會(huì)持續(xù)更新。
本文參考文章: