资讯专栏INFORMATION COLUMN

iOS/OSX 调试:用例调研

dreambei / 3425人阅读

摘要:这个工程的真相是调用了比之苹果提供给我们更为完整的头文件。你应该总是正确记录这些解决方法,最重要的是,在苹果文档中设置一个雷达。

原文链接:http://www.objc.io/issue-19/debugging-case-study.html

// 速翻,无校对版
我是前言

代码世界也不存在圣人,所以调试也是我们大家所必备的良好技能。不是乱枪打鸟,我将会看看回归到UIKit中的BUG,并告诉你我以前理解,隔离,并最终解决问题的工作流程。

问题来了

我们收到一个BUG报告:快速电议按钮弹出一个popover,popover消失掉以后尼玛父视图控制器一同消失掉了。幸运的是,栗子还在,那么让我们开始第一步 - 重现这个BUG - 需要注意的是:

我的第一感觉是我们可以在代码中主动消失了视图控制器,并且我们错误地让父视图控制器消失了。但是,当使用Xcode的集成视图调试功能调试的时候,很明显这里有一个全局的UIDimmingView作为外界触点事件输入源的第一响应者:

大苹果在Xcode 6中增加了调试视图图层特性,这个特性可能是被现下流行的Reveal和Spark Inspector应用所激发的灵感,当然上述两款应用在许多方面还是完爆Xcode.

使用LLDB

在可视化调试以前,通常的作法是在LLDB中键入po [[UIWindow keyWindow] recursiveDescription]用文字形式打印出图层的关系来检视视图层级。

与检视视图层级类似的,我们也需要通过[[[UIWindow keyWindow] rootViewController] _printHierarchy]检视视图控制器的层级。这是大苹果在iOS8中针对增加UIViewController添加隐藏的私有帮助指令:

(lldb) po [[[UIWindow keyWindow] rootViewController] _printHierarchy]
, state: disappeared, view:  not in the window
   | , state: disappeared, view:  not in the window
   + , state: appeared, view: , presented with: <_UIFullscreenPresentationController 0x80116c00>
   |    | , state: appeared, view: 
   |    |    | , state: appeared, view: 
   |    + , state: appeared, view: , presented with: 
   |    |    | , state: appeared, view: 
   |    |    |    | , state: appeared, view: 

LLDB功能强劲并可以脚本化。FB放出了一系列的叫做Chisel的大python脚本来帮助日常的调试工作。pviewspvc对于视图与视图控制器的层级打印来说是等价的。Chisel的视图控制器树也类似,但是同时展示视图的边框属性。我通常是使用它来检视响应者链,并且尽管你可以手动循环找出你所感兴趣对象的下一个响应者,或是可以添加一个扩展指令,总归键入presponder object是目前为止最便捷的方式。

添加断点

让我们好好琢磨下是什么代码让我们的试图控制器消失得。最明显的方式是设置一个断点打在viewWillDisappear:来看看函数栈的记录:

(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功能一样,其会打印出所有线程的状态,而不是仅仅当前线程。

审视函数栈记录,我们发现当我们调用一个设计排好的动画的时候视图控制器便已经消失了,搜易我们需要添加一个更早的断点。在这种情况下,我们对-[UIViewController dismissViewControllerAnimated:completion:]这个方法调用比较感冒。我们添加了一个断点并继续跑代码。

Xcode断点接口功能同样强劲,让你添加所有条件,忽略次数,或者甚至是自定义的事件比如说播放一个声音效果和自动运行。这里我们用不到这些特性,但是他们着实可以节省我们不少时间:

(lldb) bt
* thread #1: tid = 0x1039b3, 0x033bb685 UIKit`-[UIViewController dismissViewControllerAnimated:completion:], queue = "com.apple.main-thread", stop reason = breakpoint 7.1
  * frame #0: 0x033bb685 UIKit`-[UIViewController dismissViewControllerAnimated:completion:]
    frame #1: 0x03a7da2c UIKit`-[UIPopoverPresentationController dimmingViewWasTapped:] + 244
    frame #2: 0x036153ed UIKit`-[UIDimmingView handleSingleTap:] + 118
    frame #3: 0x03691287 UIKit`_UIGestureRecognizerSendActions + 327
    frame #4: 0x0368fb04 UIKit`-[UIGestureRecognizer _updateGestureWithEvent:buttonEvent:] + 561
    frame #5: 0x03691b4d UIKit`-[UIGestureRecognizer _delayedUpdateGesture] + 60
    frame #6: 0x036954ca UIKit`___UIGestureRecognizerUpdate_block_invoke661 + 57
    frame #7: 0x0369538d UIKit`_UIGestureRecognizerRemoveObjectsFromArrayAndApplyBlocks + 317
    frame #8: 0x03689296 UIKit`_UIGestureRecognizerUpdate + 3720
    frame #9: 0x032a226b UIKit`-[UIWindow _sendGesturesForEvent:] + 1356
    frame #10: 0x032a30cf UIKit`-[UIWindow sendEvent:] + 769
    frame #21: 0x032508b6 UIKit`UIApplicationMain + 1526
    frame #22: 0x000a119d PSPDFCatalog`main(argc=1, argv=0xbffcd65c) + 141 at main.m:15

继续说!跟预期的一样,满屏UIDimmingView截获到我们的想触碰以及处理句柄handleSingleTap:,然后将它丢给了UIPopoverPresentationController的dimmingViewWasTapped:,它(应该)执行了消失视图控制器的动作。但是,当我们快速点击的时候,这个断点停了两次。这里是不是有第二个隐藏(未被发现)的图层呢?还是说调用了同一个实例?我们只能进入这个断点,这时po self就没啥用了。

Calling Conventions 101

根据组装跟功能调用约定的基本常识,我们还是能获取self的值。在iOS模拟器中的iOS ABI Function Call Guide和Mac OS X ABI Function Call Guide是两大资源。

我们知道每个OC方法中有两个隐藏参数:self_cmd。所以我们需要栈上的第一个对象。针对32位指令集架构,保存在栈上的使用$esp,所以你可以使用po *(int*)($esp+4)来获取self,还有使用p (SEL)*(int*)($esp+8)来获取OC方法中的_cmd。在$esp中的第一个值返回的是一个地址。接下来的值存在$esp+12, $esp+16类似等等。

X86-64体系提供了更多得寄存器,所以变量被放置在$rdi, $rsi, $rdx, $rxc, $r8, $r9。所有之后的栈上变量存储在$rbp,从$rbp+16,$rbp+24等。

armv7体系总的来说将变量丢在$r0, $r1, $r2, $r3,然后将剩余的丢到栈上$sp:

(lldb) po $r0
 page:0>

(lldb) p (SEL)$r1
(SEL) $1 = "dismissViewControllerAnimated:completion:"

arm64与armv7相似,然后,因为有了更多可用的寄存器, $x0到$x7全部用来存变量,而不是放在栈寄存器$sp上。

你可以学习更多X86与X86-64中关于栈的知识,同时还可以阅读AMD64 ABI Draft。

使用Runtime

另外一个技巧来追溯方法执行是使用方法重写成在调用父类方法之前进行带日志打印的模式。但是,手动的进行一个实际方法切换(swizzling)仅仅只是做到调试起来比较方便但是做不到时间节省。前阵子,我写了一个小方法库叫做Aspects来实现后者的需求。它可以用来做产品化的代码,但是我基本是用来调试我的测试用例。(如果你有疑问,你可以戳这里)

#import "Aspects.h"

[UIPopoverPresentationController aspect_hookSelector:NSSelectorFromString(@"dimmingViewWasTapped:") 
                                         withOptions:0 
                                          usingBlock:^(id  info, UIView *tappedView) {
    NSLog(@"%@ dimmingViewWasTapped:%@", info.instance, tappedView);
} error:NULL];

dimmingViewWasTapped:因为是私有钩子 - 所以我们使用了NSSelectorFromString。你可以核对这个方法是存在的,然后通过iOS运行时头文件也检查下每个框架类得所有的私有和公有方法。这个工程的真相是调用了比之苹果提供给我们更为完整的头文件。(调用私有API未必是好事,这个要懂)

在钩子方法打印的信息如下:

PSPDFCatalog[84049:1079574]  dimmingViewWasTapped:
PSPDFCatalog[84049:1079574]  dimmingViewWasTapped:

我们看到对象的地址是一样的,所以我们可怜的隐藏视图真的是调用了两次。我们可以再一次使用Aspects来看看到底是那个视图控制器真正进行了调用:

[UIViewController aspect_hookSelector:@selector(dismissViewControllerAnimated:completion:)
                          withOptions:0
                           usingBlock:^(id  info) {
    NSLog(@"%@ dismissed.", info.instance);
} error:NULL];
2014-11-22 19:24:51.900 PSPDFCatalog[84210:1084883]  dismissed.
2014-11-22 19:24:52.209 PSPDFCatalog[84210:1084883]  dismissed.

这个隐藏的视图对我们的导航栏控制器调用了dismiss共两次。UIViewControllers的dismissViewControllerAnimated:completion:将试图控制器的请求丢给了它的中间子视图控制器,如果有一个这个东西的话,不然它就将让自己消失掉。所以第一次,消失请求丢给popover,然后第二次,导航栏控制器自己消失了。

找到方案

我们现在已经知道了发生了什么 - 现在让我们转向“为什么”。UIKit是个封闭的资源,但是我们可以使用反汇编像Hopper这样的工具来大概阅读UIKit并且来深层次的洞悉下UIPopoverPresentationController的具体机制。你可以从/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk/System/Library/Frameworks/UIKit.framework.现获取到二进制文件然后使用Hopper选中File -> Read Executable to Disassemble…来查看跟进而今和符号化的代码到底长什么样。32位反汇编是最成熟的一种技术,所以你可以通过选择32位文件来获取最好的反汇编结果。IDA by Hex-Rays是另外一个强大昂贵的反汇编工具来提供比下图更牛逼的反汇编结果:

在阅读代码时候带上一些基础技能是非常有用的。不过,你也可以使用伪代码视图结构来获取一些类似C语言的东东:

阅读伪代码相当大开眼界。有两个代码路径 - 其实一个是如果代理实现了popoverPresentationControllerShouldDismissPopover:,并且另一个是如果没有 - 代码路径则会截然不同。尽管又继续判断代理是否(controller.presented && !controller.dismissing),另一个路径则没有并且总是使消失。根据这之中的交代,我们可以尝试去通过实现我们自己的UIPopoverPresentationControllerDelegate:去解决BUG:

- (BOOL)popoverPresentationControllerShouldDismissPopover:(UIPopoverPresentationController *)popoverPresentationController {
    return YES;
}

我第一个尝试是设置主视图控制器来创建popover。然而,这样破坏了UIPopoverController。尽管没有提到,popover控制器在_setupPresentationController中设置它自身作为代理,然后代理消失得时候会坏事。相反的,我使用UIPopoverController子类并且添加上述方法。这样两个类得连接没有被说明,并且修复了为说明的行为;但是,这个更贴切默认和存在问题的实现解决了这个问题,显然它是未来代码。

雷达报告

先不要止步。你应该总是正确记录这些解决方法,最重要的是,在苹果文档中设置一个雷达。作为一个额外的收益,这样让你可以验证你是否已经了解了这个BUG,并且在你的应用中出现的时候不会出现副作用 - 并且当你变更iOS版本的时候更容易回溯和验证雷达是否仍然有效:

// The UIPopoverController is the default delegate for the UIPopoverPresentationController
// of it"s contentViewController.
//
// There is a bug when someone double-taps on the dimming view, the presentation controller invokes
// dismissViewControllerAnimated:completion: twice, thus also potentially dismissing the parent controller.
//
// Simply implementing this delegate runs a different code path that properly checks for dismissing.
// rdar://problem/19053416
- (BOOL)popoverPresentationControllerShouldDismissPopover:(UIPopoverPresentationController *)popoverPresentationController {
    return YES;
}

写一个雷达是一个非常有趣的挑战,并且不像你想象中那样花时间。略略略。。。
介绍两个雷达:

QuickRadar

OpenRadar

资源

略(参照原文)

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/15534.html

相关文章

  • fir.im Weekly - iOS/Android 应用程序架构解析

    摘要:原文链接原文作者译文出自小鄧子的简书下拉刷新开源库分析对比家艺分析对比了上数的下拉刷新开源库,后面能会加入其它有代表性的库。廖祜秋秋百万也说道史上最强的下拉刷新类库分析,没有之一,太棒了除了下拉刷新对比分析,家艺同学还做了的源码分析。 假如问你一个iOS or Android app的架构,你会从哪些方面来说呢? 本期 fir.im Weekly 收集了关于  iOS/Android ...

    yedf 评论0 收藏0
  • iOS/OSX 调试:跳舞吧!与LLDB共舞华尔兹

    摘要:它与绑定并且驻在控制台界面化于窗口的下端。与此同时,让我们开始如何使用调试器打印变量值的旅程吧。任何带美元符号是的命名空间,其存在是为了为你提供帮助。然后,变量必须以美元符号作为开头噢。这意味着剩下函数没有被执行。 原文链接:http://www.objc.io/issue-19/lldb-debugging.html // 速翻,无校对版 前言 你是否呕心沥血的尝试去理解代码...

    fnngj 评论0 收藏0
  • [前端工坊]快应用-技术调研

    摘要:文章来自微信公众号前端工坊,不定期更新有趣好玩的前端相关原创技术文章。快应用和微信小程序的区别开发环境快应用需要自行安装配置一系列环境及调试工具,而小程序只需一个开发者工具即可,这一点小程序胜。 文章来自微信公众号:前端工坊(fe_workshop),不定期更新有趣、好玩的前端相关原创技术文章。如果喜欢,请关注公众号:前端工坊版权归公众号所有,转载请注明出处。 作者: xdooi sh...

    y1chuan 评论0 收藏0
  • [前端工坊]快应用-技术调研

    摘要:文章来自微信公众号前端工坊,不定期更新有趣好玩的前端相关原创技术文章。快应用和微信小程序的区别开发环境快应用需要自行安装配置一系列环境及调试工具,而小程序只需一个开发者工具即可,这一点小程序胜。 文章来自微信公众号:前端工坊(fe_workshop),不定期更新有趣、好玩的前端相关原创技术文章。如果喜欢,请关注公众号:前端工坊版权归公众号所有,转载请注明出处。 作者: xdooi sh...

    paulli3 评论0 收藏0
  • Forrester企业级容器平台权威排行出炉,小初创Rancher缘何成为领导者?

    摘要:报告划重点和领跑企业容器云市场在此次发布的企业级容器平台的类似的魔力象限中,和是企业级容器管理平台市场的卓越领导者。 showImg(https://segmentfault.com/img/remote/1460000016766848?w=1268&h=365); 全球著名的调研机构Forrester Research近日发布了《The Forrester New Wave: En...

    Pocher 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<