你是否曾经苦恼于理解项目的代码,而去尝试打印一个变量的值?然后使用NSLog 并且每次必须重新编译,从头开始?但是不一定要这么做。你可以使用调试器。而且即使你已经知道如何使用调试器检查变量,它可以做的还有很多。
LLDB 是什么?
LLDB是Mac OS X上Xcode的默认调试器,支持再桌面和iOS设备和模拟器上调试C ,Objective-C和C++。它是新一代高性能调试器,它可以高效利用LLVM项目中的现有库,例如Clang表达式解析器和LLVM反汇编程序。
随着Xcode 5的发布,LLDB调试器已经取代了GDB,成为了Xcode工程中默认的调试器。它与LLVM编译器一起,带给我们更丰富的流程控制和数据检测的调试功能。LLDB为Xcode提供了底层调试环境,其中包括内嵌在Xcode IDE中的位于调试区域的控制面板。
Chisel 是facebook下一个开源LLDB命令集合。
与此同时,让我们以在调试器中打印变量来开始我们的旅程吧。
基础命令
这是一个简单加了断点的程序,程序会在这一行停止运行,并且控制台会被打开,允许我们和调试器交互。这时候我们应该打些什么命令呢?
帮助 help
最简单命令是 help
,它会列举出所有的命令。如果你忘记了一个命令是做什么的,或者想知道更多的话,你可以通过 help <command-name>
来了解更多细节,例如 help print
或者 help thread
。只需要在控制台 上图lldb字样的地方键入 help即可。
打印对象 print
打印值很简单;只要试试 print 命令:
LLDB 实际上会作前缀匹配。所以你也可以使用 prin,pri,或者 p。但你不能使用 pr,因为 LLDB 不能消除和 process 的歧义 (幸运的是 p 并没有歧义)。而print 则是expression --
的简写方式。
你可能还注意到了,结果中有个 $0。实际上你可以使用它来指向这个结果。试试 print $0 + 7,你会看到 130。任何以$符开头的东西都是存在于 LLDB 的命名空间的,它们是为了帮助你进行调试而存在的。
打印复杂对象时,print可能显得力不从心 ,我们想看的是对象的 description 方法的结果,这时可以使用 po
,po 其实是 e -o --
的别名。
甚至可以给print 指定不同的打印格式。它们都是以 print/<fmt>
或者简化的 p/<fmt>
格式书写。
1 | //默认的格式 |
修改对象 expression
如果想改变一个值怎么办?我们要用到的是 expression
这个方便的命令。
上图中修改了num的值,断点步进后可以看到NSLog的对应值已经发生了变化。
变量
现在你已经可以打印对象和简单类型,并且知道如何使用 expression 命令在调试器中修改它们了。现在让我们使用一些变量来减少输入量。就像你可以在 C 语言中用 int a = 0 来声明一个变量一样,你也可以在 LLDB 中做同样的事情。不过为了能使用声明的变量,变量必须以$符开头。
1 | (lldb) e int $a = 2 |
UI调试
因为全局变量是可访问的,可以像这样打印整个视图层级:
1 | (lldb) po [[[UIApplication sharedApplication] keyWindow] recursiveDescription] |
1、更新UI
就像上文变量中提到那样,我们可以拿到这个view:
(lldb) expression id $myView = (id)0x7fe5ac5069b0
尝试做一些修改:
(lldb) expression (void)[$myView setBackgroundColor:[UIColor redColor]]
但是只有程序继续运行之后才会看到界面的变化。因为改变的内容必须被发送到渲染服务中,然后显示才会被更新。
渲染服务实际上是一个另外的进程 (被称作 backboardd)。这就是说即使我们正在调试的内容所在的进程被打断了,backboardd 也还是继续运行着的。
这意味着你可以运行下面的命令,而不用继续运行程序:
(lldb) expression (void)[CATransaction flush]
这个时候就能看到背景颜色的改变了。
流程控制
通过xcode加断点调试时,调试条上回出现四个可以控制程序执行流程的按钮:
从左到右分别是 continue program execution 、 step over 、 step into 和 step out。
1、 continue program execution 按钮,会取消程序的暂停,允许程序正常执行 (要么一直执行下去,要么到达下一个断点)。
在 LLDB 中,你可以使用 process continue
或者 thread continue
命令来达到同样的效果。
2、 step over 按钮,会以黑盒的方式执行一行代码。如果所在这行代码是一个函数调用,那么就不会跳进这个函数,而是会执行这个函数,然后继续。
LLDB 则可以使用 thread step-over
,next
,或者 n
命令。
3、 step in按钮,可以跳进一个函数调用来调试或者检查程序的执行情况。
在LLDB中使用 thread step-in
,step
,或者 s 命令。注意,当前行不是函数调用时,next 和 step 效果是一样的。
4、step out按钮 ,如果你曾经不小心跳进一个函数,但实际上你想跳过它,常见的反应是重复的运行 n 直到函数返回。其实这种情况,step out 按钮是你的救世主。它会继续执行到下一个返回语句 (直到一个堆栈帧结束) 然后再次停止。
在LLDB中使用 thread step-out
,
thread return 使用help thread
可以看到这个比较实用的函数。它有一个可选参数,在执行时它会把可选参数加载进返回寄存器里,然后立刻执行返回命令,跳出当前栈帧。这意味这函数剩余的部分不会被执行。这会给 ARC 的引用计数造成一些问题,或者会使函数内的清理部分失效。但是在函数的开头执行这个命令,是个非常好的隔离这个函数,伪造返回值的方式 。
断点
Xcode在断点导航中提供了一系列工具创建和管理断点,我们可以来看LLDB中等价的命令,主要是breakpoint
命令。
1、查看 启用/禁用
上图是xcode查看断点的地方,点击断点会开启或关闭断点。对应的LLDB如下:
1 | //查看断点 命令输出列表显示每个逻辑断点都有一个整数标识 |
2、 创建/删除
在Xcode创建断点的方式一种是 直接在代码左边的行数出点击 即可创建断点。对应的LLDB如下:
1 | //在main.m的第24行创建断点 |
还有一种是在断点导航,点击左下角的加号按钮,选择Symbolic BreakPoint会出现:
对应的LLDB如下:
1 | //在一个符号 (C 语言函数) 上创建断点,而完全不用指定哪一行 |
在 [NSArray objectAtIndex:]
这个断点上,我们怎么能知道设置了什么呢?接下来我们可以用$arg1、$arg2等命令来打印出我们想要的信息。
在这里$arg1是指对象本身,$arg2是对象被调用的函数,po命令无法直接输出函数名,需要加上(SEL),$arg3是被赋给函数的参数。
3、 断点行为
在Xcode中邮件断点可以编辑添加action信息,你可以添加多个行为:
对应的LLDB如下:
1 | (lldb) breakpoint set -f main.m -l 31 |
执行断点后自动继续运行,允许你完全通过断点来修改程序!你可以在某一行停止,运行一个 expression 命令来改变变量,然后继续运行。
查看 线程/调用栈 状态
在进程停止后,LLDB会选择一个当前线程和线程中当前帧(frame)。很多检测状态的命令可以用于这个线程或帧。
为了检测进程的当前状态,可以从以下命令thread list
开始:
1 | (lldb) thread list |
星号(*)表示thread #1为当前线程。为了获取线程的跟踪栈,可以使用以下命令thread backtrace
:
1 | //默认为当前线程 也可以指定线程 : thread backtrace 2 |
如果想查看所有线程的调用栈,则可以使用以下命令:(lldb) thread backtrace all
检查帧参数和本地变量的最简便的方式是使用frame variable
命令:
1 | (int) argc = 1 |
如果没有指定任何变量名,则会显示所有参数和本地变量。如果指定参数名或变量名,则只打印指定的值。如:
1 | (lldb) frame variable self |
如果想查看另外一帧,可以使用frame select
命令,如下所示:
1 | (lldb) frame select 2 |
image
image指令是target module指令的缩写,借助它我们能够查看当前的Binary Images相关的信息。日常开发我们主要利用它寻址。image
命令的用法也挺多,首先可以用它来查看工程中使用的库,如下所示:
1 | (lldb) image list |
我们还可以用它来查找可执行文件或共享库的原始地址,这一点还是很有用的,当我们的程序崩溃时,我们可以使用这条命令来查找崩溃所在的具体位置,如下所示:
1 | NSArray *array = @[@1, @2]; |
根据以上信息,我们可以判断崩溃位置是在ViewController中,要想知道具体在哪一行,可以使用以下命令image lookup --address
:
1 | (lldb) image lookup --address 0x00000001019004ff |
可以看到,最后定位到了ViewController.m:23行,正是我们代码所在的位置。
image更多用法可以参考: Executable and Shared Library Query Commands。