iOS/OSX 調試:用例調研

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

// 速翻,無校對版

我是前言

代碼世界也不存在聖人,因此調試也是咱們你們所必備的良好技能。不是亂槍打鳥,我將會看看回歸到UIKit中的BUG,並告訴你我之前理解,隔離,並最終解決問題的工做流程。python

問題來了

咱們收到一個BUG報告:快速電議按鈕彈出一個popover,popover消失掉之後尼瑪父視圖控制器一同消失掉了。幸運的是,栗子還在,那麼讓咱們開始第一步 - 重現這個BUG - 須要注意的是:
圖片描述ios

個人第一感受是咱們能夠在代碼中主動消失了視圖控制器,而且咱們錯誤地讓父視圖控制器消失了。可是,當使用Xcode的集成視圖調試功能調試的時候,很明顯這裏有一個全局的UIDimmingView做爲外界觸點事件輸入源的第一響應者:git

圖片描述

大蘋果在Xcode 6中增長了調試視圖圖層特性,這個特性多是被現下流行的RevealSpark Inspector應用所激發的靈感,固然上述兩款應用在許多方面仍是完爆Xcode.github

使用LLDB

在可視化調試之前,一般的做法是在LLDB中鍵入po [[UIWindow keyWindow] recursiveDescription]用文字形式打印出圖層的關係來檢視視圖層級。xcode

與檢視視圖層級相似的,咱們也須要經過[[[UIWindow keyWindow] rootViewController] _printHierarchy]檢視視圖控制器的層級。這是大蘋果在iOS8中針對增長UIViewController添加隱藏的私有幫助指令sass

(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功能強勁並能夠腳本化。FB放出了一系列的叫作Chisel的大python腳本來幫助平常的調試工做。pviewspvc對於視圖與視圖控制器的層級打印來講是等價的。Chisel的視圖控制器樹也相似,可是同時展現視圖的邊框屬性。我一般是使用它來檢視響應者鏈,而且儘管你能夠手動循環找出你所感興趣對象的下一個響應者,或是能夠添加一個擴展指令,總歸鍵入presponder object是目前爲止最便捷的方式。架構

添加斷點

讓咱們好好琢磨下是什麼代碼讓咱們的試圖控制器消失得。最明顯的方式是設置一個斷點打在viewWillDisappear:來看看函數棧的記錄:app

(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 GuideMac 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
<PSPDFViewController: 0x15a1ca00 document:<PSPDFDocument 0x15616e70 UID:amazondynamososp2007_0c7fb1fc6c0841562b090b94f0c1c890 files:1 pageCount:16 isValid:1> page:0>

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

arm64與armv7類似,而後,由於有了更多可用的寄存器, $x0到$x7所有用來存變量,而不是放在棧寄存器$sp上。

你能夠學習更多X86X86-64中關於棧的知識,同時還能夠閱讀AMD64 ABI Draft

使用Runtime

另一個技巧來追溯方法執行是使用方法重寫成在調用父類方法以前進行帶日誌打印的模式。可是,手動的進行一個實際方法切換(swizzling)僅僅只是作到調試起來比較方便可是作不到時間節省。前陣子,我寫了一個小方法庫叫作Aspects來實現後者的需求。它能夠用來作產品化的代碼,可是我基本是用來調試個人測試用例。(若是你有疑問,你能夠戳這裏

#import "Aspects.h"

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

dimmingViewWasTapped:由於是私有鉤子 - 因此咱們使用了NSSelectorFromString。你能夠覈對這個方法是存在的,而後經過iOS運行時頭文件也檢查下每一個框架類得全部的私有和公有方法。這個工程的真相是調用了比之蘋果提供給咱們更爲完整的頭文件。(調用私有API未必是好事,這個要懂)

在鉤子方法打印的信息以下:

PSPDFCatalog[84049:1079574] <UIPopoverPresentationController: 0x7fd09f91c530> dimmingViewWasTapped:<UIDimmingView: 0x7fd09f92f800; frame = (0 0; 768 1024)>
PSPDFCatalog[84049:1079574] <UIPopoverPresentationController: 0x7fd09f91c530> dimmingViewWasTapped:<UIDimmingView: 0x7fd09f92f800; frame = (0 0; 768 1024)>

咱們看到對象的地址是同樣的,因此咱們可憐的隱藏視圖真的是調用了兩次。咱們能夠再一次使用Aspects來看看究竟是那個視圖控制器真正進行了調用:

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

寫一個雷達是一個很是有趣的挑戰,而且不像你想象中那樣花時間。略略略。。。
介紹兩個雷達:

資源

略(參照原文)

相關文章
相關標籤/搜索