没人写的代码是完美无暇的,但调试代码我们却都应该有能力能做好。相比提供一个关于本话题的随机小建议,我更倾向于选择带你亲身经历一个 bug 修复的过程,这是一个 UIKit 的 bug,我会展示我用来理解,隔离,并最终解决这个问题的流程。
问题
我收到了一个 bug 反馈报告,当快速点击一个按钮来弹出一个 popover 并 dismiss 它的同时,父视图控制器也会被 dismiss。谢天谢地,还附上了一个截图示意,所以第一步 — 重现 bug — 已经被做到了:
我的第一个猜测是,我们可能包含了 dismiss 视图控制器的代码,我们错误地 dismiss 了父视图控制器。然而,当使用 Xcode 集成的视图调试功能时,很明显有一个全局 UIDimmingView
作为 first responder 来响应点击事件:
苹果在 Xcode 6 中添加了调试视图层次结构的功能,这一举动很可能是受到非常受欢迎的应用 Reveal 和 Spark Inspector 的启发。相对于 Xcode,它们在许多方面表现更好,功能更多。
使用 LLDB
在可视化调试出现之前,最常见的做法是在 LLDB 使用 po [[UIWindow keyWindow] recursiveDescription]
来检查层次结构。它可以以文本形式打印出完整的视图层次结构。
类似于检查视图层次,我们也可以用 po [[[UIWindow keyWindow] rootViewController] _printHierarchy]
来检查视图控制器。这是一个苹果默默在 iOS 8 中为 UIViewController
添加的私有辅助方法 。
1 2 3 4 5 6 7 8 9 |
(lldb) po [[[UIWindow keyWindow] rootViewController] _printHierarchy] <PSPDFNavigationController 0x7d025000>, state: disappeared, view: <UILayoutContainerView 0x7b3218d0> not in the window | <PSCatalogViewController 0x7b3100d0>, state: disappeared, view: <UITableView 0x7c878800> not in the window + <UINavigationController 0x8012c5d0>, state: appeared, view: <UILayoutContainerView 0x8012b7a0>, presented with: <_UIFullscreenPresentationController 0x80116c00> | | <PSPDFViewController 0x7d05ae00>, state: appeared, view: <PSPDFViewControllerView 0x80129640> | | | <PSPDFContinuousScrollViewController 0x7defa8e0>, state: appeared, view: <UIView 0x7def1ce0> | + <PSPDFNavigationController 0x7d21a800>, state: appeared, view: <UILayoutContainerView 0x8017b490>, presented with: <UIPopoverPresentationController 0x7f598c60> | | | <PSPDFContainerViewController 0x8017ac40>, state: appeared, view: <UIView 0x7f5a1380> | | | | <PSPDFStampViewController 0x8016b6e0>, state: appeared, view: <UIView 0x7f3dbb90> |
LLDB 非常强大并且可以脚本化。 Facebook 发布了一组名为 Chisel 的 Python 脚本集合 为日常调试提供了非常多的帮助。pviews
和 pvc
等价于视图和视图控制器的层次打印。Chisel 的视图控制器树和上面方法打印的很类似,但是同时还显示了视图的尺寸。
我通常用它来检查响应链,虽然你可以对你感兴趣的对象手动循环执行 nextResponder
,或者添加一个类别辅助方法,但输入 presponder object
依旧是迄今为止最快的方法。
添加断点
我们首先要找出实际 dismiss 我们视图控制器的代码。最容易想到的是在 viewWillDisappear:
设置一个断点来进行调用栈跟踪:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
(lldb) bt * thread #1: tid = 0x1039b3, 0x004fab75 PSPDFCatalog`-[PSPDFViewController viewWillDisappear:](self=0x7f354400, _cmd=0x03b817bf, animated='\x01') + 85 at PSPDFViewController.m:359, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1 * frame #0: 0x004fab75 PSPDFCatalog`-[PSPDFViewController viewWillDisappear:](self=0x7f354400, _cmd=0x03b817bf, animated='\x01') + 85 at PSPDFViewController.m:359 frame #1: 0x033ac782 UIKit`-[UIViewController _setViewAppearState:isAnimating:] + 706 frame #2: 0x033acdf4 UIKit`-[UIViewController __viewWillDisappear:] + 106 frame #3: 0x033d9a62 UIKit`-[UINavigationController viewWillDisappear:] + 115 frame #4: 0x033ac782 UIKit`-[UIViewController _setViewAppearState:isAnimating:] + 706 frame #5: 0x033acdf4 UIKit`-[UIViewController __viewWillDisappear:] + 106 frame #6: 0x033c46a1 UIKit`-[UIViewController(UIContainerViewControllerProtectedMethods) beginAppearanceTransition:animated:] + 200 frame #7: 0x03380ad8 UIKit`__56-[UIPresentationController runTransitionForCurrentState]_block_invoke + 594 frame #8: 0x033b47ab UIKit`__40+[UIViewController _scheduleTransition:]_block_invoke + 18 frame #9: 0x0327a0ce UIKit`___afterCACommitHandler_block_invoke + 15 frame #10: 0x0327a079 UIKit`_applyBlockToCFArrayCopiedToStack + 415 frame #11: 0x03279e8e UIKit`_afterCACommitHandler + 545 frame #12: 0x060669de CoreFoundation`__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__ + 30 frame #20: 0x032508b6 UIKit`UIApplicationMain + 1526 frame #21: 0x000a119d PSPDFCatalog`main(argc=1, argv=0xbffcd65c) + 141 at main.m:15 (lldb) |
利用 LLDB 的 bt
命令,你可以打印断点。bt all
可以达到一样的效果,区别在于会打印全部线程的状态,而不仅是当前的线程。
看看这个栈,我们注意到视图控制器已经被 dismiss 途中,因为这个方法是在预定的动画中被调用的,所以我们需要在更早的地方增加断点。在这个例子中,我们关注的是对于 -[UIViewController dismissViewControllerAnimated:completion:]
的调用。我们在 Xcode 的断点列表中添加一个符号断点,并且重新执行示例代码。
Xcode 的断点接口非常强大,它允许你添加条件,跳过计数,或者自定义动作,比如添加音效和自动继续等。虽然它们可以节省相当多的时间,但在这里我们不需要这些特性:
1 2 3 s="crayon-nums " data-settings="show">
1 2 3 我用来理解,隔离,并最终解决这个问题的流程。
问题我收到了一个 bug 反馈报告,当快速点击一个按钮来弹出一个 popover 并 dismiss 它的同时,父视图控制器也会被 dismiss。谢天谢地,还附上了一个截图示意,所以第一步 — 重现 bug — 已经被做到了:
我的第一个猜测是,我们可能包含了 dismiss 视图控制器的代码,我们错误地 dismiss 了父视图控制器。然而,当使用 Xcode 集成的视图调试功能时,很明显有一个全局 苹果在 Xcode 6 中添加了调试视图层次结构的功能,这一举动很可能是受到非常受欢迎的应用 Reveal 和 Spark Inspector 的启发。相对于 Xcode,它们在许多方面表现更好,功能更多。 使用 LLDB在可视化调试出现之前,最常见的做法是在 LLDB 使用 类似于检查视图层次,我们也可以用
LLDB 非常强大并且可以脚本化。 Facebook 发布了一组名为 Chisel 的 Python 脚本集合 为日常调试提供了非常多的帮助。 添加断点我们首先要找出实际 dismiss 我们视图控制器的代码。最容易想到的是在
利用 LLDB 的 看看这个栈,我们注意到视图控制器已经被 dismiss 途中,因为这个方法是在预定的动画中被调用的,所以我们需要在更早的地方增加断点。在这个例子中,我们关注的是对于 Xcode 的断点接口非常强大,它允许你添加条件,跳过计数,或者自定义动作,比如添加音效和自动继续等。虽然它们可以节省相当多的时间,但在这里我们不需要这些特性:
|