1. 程式人生 > >Xcode自帶除錯命令集

Xcode自帶除錯命令集

LLDB的Xcode預設的偵錯程式,它與LLVM編譯器一起,帶給我們更豐富的流程控制和資料檢測的除錯功能。平時用Xcode執行程式,實際走的都是LLDB。熟練使用LLDB,可以讓你debug事半功倍

LLDB基礎知識

LLDB控制檯

Xcode中內嵌了LLDB控制檯,在Xcode中程式碼的下方,我們可以看到LLDB控制檯。



LLDB控制檯平時會輸出一些log資訊。如果我們想輸入命令除錯,必須讓程式進入暫停狀態。讓程式進入暫停狀態的方式主要有2種:

  1. 斷點或者watchpoint: 在程式碼中設定一個斷點(watchpoint),當程式執行到斷點位置的時候,會進入stop狀態
  2. 直接暫停,控制檯上方有一個暫停按鈕,上圖紅框已標出,點選即可暫停程式

LLDB語法

在使用LLDB之前,我們來先看看LLDB的語法,瞭解語法可以幫助我們清晰的使用LLDB:

<command> [<subcommand> [<subcommand>...]] <action> [-options [option-value]] [argument [argument...]]

一眼看上去可能比較迷茫,給大家解釋一下:

  1. <command>(命令)和<subcommand>(子命令):LLDB除錯命令的名稱。命令和子命令按層級結構來排列:一個命令物件為跟隨其的子命令物件建立一個上下文,子命令又為其子命令建立一個上下文,依此類推。
  2. <action>:執行命令的操作
  3. <options>:命令選項
  4. <arguement>:命令的引數
  5. []:表示命令是可選的,可以有也可以沒有

舉個例子,假設我們給main方法設定一個斷點,我們使用下面的命令:

breakpoint set -n main

這個命令對應到上面的語法就是:

  1. commandbreakpoint 表示斷點命令
  2. actionset 表示設定斷點
  3. option-n 表示根據方法name設定斷點
  4. arguementmian 表示方法名為mian

原始(raw)命令

LLDB支援不帶命令選項(options)的原始(raw)命令,原始命令會將命令後面的所有東西當做引數(arguement)傳遞。不過很多原始命令也可以帶命令選項,當你使用命令選項的時候,需要在命令選項後面加--

區分命令選項和引數。

e.g: 常用的expression就是raw命令,一般情況下我們使用expression列印一個東西是這樣的:

(lldb) expression count
(int) $2 = 4

當我們想列印一個物件的時候。需要使用-O命令選項,我們應該用--將命令選項和引數區分:

(lldb) expression -O -- self
<ViewController: 0x7f9000f17660>

唯一匹配原則

LLDB的命令遵循唯一匹配原則:假如根據前n個字母已經能唯一匹配到某個命令,則只寫前n個字母等效於寫下完整的命令。
e.g: 前面提到我設定斷點的命令,我們可以使用唯一匹配原則簡寫,下面2條命令等效:

breakpoint set -n main
br s -n main

~/.lldbinit

LLDB有了一個啟動時載入的檔案~/.lldbinit,每次啟動都會載入。所以一些初始化的事兒,我們可以寫入~/.lldbinit中,比如給命令定義別名等。但是由於這時候程式還沒有真正執行,也有部分操作無法在裡面玩,比如設定斷點。

LLDB命令

expression

expression命令的作用是執行一個表示式,並將表示式返回的結果輸出。expression的完整語法是這樣的:

expression <cmd-options> -- <expr>
  1. <cmd-options>:命令選項,一般情況下使用預設的即可,不需要特別標明。
  2. --: 命令選項結束符,表示所有的命令選項已經設定完畢,如果沒有命令選項,--可以省略
  3. <expr>: 要執行的表示式

expression是LLDB裡面最重要的命令都不為過。因為他能實現2個功能。

  • 執行某個表示式。
    我們在程式碼執行過程中,可以通過執行某個表示式來動態改變程式執行的軌跡。
    假如我們在執行過程中,突然想把self.view顏色改成紅色,看看效果。我們不必寫下程式碼,重新run,只需暫停程式,用expression改變顏色,再重新整理一下介面,就能看到效果

      // 改變顏色
      (lldb) expression -- self.view.backgroundColor = [UIColor redColor]
      // 重新整理介面
      (lldb) expression -- (void)[CATransaction flush]
  • 將返回值輸出。
    也就是說我們可以通過expression來列印東西。
    假如我們想列印self.view:

    (lldb) expression -- self.view
    (UIView *) $1 = 0x00007fe322c18a10

p & print & call

一般情況下,我們直接用expression還是用得比較少的,更多時候我們用的是pprintcall。這三個命令其實都是expression --的別名(--表示不再接受命令選項,詳情見前面原始(raw)命令這一節)

  1. print: 列印某個東西,可以是變數和表示式
  2. p: 可以看做是print的簡寫
  3. call: 呼叫某個方法。

表面上看起來他們可能有不一樣的地方,實際都是執行某個表示式(變數也當做表示式),將執行的結果輸出到控制檯上。所以你可以用p呼叫某個方法,也可以用call列印東西
e.g: 下面程式碼效果相同:

(lldb) expression -- self.view
(UIView *) $5 = 0x00007fb2a40344a0
(lldb) p self.view
(UIView *) $6 = 0x00007fb2a40344a0
(lldb) print self.view
(UIView *) $7 = 0x00007fb2a40344a0
(lldb) call self.view
(UIView *) $8 = 0x00007fb2a40344a0
(lldb) e self.view
(UIView *) $9 = 0x00007fb2a40344a0

根據唯一匹配原則,如果你沒有自己新增特殊的命令別名。e也可以表示expression的意思。原始命令預設沒有命令選項,所以e也能帶給你同樣的效果

po

我們知道,OC裡所有的物件都是用指標表示的,所以一般列印的時候,打印出來的是物件的指標,而不是物件本身。如果我們想列印物件。我們需要使用命令選項:-O。為了更方便的使用,LLDB為expression -O --定義了一個別名:po

(lldb) expression -- self.view
(UIView *) $13 = 0x00007fb2a40344a0
(lldb) expression -O -- self.view
<UIView: 0x7fb2a40344a0; frame = (0 0; 375 667); autoresize = W+H; layer = <CALayer: 0x7fb2a4018c80>>
(lldb) po self.view
<UIView: 0x7fb2a40344a0; frame = (0 0; 375 667); autoresize = W+H; layer = <CALayer: 0x7fb2a4018c80>>

還有其他很多命令選項,不過我們一般用得比較少,所以我就不具體的一一介紹了,如果想了解,在LLDB控制檯上輸入:help expression即可查到expression所有的資訊

thread

thread backtrace & bt

有時候我們想要了解執行緒堆疊資訊,可以使用thread backtrace
thread backtrace作用是將執行緒的堆疊打印出來。我們來看看他的語法

thread backtrace [-c <count>] [-s <frame-index>] [-e <boolean>]

thread backtrace後面跟的都是命令選項:

-c:設定列印堆疊的幀數(frame)
-s:設定從哪個幀(frame)開始列印
-e:是否顯示額外的回溯
實際上這些命令選項我們一般不需要使用。
e.g: 當發生crash的時候,我們可以使用thread backtrace檢視堆疊呼叫

(lldb) thread backtrace
* thread #1: tid = 0xdd42, 0x000000010afb380b libobjc.A.dylib`objc_msgSend + 11, queue = 'com.apple.main-thread', stop reason = EXC_BAD_ACCESS (code=EXC_I386_GPFLT)
    frame #0: 0x000000010afb380b libobjc.A.dylib`objc_msgSend + 11
  * frame #1: 0x000000010aa9f75e TLLDB`-[ViewController viewDidLoad](self=0x00007fa270e1f440, _cmd="viewDidLoad") + 174 at ViewController.m:23
    frame #2: 0x000000010ba67f98 UIKit`-[UIViewController loadViewIfRequired] + 1198
    frame #3: 0x000000010ba682e7 UIKit`-[UIViewController view] + 27
    frame #4: 0x000000010b93eab0 UIKit`-[UIWindow addRootViewControllerViewIfPossible] + 61
    frame #5: 0x000000010b93f199 UIKit`-[UIWindow _setHidden:forced:] + 282
    frame #6: 0x000000010b950c2e UIKit`-[UIWindow makeKeyAndVisible] + 42

我們可以看到crash發生在-[ViewController viewDidLoad]中的第23行,只需檢查這行程式碼是不是幹了什麼非法的事兒就可以了。

LLDB還為backtrace專門定義了一個別名:bt,他的效果與thread backtrace相同,如果你不想寫那麼長一串字母,直接寫下bt即可:

(lldb) bt

thread return

Debug的時候,也許會因為各種原因,我們不想讓程式碼執行某個方法,或者要直接返回一個想要的值。這時候就該thread return上場了。

thread return [<expr>]

thread return可以接受一個表示式,呼叫命令之後直接從當前的frame返回表示式的值。

