你已经学习了如何创建断点, 如何打印和修改值, 同时还有当调试器停下来的时候如何执行代码. 但是你还没有学会如何在调试器中自由切换和检查数据.是时候学习一下了! 在本章中, 你将会学习到当LLDB暂停的时候如何在调试器的函数里和函数外自由切换. 这是一个重要的技能因为当值改变的时候你经常想要检查代码片段里面或者外面的值.

栈 101

当电脑在执行应用程序的时候, 它会将值存储在栈和堆里.两者都有各自的优点.作为一个高级调试者, 你要好好的理解他们是如何工作的.现在, 让我们简单的看一下栈. 你可能在计算机相关的知识里看了很多关于栈的知识.在任何情况下, 它都需要有一个最基本的理解一个线程在执行的时候是如何追踪代码和变量的. 这些知识可以让你方便的使用LLDB在代码里面游走. 栈是LIFO(Last-In-First-Out)用后进先出的队列来存储你当前执行的代码的引用. LIFO的顺序意味着无论你最后加入了什么都会最先移除.将栈想象成一碟盘子.在顶部加一个盘子, 它将会首先被你拿走. 栈指针指向当前栈的顶部. 在盘子的类比中, 栈指针指向盘子的顶部, 告诉你下一次从哪里取盘子, 或者下一次把盘子放在哪里.

page69image15224.png

在这张图表中, 最高的地址显示在顶部(0xFFFFFFFF)并且较低的地址显示在底部(0x00000000)展示的栈是向下生长的. 有些图表的高地址在底部与盘子的类比是相匹配的, 栈是向下生长的.然而, 我相信任何图表展示的栈都应该是从一个高地址向下生长的否则的话稍后我们在讨论栈指针的偏移的时候会遇到一些头疼的问题. 你会在第十二章看到一些栈指针和寄存器的一些更深入的问题“Assembly and the Stack”, 但是在本章中你将浏览在栈中的代码里步进的几种不同的方法.

检查栈的帧

在本章中你将依然使用Signals项目. 在本章中你会用到一点汇编的知识.不要怕!并没有想象的那么糟糕.但是, 要确保在本章中你用的是iPhone 7的模拟器因为如果是在iOS真机上的话生成的汇编代码可能有些不同.这是因为真机用的是ARM 架构, 模拟器使用的是你Mac本地的指令集 x86_64. 在Xcode中打开Signals项目.接下来, 在下面的函数名字处添加一个符号断点.确保在函数标志处添加了空格否则断点将无法识别这写符号.

Signals.MasterViewController.viewWillAppear (Swift.Bool) -> () 这行代码在MasterViewController’s viewWillAppear(_:)方法处创建了一个符号断点. page70image17024.png

构建并运行程序. 正如期望的那样, 程序将会在MasterViewController的viewWillAppear(_:)方法处停下来.接下来, 在Xcode的左边找到栈追踪面板.如果你还没有看到, 点击左边面板的Debug Navigator或者按 Command + 6快捷键. 确保底部右下角的三个按钮都是禁用状态.这些按钮是用来帮助你过滤那些只在你的源代码里出现的函数的.鉴于你既要学习公有的代码又要学习私有的代码, 你应该总是保持这些按钮处于禁用状态以便你可以看到栈追踪的完整信息. page71image1232.png

在调试的导航面板中, 栈追踪面板将会出现, 并显示出栈帧的列表, 第一个就是viewWillAppear(_:)函数. 紧跟着的是一个Swift/ Objective-C 桥接方法,@objc MasterViewController.viewWillAppear(Bool) - > ():. 这个方法是自动生成的因此Objective-C可以进入Swift代码的内部. 在这后面, 有一些UIKit的Objective-C代码的栈帧.再往下一点, 你会看到一些属于CoreAnimation的C++的代码.更深入一点, 你会看到包含在属于CoreFoundation的CFRunLoop的一组方法.最后, 是main函数来做结尾的(是的, Swift程序仍然有main函数, 只不过是隐藏起来了而已). 你在Xcode中看到的栈追踪到的信息就是LLDB可以告诉你的内容的一个样板. 现在我们来看一下. 在LLDB控制台中输入以下内容: (lldb) thread backtrace 你也可以简单的输入bt, 也可以达到同样的效果. 他们实际上是两个不同的命令, 你也可以同过你可信赖的朋友help来查看他们的不同. 执行了上面的命令之后, 你将会看到一个与你再Xcode的调试栏里一致的栈追踪信息. 在LLDB控制台中输入下面的指令:

