LLDB

作者: CoderSC | 来源:发表于2017-08-29 10:15 被阅读0次

    LLDB的Xcode默认的调试器,它与LLVM编译器一起,带给我们更丰富的流程控制和数据检测的调试功能。平时用Xcode运行程序,实际走的都是LLDB。熟练使用LLDB,可以让你debug事半功倍

    LLDB基础知识

    LLDB控制台

    Xcode中内嵌了LLDB控制台,在Xcode中代码的下方,我们可以看到LLDB控制台。


    LLDB控制台平时会输出一些log信息。如果我们想输入命令调试,必须让程序进入暂停状态。让程序进入暂停状态的方式主要有2种:
    断点或者watchpoint: 在代码中设置一个断点(watchpoint),当程序运行到断点位置的时候,会进入stop状态
    直接暂停,控制台上方有一个暂停按钮,上图红框已标出,点击即可暂停程序

    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

    这个命令对应到上面的语法就是:
    command:breakpoint表示断点命令
    action:set表示设置断点
    option:-n表示根据方法name设置断点
    arguement:mian表示方法名为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还是用得比较少的,更多时候我们用的是p
    printcall。这三个命令其实都是expression --的别名(--表示不再接受令选项)

    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.c/continue/thread continue: 这三个命令效果都等同于上图中第一个按钮的。表示程序继续运行
    2.n/next/thread step-over: 这三个命令效果等同于上图第二个按钮。表示单步运行
    3.s/step/thread step-in: 这三个命令效果等同于上图第三个按钮。表示进入某个方法
    4.finish/step-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
    是调用string
    的getter
    方法。所以一般打印指定变量,我更喜欢用p
    或者po

    其他不常用命令

    一般frame variable打印所有变量用得比较多
    frame还有2个不怎么常用的命令:
    frame info: 查看当前frame的信息
    (lldb) frame infoframe #0: 0x0000000101bf87d5 TLLDB`-[ViewController text:](self=0x00007fa158526e60, _cmd="text:", ret=YES) + 37 at ViewController.m:38
    frame select: 选择某个frame
    (lldb) frame select 1frame #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,这样更方便

    相关文章

      网友评论

          本文标题:LLDB

          本文链接:https://www.haomeiwen.com/subject/oifodxtx.html