e.g: 我們有一個someMethod方法,預設情況下是返回YES。我們想要讓他返回NO


我們只需在方法的開始位置加一個斷點,當程式中斷的時候,輸入命令即可:

(lldb) thread return NO

效果相當於在斷點位置直接呼叫return NO;,不會執行斷點後面的程式碼

c & n & s & finish

一般在除錯程式的時候,我們經常用到下面這4個按鈕:


用觸控板的孩子們可能會覺得點選這4個按鈕比較費勁。其實LLDB命令也可以完成上面的操作,而且如果不輸入命令,直接按Enter鍵,LLDB會自動執行上次的命令。按一下Enter就能達到我們想要的效果,有木有頓時感覺逼格滿滿的!!!
我們來看看對應這4個按鈕的LLDB命令:

  1. ccontinuethread continue: 這三個命令效果都等同於上圖中第一個按鈕的。表示程式繼續執行
  2. nnextthread step-over: 這三個命令效果等同於上圖第二個按鈕。表示單步執行
  3. sstepthread step-in: 這三個命令效果等同於上圖第三個按鈕。表示進入某個方法
  4. finishstep-out: 這兩個命令效果等同於第四個按鈕。表示直接走完當前方法,返回到上層frame

thread其他不常用的命令

thread 相關的還有其他一些不常用的命令,這裡就簡單介紹一下即可,如果需要了解更多,可以使用命令help thread查閱

  1. thread jump: 直接讓程式跳到某一行。由於ARC下編譯器實際插入了不少retain,release命令。跳過一些程式碼不執行很可能會造成物件記憶體混亂髮生crash。
  2. thread list: 列出所有的執行緒
  3. thread select: 選擇某個執行緒
  4. thread until: 傳入一個line的引數,讓程式執行到這行的時候暫停
  5. thread info: 輸出當前執行緒的資訊

frame

前面我們提到過很多次frame(幀)。可能有的朋友對frame這個概念還不太瞭解。隨便打個斷點


我們在控制檯上輸入命令bt,可以打印出來所有的frame。如果仔細觀察,這些frame和左邊紅框裡的堆疊是一致的。平時我們看到的左邊的堆疊就是frame。

frame variable

平時Debug的時候我們經常做的事就是檢視變數的值,通過frame variable命令,可以打印出當前frame的所有變數

(lldb) frame variable
(ViewController *) self = 0x00007fa158526e60
(SEL) _cmd = "text:"
(BOOL) ret = YES
(int) a = 3

可以看到,他將self,_cmd,ret,a等本地變數都列印了出來

如果我們要需要列印指定變數,也可以給frame variable傳入引數:

(lldb) frame variable self->_string
(NSString *) self->_string = nil

不過frame variable只接受變數作為引數,不接受表示式,也就是說我們無法使用frame variable self.string,因為self.string是呼叫stringgetter方法。所以一般列印指定變數,我更喜歡用p或者po

其他不常用命令

一般frame variable列印所有變數用得比較多,frame還有2個不怎麼常用的命令:

frame info: 檢視當前frame的資訊

(lldb) frame info
frame #0: 0x0000000101bf87d5 TLLDB`-[ViewController text:](self=0x00007fa158526e60, _cmd="text:", ret=YES) + 37 at ViewController.m:38

frame select: 選擇某個frame

(lldb) frame select 1
frame #1: 0x0000000101bf872e TLLDB`-[ViewController viewDidLoad](self=0x00007fa158526e60, _cmd="viewDidLoad") + 78 at ViewController.m:23
   20      
   21      - (void)viewDidLoad {
   22          [super viewDidLoad];
-> 23          [self text:YES];
   24          NSLog(@"1");
   25          NSLog(@"2");
   26          NSLog(@"3");

當我們選擇frame 1的時候,他會把frame1的資訊和程式碼打印出來。不過一般我都是直接在Xcode左邊點選某個frame,這樣更方便

breakpoint

除錯過程中,我們用得最多的可能就是斷點了。LLDB中的斷點命令也非常強大

breakpoint set

breakpoint set命令用於設定斷點,LLDB提供了很多種設定斷點的方式:

使用-n根據方法名設定斷點:

e.g: 我們想給所有類中的viewWillAppear:設定一個斷點:

    (lldb) breakpoint set -n viewWillAppear:
    Breakpoint 13: 33 locations.

使用-f指定檔案

e.g: 我們只需要給ViewController.m檔案中的viewDidLoad設定斷點:

    (lldb) breakpoint set -f ViewContr