lldb 调试方法

偶然看到使用 lldb 里的 watchpoint 调试,感觉很神奇,就详细了解了下 lldb 调试的使用方法。 查阅了许多文章与调试器共舞 - LLDB 的华尔兹这篇文章帮助最大。

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

LLDB控制台平时会输出一些log信息(Debugger output中)。如果我们想输入命令调试,必须让程序进入暂停状态。让程序进入暂停状态的方式主要有2种:

  1. 断点或者watchpoint: 在代码中设置一个断点(watchpoint),当程序运行到断点位置的时候,会进入stop状态
  2. 直接暂停,控制台上方有一个暂停按钮,上图红框已标出,点击即可暂停程序

不过最好还是不要直接暂停,因为这种情况下的当前作用域没有任何变量,即使进入了 lldb 也做不了什么事。

expression

expression的几个命令是最常用的,能提升debug效率的命令。

print

打印一个对象:
print
可以直接缩写p达到同样的效果。

结果中有个$0,可以使用它指向这个结果。lldb 中可以创建对象,但是这个对象的命名必须要以$开头,表示命名空间是lldb,以免和外部代码中的名字冲突,如:
print2

expression

使用

如果想要改变一个值,或者执行一个方法。可以使用expression或者e
expression

print其实是特殊的expression,比如p count =18其结果和expression count = 18结果一样。事实上,printexpression -- 的缩写。

应用

由于expression可以改变值,我们可以动态的改变View的颜色,而不需要重新编译:

1
2
3
4
// 改变颜色
(lldb) expression self.view.backgroundColor = [UIColor redColor]
// 刷新界面
(lldb) expression [CATransaction flush]

不只是改变颜色,frame,animation等都能改变,非常有用。

如最开始所说,不能直接暂停,必须要在当前界面中触发断点,比如重写touch方法添加断点等。

po

print输出比较啰嗦,而且当尝试打印复杂对象的时候,有时候会很蛋疼:

1
2
3
(lldb) p @[ @"foo", @"bar" ]

(NSArray *) $8 = 0x00007fdb9b71b3e0 @"2 objects"

这个时候可以使用po(print object 的缩写),相当于调用对象的description方法。

1
2
3
4
5
6
7
(lldb) po $8
<__NSArrayI 0x7fdb9b71b3e0>(
foo,
bar
)
(lldb) po @"lunar"
lunar

thread

bt

可以打栈中的所有帧(frame)。这些frame和左边红框里的堆栈是一致的。平时我们看到的左边的堆栈就是frame:
frame

thread return

这也是比较有用的一个命令。Debug的时候,也许会因为各种原因,我们不想让代码执行某个方法,或者要直接返回一个想要的值。这时候就该thread return上场了。

它有一个可选参数,在执行时它会把可选参数加载进返回寄存器里,然后立刻执行返回命令,跳出当前栈帧。这意味这函数剩余的部分不会被执行。这会给 ARC 的引用计数造成一些问题,或者会使函数内的清理部分失效。但是在函数的开头执行这个命令,是个非常好的隔离这个函数,伪造返回值的方式 。

比如下图所示:
return
正常通过isOdd方法,一定是return NO的,但是通过(lldb) thread return YES,跳过了isOdd方法里的方法块,直接return YES

正如上面所述,必须要在断点进行到如图所示的地方才可以使用thread return,因为:

  1. 此时当前栈帧是isOdd
  2. 不会造成引用问题

breakpoint

断点当然是调试必不可少的东西,下面不会说明怎样添加断点,而是将列举一些断点的使用技巧.

流程控制

当插入断点后,程序到达断点时会就会停止运行。调试条上会出现如下四个按钮:
debug
意义都很明确,不多说了,列举下对应的命令:

  1. continue按钮 => continue,缩写c
  2. step over按钮 => next,缩写n
  3. step in按钮 => step,缩写s
  4. step out按钮 => finish

设置断点

在断点处右击,选择Edit Breakpoint弹出如下设置框:
breakpoint

蓝色的勾

表示enable和disable断点。

Condition

指的是条件表达式,该项允许我们对断点生效设置条件,表示当满足某一特定条件的前提下,该断点才生效。(该条件的录入,不能够识别预处理的宏定义,也不能识别断点作用域之外的变量和方法)。

Ignore

忽略次数。它指定了在断点生效,应用暂停之前,代码忽略断点的次数。你如果希望应用运行一段时间后断点才生效,那么就可以使用这个选项。比如说在调试某一循环体的时候。

Action

它表示当断点生效时,Xcode作出反应后的行为动作。点击右边的Add Action选项会弹出如下菜单:
menu
默认的是Debugger Command。还有以下几种动作供选择,下面将介绍两个常用的:

  1. Debugger Command:默认的选项,可以让断点执行LLDB调试命令。
  2. Log Message:可以生成消息队列,将相关的消息输出到控制台上.随便写什么都能输出,输出的对象需要使用@exp@形式。

Options

一个选项,勾上可以在运行到断点后不暂停,自动向下运行,配合Action相当于打了断点。

断点类型

异常断点

异常断点是代码出现问题导致编译器抛出异常时触发的断点。它在断点导航器中设置。点击+号,选择Exception Breakpoint选项。如下图:
exception

符号断点

他可以中断某个方法的调用,可谓是异常强大,在断点导航器界面,点击+号,选择Add Symbolic Breakpoint选项,然后会弹出如图所示的对话框:
exception

它比普通断点的自定义设置界面多出了两个内容

  • Symbol:用来设置当前断点作用域所能识别的方法,这里面既可以是自定义的方法,也可以是系统的API方法。(注意必须表明是类方法还是成员方法)例如:

    1
    2
    -[MyViewController viewDidAppear:]
    +[MyViewController sharedInstance]
  • Module:模组的意思,用来限制满足符号的方法,编译器将只会在断点满足这个模组的符号的时候才回暂停。

需要注意的是,如果一个子类没有重写父类的方法,那么设置子类这个方法的符号断点是不会被触发的。

watchpoint

一般情况下可以在属性的 set 方法中添加断点,这样就能监控属性的设置。但是有时候,由于不涉及到方法,而是直接操作内存,无法使用断点,但是我们仍要监视某一个值是否变化,这个时候就可以用watchpoint来监视。

当指针指向变化时,watchpoint会触发:
watchpoint

set

添加watchpoint的方式如上图所示

1
(lldb) watchpoint set variable xxx

注意,貌似不能用 self->xxx 的形式,而要直接用 _xxx 的形式,否则一直都是下面的错误:
watchpoint2

watchpoint 不只是监听指针变化,还能监听变量变化。只要该指针或变量进行了赋值,无论是否改变了值,都会使 watchpoint 命中。

disable/delete/enable

watchpoint资源也是比较有限的,对于不需要监听的对象要及时释放。每个watchpoint都有一个序号,操作对应的需要即可:
watchpoint3

其他技巧

run

这是我比较喜欢的方法,所以单独列出来。

调试的时候经常需要重新启动程序。但是如果重新Run程序,需要重新编译,非常浪费时间。可以在 lldb 中输入run就能直接让程序重新加载了。

代替NSLog

NSLog的文档,第一句话就说:Logs an error message to the Apple System Log facility.,所以首先,NSLog就不是设计作为普通的debug log的,而是error log;每次NSLog都会进行很多耗时工作,因此,非重要参数尽量不要用,使用调试代替。