调试:案例学习

662 查看

没人写的代码是完美无暇的,但调试代码我们却都应该有能力能做好。相比提供一个关于本话题的随机小建议,我更倾向于选择带你亲身经历一个 bug 修复的过程,这是一个 UIKit 的 bug,我会展示我用来理解,隔离,并最终解决这个问题的流程。

问题

我收到了一个 bug 反馈报告,当快速点击一个按钮来弹出一个 popover 并 dismiss 它的同时,父视图控制器也会被 dismiss。谢天谢地,还附上了一个截图示意,所以第一步 — 重现 bug — 已经被做到了:

 

0064cTs2jw1eq8wicbqncg30fx0latp4.gif

我的第一个猜测是,我们可能包含了 dismiss 视图控制器的代码,我们错误地 dismiss 了父视图控制器。然而,当使用 Xcode 集成的视图调试功能时,很明显有一个全局 UIDimmingView 作为 first responder 来响应点击事件:

0064cTs2jw1eq8wibj6gwj327y1hkar8.jpg

苹果在 Xcode 6 中添加了调试视图层次结构的功能,这一举动很可能是受到非常受欢迎的应用 Reveal 和 Spark Inspector 的启发。相对于 Xcode,它们在许多方面表现更好,功能更多。

使用 LLDB

在可视化调试出现之前,最常见的做法是在 LLDB 使用 po [[UIWindow keyWindow] recursiveDescription] 来检查层次结构。它可以以文本形式打印出完整的视图层次结构

类似于检查视图层次,我们也可以用 po [[[UIWindow keyWindow] rootViewController] _printHierarchy] 来检查视图控制器。这是一个苹果默默在 iOS 8 中为 UIViewController 添加的私有辅助方法 。

LLDB 非常强大并且可以脚本化。 Facebook 发布了一组名为 Chisel 的 Python 脚本集合 为日常调试提供了非常多的帮助。pviews 和 pvc 等价于视图和视图控制器的层次打印。Chisel 的视图控制器树和上面方法打印的很类似,但是同时还显示了视图的尺寸。
我通常用它来检查响应链,虽然你可以对你感兴趣的对象手动循环执行 nextResponder,或者添加一个类别辅助方法,但输入 presponder object 依旧是迄今为止最快的方法。

添加断点

我们首先要找出实际 dismiss 我们视图控制器的代码。最容易想到的是在 viewWillDisappear: 设置一个断点来进行调用栈跟踪:

利用 LLDB 的 bt 命令,你可以打印断点。bt all 可以达到一样的效果,区别在于会打印全部线程的状态,而不仅是当前的线程。

看看这个栈,我们注意到视图控制器已经被 dismiss 途中,因为这个方法是在预定的动画中被调用的,所以我们需要在更早的地方增加断点。在这个例子中,我们关注的是对于 -[UIViewController dismissViewControllerAnimated:completion:] 的调用。我们在 Xcode 的断点列表中添加一个符号断点,并且重新执行示例代码。

Xcode 的断点接口非常强大,它允许你添加条件,跳过计数,或者自定义动作,比如添加音效和自动继续等。虽然它们可以节省相当多的时间,但在这里我们不需要这些特性: