在线观看不卡亚洲电影_亚洲妓女99综合网_91青青青亚洲娱乐在线观看_日韩无码高清综合久久

鍍金池/ 教程/ iOS/ 調(diào)試:案例學習
與四軸無人機的通訊
在沙盒中編寫腳本
結(jié)構(gòu)體和值類型
深入理解 CocoaPods
UICollectionView + UIKit 力學
NSString 與 Unicode
代碼簽名探析
測試
架構(gòu)
第二期-并發(fā)編程
Metal
自定義控件
iOS 中的行為
行為驅(qū)動開發(fā)
Collection View 動畫
截圖測試
MVVM 介紹
使 Mac 應用數(shù)據(jù)腳本化
一個完整的 Core Data 應用
插件
字符串
為 iOS 建立 Travis CI
先進的自動布局工具箱
動畫
為 iOS 7 重新設(shè)計 App
XPC
從 NSURLConnection 到 NSURLSession
Core Data 網(wǎng)絡(luò)應用實例
GPU 加速下的圖像處理
自定義 Core Data 遷移
子類
與調(diào)試器共舞 - LLDB 的華爾茲
圖片格式
并發(fā)編程:API 及挑戰(zhàn)
IP,TCP 和 HTTP
動畫解釋
響應式 Android 應用
初識 TextKit
客戶端
View-Layer 協(xié)作
回到 Mac
Android
Core Image 介紹
自定義 Formatters
Scene Kit
調(diào)試
項目介紹
Swift 的強大之處
測試并發(fā)程序
Android 通知中心
調(diào)試:案例學習
從 UIKit 到 AppKit
iOS 7 : 隱藏技巧和變通之道
安全
底層并發(fā) API
消息傳遞機制
更輕量的 View Controllers
用 SQLite 和 FMDB 替代 Core Data
字符串解析
終身學習的一代人
視頻
Playground 快速原型制作
Omni 內(nèi)部
同步數(shù)據(jù)
設(shè)計優(yōu)雅的移動游戲
繪制像素到屏幕上
相機與照片
音頻 API 一覽
交互式動畫
常見的后臺實踐
糟糕的測試
避免濫用單例
數(shù)據(jù)模型和模型對象
Core Data
字符串本地化
View Controller 轉(zhuǎn)場
照片框架
響應式視圖
Square Register 中的擴張
DTrace
基礎(chǔ)集合類
視頻工具箱和硬件加速
字符串渲染
讓東西變得不那么糟
游戲中的多點互聯(lián)
iCloud 和 Core Data
Views
虛擬音域 - 聲音設(shè)計的藝術(shù)
導航應用
線程安全類的設(shè)計
置換測試: Mock, Stub 和其他
Build 工具
KVC 和 KVO
Core Image 和視頻
Android Intents
在 iOS 上捕獲視頻
四軸無人機項目
Mach-O 可執(zhí)行文件
UI 測試
值對象
活動追蹤
依賴注入
Swift
項目管理
整潔的 Table View 代碼
Swift 方法的多面性
為什么今天安全仍然重要
Core Data 概述
Foundation
Swift 的函數(shù)式 API
iOS 7 的多任務(wù)
自定義 Collection View 布局
測試 View Controllers
訪談
收據(jù)驗證
數(shù)據(jù)同步
自定義 ViewController 容器轉(zhuǎn)場
游戲
調(diào)試核對清單
View Controller 容器
學無止境
XCTest 測試實戰(zhàn)
iOS 7
Layer 中自定義屬性的動畫
第一期-更輕量的 View Controllers
精通 iCloud 文檔存儲
代碼審查的藝術(shù):Dropbox 的故事
GPU 加速下的圖像視覺
Artsy
照片擴展
理解 Scroll Views
使用 VIPER 構(gòu)建 iOS 應用
Android 中的 SQLite 數(shù)據(jù)庫支持
Fetch 請求
導入大數(shù)據(jù)集
iOS 開發(fā)者的 Android 第一課
iOS 上的相機捕捉
語言標簽
同步案例學習
依賴注入和注解,為什么 Java 比你想象的要好
編譯器
基于 OpenCV 的人臉識別
玩轉(zhuǎn)字符串
相機工作原理
Build 過程