(lldb) frame info 你会得到一些类似下面的输出:

frame #0: 0x00000001075d0ae0 Signals`MasterViewController.viewWillAppear(animated=<invalid> (0xd1), self=0x00007fff5862dac0) -> () at MasterViewController.swift:47 正如你看到的, 这个输出与你在调试栏里看到的是一致的.所以这就是为什么你可以在调试栏里看到相当重要的一切?好, 使用LLDB控制台给你的细分度来控制你想查看的信息.此外, 你制作的自定义的LLDB脚本会让这些命令变得非常有用.我们也知道了Xcode是从哪里获取的信息, 对吧? 让我们回到调试栏里看一下, 你会在调用的栈里面看到一些从0开始递增的数字.这些数字可以帮助你记住你正在查看的栈帧. 输入下面的命令选择一个栈:

(lldb) frame select 1 Xcode将会调到@objc的桥接方法里, 这个方法在栈中的编号是1. 假如你使用的是模拟器而不是一个真机, 你将会得到一些跟下面看起来类似的汇编:

page72image14976.png 注意看汇编中的绿线. 右前方的那条线是callq指令它代表着你之前在执行viewWillAppear(_:)时设置的断点.

在掌握LLDB的时候, 在程序暂停的时候你可以做的最重要的三个导航动作就是在程序中步进.通过LLDB, 你可以在代码中步过, 步入和步出. 它们当中的每一个都可以让你继续执行程序的代码, 但是在一个小的整体中可以让你检测程序的代码是如何执行的.

步过

步过允许你执行调试器当前暂停位置的下一条代码语句(通常是下一行).这就意味着如果当前语句调用了另外一个函数, LLDB将会继续运行直到这个函数执行完毕并返回. 让我来实际看一下. 在LLDB控制台中输入下面的代码:

(lldb) run 这会在Xcode不用重新编译项目的情况下重新启动Signals程序.Xcode会在你之前创建的符号断点出停下来. 接下来, 输入下面的内容:

(lldb) next 调试器会向前移动一行代码. 这就是步过.简单, 但是有用!

步入

步入意味着如果下一条语句调用了一个函数, 调试器会移动到这个函数的起始位置并再次暂停. 让我们实际看一下. 再次从LLDB中启动断点程序:

(lldb) run 接下来输入:

(lldb) step 不幸的是. 程序已经步入了, 因为这行代码包含了一个函数调用. 在这种情况下, LLDB实际上更像是用“step over”代替了“step into”. 这是因为LLDB在默认情况下会忽略步进一个函数如果这个函数里面没有调试符号的话.在这里, 调用的是UIKit里的函数, 在那里你并没有调试符号. 然而这里却又一个方法来设置LLDB的行为当步入一个没有调试符号的函数的时候.在LLDB中执行下面这条命令并查看这条指令的作用:

(lldb) settings show target.process.thread.step-in-avoid-nodebug 如果为真, 在这些实例上步入实际上被当做步过来执行.你也可以改变这个设置(这是你后面做的), 或者告诉调试器忽略这个设置(这是你现在做的). 在LLDB控制台中输入下面的命令:

(lldb) step -a0 这条指令告诉LLDB无论是否有调试符号都执行步入操作.

步出