調(diào)試:案例學習

沒人寫的代碼是完美無暇的,但調(diào)試代碼我們卻都應該有能力能做好。相比提供一個關(guān)于本話題的隨機小建議,我更傾向于選擇帶你親身經(jīng)歷一個 bug 修復的過程,這是一個 UIKit 的 bug,我會展示我用來理解,隔離,并最終解決這個問題的流程。

問題

我收到了一個 bug 反饋報告,當快速點擊一個按鈕來彈出一個 popover 并 dismiss 它的同時,視圖控制器也會被 dismiss。謝天謝地,還附上了一個截圖示意,所以第一步 -- 重現(xiàn) bug -- 已經(jīng)被做到了:

http://wiki.jikexueyuan.com/project/objc/images/19-1.gif" alt="" />

我的第一個猜測是,我們可能包含了 dismiss 視圖控制器的代碼,我們錯誤地 dismiss 了父視圖控制器。然而,當使用 Xcode 集成的視圖調(diào)試功能時,很明顯有一個全局 UIDimmingView 作為 first responder 來響應點擊事件:

http://wiki.jikexueyuan.com/project/objc/images/19-2.png" alt="" />

蘋果在 Xcode 6 中添加了調(diào)試視圖層次結(jié)構(gòu)的功能,這一舉動很可能是受到非常受歡迎的應用 RevealSpark Inspector 的啟發(fā)。相對于 Xcode,它們在許多方面表現(xiàn)更好,功能更多。

使用 LLDB

在可視化調(diào)試出現(xiàn)之前,最常見的做法是在 LLDB 使用 po [[UIWindow keyWindow] recursiveDescription] 來檢查層次結(jié)構(gòu)。它可以以文本形式打印出完整的視圖層次結(jié)構(gòu)

類似于檢查視圖層次,我們也可以用 po [[[UIWindow keyWindow] rootViewController] _printHierarchy] 來檢查視圖控制器。這是一個蘋果默默在 iOS 8 中為 UIViewController 添加的私有輔助方法 。

(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 發(fā)布了一組名為 Chisel 的 Python 腳本集合 為日常調(diào)試提供了非常多的幫助。pviewspvc 等價于視圖和視圖控制器的層次打印。Chisel 的視圖控制器樹和上面方法打印的很類似,但是同時還顯示了視圖的尺寸。 我通常用它來檢查響應鏈,雖然你可以對你感興趣的對象手動循環(huán)執(zhí)行 nextResponder,或者添加一個類別輔助方法,但輸入 presponder object 依舊是迄今為止最快的方法。

添加斷點

我們首先要找出實際 dismiss 我們視圖控制器的代碼。最容易想到的是在 viewWillDisappear: 設(shè)置一個斷點來進行調(diào)用棧跟蹤:

(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 可以達到一樣的效果,區(qū)別在于會打印全部線程的狀態(tài),而不僅是當前的線程。

看看這個棧,我們注意到視圖控制器已經(jīng)被 dismiss 途中,因為這個方法是在預定的動畫中被調(diào)用的,所以我們需要在更早的地方增加斷點。在這個例子中,我們關(guān)注的是對于 -[UIViewController dismissViewControllerAnimated:completion:] 的調(diào)用。我們在 Xcode 的斷點列表中添加一個符號斷點,并且重新執(zhí)行示例代碼。

Xcode 的斷點接口非常強大,它允許你添加條件,跳過計數(shù),或者自定義動作,比如添加音效和自動繼續(xù)等。雖然它們可以節(jié)省相當多的時間,但在這里我們不需要這些特性:

(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: 中處理,接著轉(zhuǎn)發(fā)到 UIPopoverPresentationController 中的 dimmingViewWasTapped: 方法來 dismiss 視圖控制器 (就像它該做的那樣),然而。當我們快速點擊時,這個斷點被調(diào)用了兩次。這里有第二個 dimming 視圖?還是說調(diào)用的是相同的實例?我們只有斷點時候的程序集,所以調(diào)用 po self 是無效的。

調(diào)用約定入門

根據(jù)程序集和函數(shù)調(diào)用約定的一些基本知識,我們依然可以拿到 self 的值。iOS ABI Function Call Guide 和在 iOS 模擬器時使用的 Mac OS X ABI Function Call Guide 都是極好的資源。

我們知道每個 Objective-C 方法都有兩個隱式參數(shù):self_cmd。于是我們所需要的就是在棧上的第一個對象。在 32-bit 架構(gòu)中,棧信息保存在 $esp 里,所以在 Objective-C 方法中你可以你可以使用 po *(int*)($esp+4) 來獲取 self,以及使用 p (SEL)*(int*)($esp+8) 來獲取 _cmd。$esp 里的第一個值是返回地址。隨后的變量保存在 $esp+12,$esp+16 以及依此類推的其他位置上。

x86-64 架構(gòu) (那些包含 arm64 芯片 iPhone 設(shè)備的模擬器) 提供了更多寄存器,所以變量放置在 $rdi,$rsi,$rdx$rcx,$r8,$r9 中。所有后續(xù)的變量在 $rbp 棧上。開始于 $rbp+16,$rbp+24 等。

armv7 架構(gòu)的變量通常放置在 $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 中。

你可以學到更多關(guān)于 x86,x86-64 的棧布局知識,還可以閱讀 AMD64 ABI Draft 來進行深入。

使用 Runtime

跟蹤方法執(zhí)行的另一種做法是重寫方法,并在調(diào)用父類之前加入日志輸出。然而,手動 swizzling 調(diào)試起來雖然方便,但是在要花的時間上來說其實效率不高。在前一陣子,我寫了一個很小的叫做 Aspects 的庫,來專門做這件事情。它可以用于生產(chǎn)代碼,但是我大部分時候只用它來調(diào)試和寫測試用例。(如果你對 Aspects 感興趣,你可以在這里了解更多相關(guān)知識。)

#import "Aspects.h"

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

這里我們?yōu)?dimmingViewWasTapped: 添加了一個鉤子,它是私有方法 — 因此我們使用 NSSelectorFromString。你可以驗證方法是否存在,并通過使用 iOS Runtime Headers 來查找?guī)缀趺總€框架類的其他私有和公共方法。這個項目利用了不可能在運行時真正地隱藏方法這一事實,它在所有類中查找方法并,從而創(chuàng)建了一個比蘋果所提供給我們的相比,更完整的頭文件。(當然,調(diào)用私有 API 并不是一個好主意 — 這里只是用來便于理解到底發(fā)生了什么)

在鉤子方法的日志中,我們獲得如下輸出:

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)>

我們看到對象地址完全相同,所以我們可憐的 dimming 視圖真的被調(diào)用了兩次,我們可以使用 Aspects 來查看具體 dismiss 方法調(diào)用在了哪個控制器上:

[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.

兩次 dimming 視圖都調(diào)用了主導航控制器的 dismiss 方法。如果子視圖控制器存在的話,視圖控制器的 dismissViewControllerAnimated:completion: 會將視圖控制器的 dismiss 請求轉(zhuǎn)發(fā)到它的子視圖控制器中,否則它將 dismiss 自己。所以第一次 dismiss 請求執(zhí)行于 popover,而第二次,導航控制器本身被 dismiss 了。

查找臨時方案

現(xiàn)在我們知道發(fā)生了什么事情 — 接下來我們可以進入為何發(fā)生的環(huán)節(jié)。UIKit 是閉源代碼,但是我們使用像 Hopper 這樣的反匯編工具來解讀 UIKit 程序集并且仔細看看 UIPopoverPresentationController 里發(fā)生了什么事情。你可以在 /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk/System/Library/Frameworks/UIKit.framework 里找到二進制文件。然后在 Hopper 里使用 File -> Read Executable to Disassemble...,這將遍歷整個二進制文件并且將代碼符號化。32-bit 反匯編是最成熟的一個。所以你選擇 32-bit 文件可以拿到最好的結(jié)果。Hex-Rays 出品的 IDA 是另一個很強大很昂貴的反匯編程序,通??梢蕴峁?a rel="nofollow" >更好的結(jié)果:

http://wiki.jikexueyuan.com/project/objc/images/19-3.png" alt="" />

一些匯編語言的基礎(chǔ)知識對閱讀代碼會非常有用。不過,你也可以使用偽代碼視圖來得到類似于 C 代碼的結(jié)果:

http://wiki.jikexueyuan.com/project/objc/images/19-4.png" alt="" />

閱讀偽代碼結(jié)果讓人大開眼界。這里有兩個代碼路徑 — 其中一個是如果 delegate 實現(xiàn)了 popoverPresentationControllerShouldDismissPopover: 時調(diào)用,另一個在沒有實現(xiàn)時調(diào)用 — 兩個代碼路徑實際上相當不同。delegate 實現(xiàn)了委托方法的那個路徑中,包含了 if (controller.presented && !controller.dismissing),而另一個代碼路徑 (我們現(xiàn)在實際進入的) 卻沒有,并總是調(diào)用 dismiss。通過內(nèi)部信息,我們可以嘗試通過實現(xiàn)我們自己的 UIPopoverPresentationControllerDelegate 來繞開這個 bug:

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

我的第一次嘗試是把創(chuàng)建 popover 的主視圖控制器設(shè)為 delegate。然而它破壞了 UIPopoverController。雖然文檔沒提,但 popover 控制器會在 _setupPresentationController 中將自己設(shè)為 delegate,另外,移除這個 delegate 將造成破壞。之后,我使用了一個 UIPopoverController 的子類并直接添加了上面的方法。這兩個類之間的聯(lián)系并沒有文檔化,而且我們的解決方案依賴于這個沒有文檔的行為;不過,這個實現(xiàn)是匹配默認行為的,它純粹是為了解決這個問題,所以它是經(jīng)得起未來考驗的代碼。

反饋 Radar

現(xiàn)在請不要停下。我們通常需要為這樣的繞開問題的方案寫一些文檔,但還有一件重要的事情是,給 Apple 提交一個 radar。這么做會帶來額外的好處,這能讓你驗證你是否真正理解這個 bug,并且在你的程序中沒有其他副作用 — 如果你之后放棄支持這個 iOS 版本,你可以很容易回滾代碼并測試這個 radar 是否修正過。

// UIPopoverController 是它的 contentViewController,即 UIPopoverPresentationController 的默認的 delegate
//
// 這里有一個 bug:當雙擊 diming 視圖時,presentation 視圖控制器將調(diào)用兩次
// dismissViewControllerAnimated:completion:,并 dismiss 掉它的父控制器.
//
// 通過實現(xiàn)這個 delegate 可以讓代碼運行另一條正確地檢查了是否正在 dismiss 的代碼路徑
// rdar://problem/19067761
- (BOOL)popoverPresentationControllerShouldDismissPopover:(UIPopoverPresentationController *)popoverPresentationController {
    return YES;
}

寫一個 Radar 實際上是非常有趣的挑戰(zhàn),它并不像你想象的那么花時間。用一個示例,你將幫助那些勞累蘋果工程師,沒有示例,工程師將很有可能推遲,甚至不考慮這個 radar。我為這個問題創(chuàng)建了一個大約 50 行代碼的例子,還包括一些意見和解決方案。單視圖的模板通常是創(chuàng)建一個示例的最快方式。

現(xiàn)在,我們都知道蘋果的 Radar 網(wǎng)頁并沒有那么好用,不過你可以不使用它。QuickRadar 是一個用來提交 radar 的非常優(yōu)秀的 Mac 前端,同時它會自動提交一個副本到 OpenRadar。此外,復制 radar 也極其方便。你應該馬上下載它,另外,如果你覺得例子里這樣的錯誤值得被修復,可以復制 rdar://19067761。

并不是所有問題都可以用一些簡單的方案繞開,但這些步驟將幫助你找到更好的解決問題的方法,或者至少幫助你的理解為什么某些事情會發(fā)生。

參考