步出意味着函数将会继续执行然后当它返回的时候暂停.从栈的视角看, 继续执行直到栈帧被弹出. 再次运行Signals项目, 这次当调试器暂停的时候, 快速的看一下栈追踪情况.接下来, 在LLDB中输入下面的命令:

(lldb) finish 你会注意到调试器现在暂停在一个栈追踪到的函数上. 试着多执行几次这个命令.记住, 在简单的按下Enter键的同时, LLDB会执行你最后一次输入的代码. finish命令会通知LLDB步出当前函数. 对左侧面板中一个挨着一个的栈帧要多一些耐心.

在Xcode中步进

尽管你已经知道了很多使用控制台控制的细分指令, Xcode已经为你提供了这些选项就是LLDB控制台上面的按钮. 当一个程序运行的时候这些按钮就会出现.

page74image17480.png

他们的顺序是步过, 步入和步过. 最后, 步过和步入还有更多的功能. 你可以手动控制执行不同的线程, 通过在点击这些按钮的时候按下Control和Shift. 这样做的结果是步过调试器当前暂停的线程, 而其余的线程仍然暂停.这是一个非常有用的技巧当你调试一些很难调试的并发代码的时候像网络请求或者GCD代码的时候. 当然LLDB在控制台中通过使用--run-mode选项来做同样的事情, 或者更简单的用-m跟随合适的选项. 检测栈中的数据

frame命令中一个非常有趣的命令是frame variable子命令.这个命令将会获取在你执行的头文件中发现的调试符号信息并将指定栈帧的信息提取出来.感谢调试信息, frame variable命令通过合适的选项可以简单的告诉你在你的函数中所有变量的范围以及任何在你程序中的全局变量. 再次运行Signals项目并且确保你已经触发了viewWillAppear(_:)中的断点.接下来 找到栈的顶部即可以通过点击Xcode调试栏里的顶部也可以通过在控制台中输入 frame select 0. 接下来, 输入下面的内容:

(lldb) frame variable 你会看到类似下面的输出:

(Bool) animated = false (Signals.MasterViewController) self = 0x00007fb3d160aad0 { UIKit.UITableViewController = { baseUIViewController@0 = <extracting data from value failed> _tableViewStyle = 0 _keyboardSupport = nil _staticDataSource = nil _filteredDataSource = 0x000061800005f0b0 _filteredDataType = 0 } detailViewController = nil } 这回提取出当前栈帧可用的变量和代码. 如果可能的话, 他也会从当前的可用变量中提取出所有的实例变量, 既有公有变量也有私有变量. 如果你是善于观察的读者, 你也许会注意到frame variable的输出与控制台窗口中左侧面板中的变量视图的的内容是一致的. 如果没有看到变量视图, 你可以通过点击Xcode右下角的显示左侧面板的按钮来展开变量视图.你可以将frame variable的输出与变量视图的内容做一个对比. 你会注意到frame variable与使用苹果的私有API的变量视图相比给你提供了变量的更多的信息. page76image11200.png

接下来, 输入下面的内容: (lldb) frame variable -F self 这是一个查看MasterViewController所有可用的私有变量的简单方式.它用到了-F选项, 代表着flat. 这将会保持0缩进并且仅仅打印出This will keep the indentation to 0中关于self的信息. 你将会得到一些类似与下面的输出:

self = 0x00007fff5540eb40 self = self = self = self = {} self.detailViewController = 0x00007fc728816e00 self.detailViewController.some = self.detailViewController.some = self.detailViewController.some = {} self.detailViewController.some.signal = 0x00007fc728509de0 正如你看到的, 在处理苹果的框架的时候这是一种很有吸引力的方式.

我们为什么要学这些?

在本章中你学到了浏览栈帧和他们的内容. 你也学到了如何使用步入, 步出和步过来在代码中切换. thread命令还有许多你没有发现的选项.尝试通过help thread命令来了解他们, 看看你是否能学到一些很酷的命令. 花点时间看一下thread until, thread jump和thread return命令.你再后面会用到它们, 你现在也可以先了解一下它们的作用.