WWDC 2018:效率提升爆表的 Xcode 和 LLDB 除錯技巧

xietao3發表於2018-06-13

WWDC 2018 Session 412 : Advanced Debugging with Xcode and LLDB

前言

在程式設計師寫 bug 的職業生涯中,只有 bug 會永遠陪伴著你,如何處理與 bug 之間的關係,是每一位程式設計師的必修課。特別是入門程式設計師經常受 bug 的影響,熬夜加班壓力大,長痘長胖還脫髮。

每一位 iOS 和 macOS 開發者都是幸運的,因為蘋果的 Xcode 和 LLDB 除錯工具,這是每一位開發者應該使用的除錯神器,可以幫助我們更快地解決問題。本文將主要講解 Xcode 的 斷點除錯LLDB 偵錯程式 以及 檢視結構除錯(UI Hierarchy)的使用技巧,這些技巧將大幅減少除錯中重新編譯的次數,減少你的等待時間。

這些技巧使用起來非常簡單,而且在開發場景非常實用,每一位開發者都有必要掌握這些技巧。

一、提升 Swift 除錯可用性 (Swift Debugging Reliability)

1.1 解決從 AST context 獲取模組失敗問題(Failed to get module from AST context)

相信很多開發者在使用 Swift 的時候,除錯過程中的一些問題會讓你很頭痛。 比如說下面這個問題,LLDB 在 AST Context 重建編譯狀態時,有些時候在複雜的情況下可能無法檢測到部分模組的變化,於是偵錯程式提示Failed to get module from AST context

WWDC 2018:效率提升爆表的 Xcode 和 LLDB 除錯技巧

在 Xcode 10 中,為了應對這個問題,會為當前的 frame 呼叫棧建立一個新的 expression evaluator 。

WWDC 2018:效率提升爆表的 Xcode 和 LLDB 除錯技巧

1.2 解決 Swift 型別問題(Swift Type Resolution)

還有一些開發者會遇到在除錯的時候無法顯示變數型別、列印變數資訊的問題如下圖:

WWDC 2018:效率提升爆表的 Xcode 和 LLDB 除錯技巧

蘋果針對大量的錯誤報告進行追蹤,在 Xcode 10 中修復了這個 bug ,除錯資訊中將不再會出現此類錯誤。

WWDC 2018:效率提升爆表的 Xcode 和 LLDB 除錯技巧

二、吐血推薦的除錯小技巧(Advanced Debugging Tips and Tricks)

2.1 自動建立除錯標籤頁(Configure behaviors to dedicate a tab for debugging)

想必你經常在看程式碼的時候由於執行到斷點而被強行切換到斷點所在的頁面,在斷點頁面和之前頁面進行切換的體驗是非常差的。現在你可以設定在被斷點的時候自動新建一個標籤頁,通過切換標籤頁你可以快速便捷地切回到之前瀏覽的頁面。

WWDC 2018:效率提升爆表的 Xcode 和 LLDB 除錯技巧

設定自動新建 Debug Tab 方法:頂部導航欄 Xcode -> Behaviors -> Edit Behaviors... -> Runing -> Pauses -> ✅ Show Tab Name tab name in active window

WWDC 2018:效率提升爆表的 Xcode 和 LLDB 除錯技巧

2.2 在 LLDB 中修改 App 狀態(LLDB expressions can modify program state)

在 LLDB 中通過expression命令可以改變程式當前的各種狀態,eexpr 作為簡寫也可以實現同樣的功能。我們用一個簡單的UILabel來舉例,為myLabel設定一個值 hello , 正常來講檢視上的myLabel就應該顯示 hello 。

func test() -> Void {
    myLabel.text = "hello"
// 斷點 -> 
}
複製程式碼

你可以在myLabel.text = "hello"這句程式碼後設定一個斷點,執行程式執行斷點後,在控制檯的 LLDB 偵錯程式 中輸入下面的表示式改變它的值,在繼續執行程式之後,相信你在介面上看到的值一定是 hello world 。

// 改變 myLabel 文案
expr myLabel.text = "hello world"
複製程式碼

WWDC 2018:效率提升爆表的 Xcode 和 LLDB 除錯技巧

除了改變myLabel.text的值之外,你可以像在 Xcode 中寫程式碼一樣,在 LLDB 中進行同樣的操作。例如你可以像下面的程式碼一樣使用表示式改變它的文字顏色,也可以執行某個函式。

// 改變 myLabel 文字顏色
expr myLabel.textColor = UIColor.red

// 執行 test 方法
expr test()
複製程式碼

2.3 利用斷點實時插入程式碼(Use auto-continuing breakpoints with debugger commands to inject code live)

除了直接在控制檯通過 LLDB 偵錯程式修改 App 狀態,你還可以通過在斷點中新增命令來實現同樣的功能。而且通過斷點來設定除錯命令的方式更加方便實用,幾乎是實時插入程式碼的功能。

如下圖,設定一個斷點,通過 Edit Breakpoint... 開啟編輯框,你可以將多個不同的除錯命令按順序填入 Action 中,就能實現之前同樣的功能。另外你可以勾選 Automatically continue after evaluationg actions ,可以自動繼續執行後續程式碼,而不會停在這一行。

WWDC 2018:效率提升爆表的 Xcode 和 LLDB 除錯技巧

2.4 在彙編呼叫棧中列印函式實參("po $arg1" ($arg2, etc) in assembly frames to print function arguments)

首先,我們瞭解一下全域性斷點,你可以點選在 Breakpoints Navigator 左下角 + 號,然後選擇 Symbolic Breakpoint... ,如下圖,你可以在 Symbol 一欄輸入任何你想監聽的函式比如[UILabel setText:],之後所有頁面下的所有UILabel型別物件在設定text屬性的時候都會執行該斷點。(ps:我還不是最酷的?)

WWDC 2018:效率提升爆表的 Xcode 和 LLDB 除錯技巧

在這個斷點的控制檯中,並沒有顯示變數屬性等資訊,我們怎麼能知道設定了什麼呢?接下來我們可以用$arg1$arg2等命令來列印出我們想要的資訊。

如下圖,在這裡$arg1是指物件本身,$arg2是物件被呼叫的函式,po命令無法直接輸出函式名,需要加上(SEL)$arg3是被賦給text的值。

WWDC 2018:效率提升爆表的 Xcode 和 LLDB 除錯技巧

2.5 利用 “breakpoint set --one-shot true” 命令建立一次性斷點(Create dependent breakpoints using )

上面我們介紹了全域性斷點,它能監測到全域性的函式呼叫,但是我想監測某一個函式內區域性區域的函式呼叫,這個時候我們可以使用breakpoint set --one-shot true命令動態生成一個斷點,這個斷點將是一次性的,執行一次後將被自動刪除。

最酷的是,我們將建立會先一個斷點,如下圖,讓這個斷點來實現這一切,即用一個斷點來建立另外一個一次性的斷點,為了讓整個過程是無感的,我建議勾選 Automatically continue after evaluationg actions 選項。

WWDC 2018:效率提升爆表的 Xcode 和 LLDB 除錯技巧

上圖這個斷點到底幹了什麼?當執行到圖中第 61 行的斷點時,這個斷點並不會導致命令執行暫停,它只幹了一件事,就是通過命令breakpoint set --name "[UILabel setText:]"建立了一個全域性斷點,加上--one-shot true就代表是一次性的斷點。

如上圖的執行效果就是breakpoint set --one-shot true --name "[UILabel setText:]"命令會讓指標在myLabel.text = "hello"這一行暫停,暫停後一次性的使命就已經結束,所以在下一行myLabel.text = "hello world"是不會暫停的。

2.6 通過拖拽指令指標或 “thread jump --by 1” 命令跳過一行程式碼(Skip lines of code by dragging Instruction Pointer or “thread jump --by 1” )

首先我們看如何通過拖拽指令指標來,跳過一段程式碼不執行。如下圖,直接拖拽紅色箭頭指向的按鈕,拖到哪從哪裡開始執行,往上拖可以重複執行之前的程式碼,往下拖將不執行中間被跳過的程式碼。

WWDC 2018:效率提升爆表的 Xcode 和 LLDB 除錯技巧

我們通過thread jump --by 2命令,跳過了 2 行程式碼,如下圖將只列印 1 和 4 。

WWDC 2018:效率提升爆表的 Xcode 和 LLDB 除錯技巧

2.7 利用 watchpoints 監聽變數的變化(Pause when variables are modified by using watchpoints)

上面我們介紹了使用全域性斷點和一次性斷點對[UILabel setText:]函式監聽屬性的變化,其實我們還有另一個選擇, 使用 watchpoints 通過監測記憶體的變化來監聽屬性的變化。

我們可以在viewDidLoad函式中設定一個斷點,然後再控制檯找到你需要監聽的屬性,如下圖:

WWDC 2018:效率提升爆表的 Xcode 和 LLDB 除錯技巧

選中你想要監聽的屬性後,點選右鍵將彈出下圖視窗,點選 Watch "count"即可監聽屬性 count 的值的改變,如執行count+=1。需要注意的是每當重新編譯後指標發生變化,就需要重新設定 watchpoints 。

WWDC 2018:效率提升爆表的 Xcode 和 LLDB 除錯技巧

2.8 Swift 呼叫棧中在 LLDB 偵錯程式使用 Obj-C 程式碼命令(Evaluate Obj-C code in Swift frames with “expression -l objc -O -- ”)

在日常除錯中,使用 LLDB 命令po [self.view recursiveDescription]命令來輸出頁面檢視結構是非常方便的,然而我們在 Swift 呼叫棧中使用這個命令的時候將列印以下錯誤:

po self.view.recursiveDescription()
error: <EXPR>:3:6: error: value of type 'UIView?' has no member 'recursiveDescription'
self.view.recursiveDescription()
~~~~~^~~~ ~~~~~~~~~~~~~~~~~~~~
複製程式碼

其實我們可以通過“expression -l objc -O -- ”命令來使用 Obj-C 程式碼來輸出我們想要的檢視結構,記得self.view兩邊一定要加上 ` 符號。

expression -l objc -O -- [`self.view` recursiveDescription]
複製程式碼

不知道你們有沒有覺得上面這個命令有點長,還好我們可以可以通過command alias <alias name> expression -l objc -O —- 為這句命令建立一個別名,之後就可以通過別名來使用相關操作。

WWDC 2018:效率提升爆表的 Xcode 和 LLDB 除錯技巧

再另一種方式,我們可以使用po unsafeBitCast(<pstr> , UnsafePointer.self)命令列印物件描述、中心點座標,當然也可以設定相關屬性。

// 列印物件
(lldb) po unsafeBitCast(0x7fe439d13160, UILabel.self)
<UILabel: 0x7fe439d13160; frame = (57 141; 42 21); text = 'Label'; opaque = NO; autoresize = RM+BM; userInteractionEnabled = NO; layer = <_UILabelLayer: 0x600003942a30>>

// 列印中心點座標
(lldb) po unsafeBitCast(0x7fe439d13160, UILabel.self).center
▿ (78.0, 151.5)
  - x : 78.0
  - y : 151.5
  
// 設定中心點座標
(lldb) po unsafeBitCast(0x7fe439d13160, UILabel.self).center.y = 300
複製程式碼

2.9 利用 “expression CATransaction.flush()” 命令重新整理頁面(Flush view changes to the screen using “expression CATransaction.flush()”)

你可以在控制檯通過 LLDB 偵錯程式中改變 UI 的座標值,但你並不能立即看到頁面有任何改變。事實上你確實修改了它的值,你只是需要使用“expression CATransaction.flush()”來重新整理一下你的頁面。

配合修改 UI 座標值的命令一起使用,你能看到你的模擬器正在發生令人振奮的一幕。

// 修改座標點
po unsafeBitCast(0x7fe439d13160, UILabel.self).center.y = 300
// 重新整理頁面
expression CATransaction.flush()
複製程式碼

2.10 利用別名和指令碼新增自定義 LLDB 命令(Add custom LLDB commands using aliases and scripts)

當你對 LLDB 命令越來越瞭解,操作越來越騷的時候,你會發現小小的控制檯會限制你的發揮,這個時候你需要一個更大的舞臺。

現在我要展示如何使用 Python 指令碼執行命令,你需要先下載一 個nudge.py ,這是蘋果開發工程師為我們準備好的 Python 指令碼,它可以幫助我們簡單、快速地移動 UI 控制元件。我們需要將 nudge.py 檔案放入你的使用者根目錄~/nudge.py

下一步我們需要在使用者根目錄下新建一個~/.lldbinit檔案,並加入下方命令和別名:

command script import ~/nudge.py
command alias poc expression -l objc -O --
command alias ? expression -l objc -- (void)[CATransaction flush]
複製程式碼

做完這些,我們就可以來使用我們的自定義命令nudge x-offset y-offset [view]了,具體用法如下:

// 引用 nudge
(lldb) command script import ~/nudge.py
The "nudge" command has been installed, type "help nudge" for detailed help.

// 拿到物件指標
(lldb) po myLabel
▿ Optional<UILabel>
  - some : <UILabel: 0x7fc04a60fff0; frame = (57 141; 42 21); text = 'Label'; opaque = NO; autoresize = RM+BM; userInteractionEnabled = NO; layer = <_UILabelLayer: 0x600001d36c10>>
  
// Y軸向上偏移5
(lldb) nudge 0 -5 0x7fc04a60fff0
複製程式碼

調整模擬器中控制元件位置的效果:

WWDC 2018:效率提升爆表的 Xcode 和 LLDB 除錯技巧

2.11 LLDB 列印命令(LLDB Print Commands)

Command Alias For Steps TO Evaluate
po <expression> expression --object-description -- <expression> 1. Expression: evaluate
2. Expression: debug description
p expression -- 1. Expression: evaluate
2. Outputs LLDB-formatted description
frame variable none 1. Reads value of from memory
2. Outputs LLDB-formatted description

p 和 po 命令從別名和執行過程上來看,分別輸出的是物件和 LLDB 格式資料。

而 frame variable 不同之處的是從當前 frame 呼叫棧的記憶體中拿到的值。只接受變數作為引數,不接受表示式。通過frame variable命令,可以列印出當前 frame 呼叫棧的的所有變數。

三、深入瞭解 Xcode 檢視除錯技巧(Advanced View Debugging)

3.1 在除錯導航欄中快速定位到檢視位置(Reveal in Debug Navigator)

在開發中我們會頻繁使用到 Debug View Hierarchy 檢視當前頁面檢視結構,正常情況下導航欄的 UI 巢狀層級會非常多,讓我們無法快速準確找到我們想檢視的控制元件所在的層級。

WWDC 2018:效率提升爆表的 Xcode 和 LLDB 除錯技巧

其實 Xcode 已經有快捷方式可以讓你快速定位到控制元件在導航欄中的位置,首先點選選中你需要檢視的控制元件,然後再導航欄中的 navigate 選項,展開後選擇 Reveal in Debug Navigator ,如下圖:

WWDC 2018:效率提升爆表的 Xcode 和 LLDB 除錯技巧

3.2 顯示被裁剪的檢視內容(View clipped content)

WWDC 2018:效率提升爆表的 Xcode 和 LLDB 除錯技巧

當我們遇到這樣一個顯示不全的 bug 的時候,我們可以用到 Debug View Hierarchy 檢視當前檢視具體情況,進入除錯頁面你會看到下面這種情況:

WWDC 2018:效率提升爆表的 Xcode 和 LLDB 除錯技巧

我想我的 label 應該是完整的,但是超出頁面被裁剪掉了,這個時候我需要確認一下事實是不是和我想的一樣。如下圖,我們需要開啟 Show Clipped Content 選項。

WWDC 2018:效率提升爆表的 Xcode 和 LLDB 除錯技巧

最後我看到了真相和我猜測的是一致的,我可以根據真實情況準確制定出解決方案。

WWDC 2018:效率提升爆表的 Xcode 和 LLDB 除錯技巧

3.3 在除錯中檢視自動佈局資訊(Auto Layout debugging)

在除錯 Debug View Hierarchy 中檢視控制元件的約束只需要啟動 Show Constraints 選項,選中任何一個控制元件都會顯示出其擁有的約束。

WWDC 2018:效率提升爆表的 Xcode 和 LLDB 除錯技巧

選中約束後可以在右邊欄物件檢查器 Object Inspector 中檢視約束的詳細資訊。

WWDC 2018:效率提升爆表的 Xcode 和 LLDB 除錯技巧

3.4 在除錯檢查器中顯示呼叫棧(Creation backtraces in the inspector)

在除錯模式下,我們有辦法看到每一個控制元件,每一個約束的建立呼叫棧,方便我們快速定位到問題的源頭。舉個例子,我手動為我的 label 對頂部距離 100 的約束。

let myLabelTopConstraint =  myLabel.topAnchor.constraint(equalTo: self.view.topAnchor, constant: 100)
NSLayoutConstraint.activate([myLabelTopConstraint])
複製程式碼

執行 Demo 後開啟 Debug View Hierarchy ,開啟顯示約束選項後,你可以找到這個約束並選中,在右邊欄的物件檢查器的 Backtrace 一欄你可以看到一個呼叫棧的列表。如下圖,點選右邊小箭頭可以跳轉到建立該物件的程式碼處。

WWDC 2018:效率提升爆表的 Xcode 和 LLDB 除錯技巧

這項功能是需要手動開啟的,你可以通過點選專案 Target -> Edit Scheme... -> Run -> Diagnostics -> Logging -> 勾選 Malloc Stack 並且切換至 All Allocation and Free History 模式開啟此功能。

WWDC 2018:效率提升爆表的 Xcode 和 LLDB 除錯技巧

3.5 獲取物件指標及其擴充(Access object pointers (copy casted expressions) )

在檢視除錯模式中,我們有時候也會需要在 LLDB 偵錯程式中輸入表示式來達到修改控制元件位置的的效果。

舉例我們要修改一個約束的值,我們首先要拿到這個約束物件的指標,好訊息是 Xcode 可以非常方便讓我們拿到,選中該約束,直接快捷鍵 ⌘ + c 就複製好了,可以直接複製到控制檯中使用。

你可以輸出該約束的描述資訊,和右邊欄檢查器中的 Description 是一樣的效果。

// po + 複製好的指標
po ((NSLayoutConstraint *)0x600000dd4460)

// 輸出結果
<NSLayoutConstraint:0x600000dd4460 UILabel:0x7fdb1c70a710'WWDC 2018:效率提升爆表的 Xcode 和...'.top == UIView:0x7fdb1c70b950.top + 100   (active)>
複製程式碼

也許你還需要複習一下之前的內容,來修改一下約束的值,並且重新整理頁面,完成這些後趕緊看看模擬器的效果。

// 設定約束的值為 200
(lldb) e [((NSLayoutConstraint *)0x600000dd4460) setConstant:200]

// 重新整理 UI
// ? 是 expression -l objc -- (void)[CATransaction flush] 命令的別名
(lldb) ? 
複製程式碼

3.6 利用快捷鍵 ⌘-click 選中被遮擋的檢視 (⌘-click-through for selection)

在除錯中,你要選擇的檢視被另一個檢視遮擋住的情況下,你可以通過 3D 的檢視模式,選中後背的檢視,如下圖。

WWDC 2018:效率提升爆表的 Xcode 和 LLDB 除錯技巧

但是這種方式實在難稱優雅,況且還有一些刁鑽的角度會讓你非常頭疼。在 2D 的情況下,正確的選中方式應該是 ⌘-click 直接選中背後被遮擋的檢視,快去試試看吧。

四、除錯深色模式(Debugging Dark Mode)

4.1 切換深色模式(Appearance overrides)

在 macOS 10.14 版本下並且安裝了 Xcode 10 ,你就可以在開發中使用 Dark Mode 了,你可以在 Xcode 底部的找到一個黑白兩色小方塊按鈕,通過選中這個按鈕,你可以切換模擬器 Dark 和 Light 兩種外觀。如果你的 Macbook 有 Touch Bar 的話,你也可以通過 Touch Bar 上的按鈕來切換。

WWDC 2018:效率提升爆表的 Xcode 和 LLDB 除錯技巧

在 StoryBoard 中你可以在底部找到 View as : Light/Dark Appearance 來預覽 Dark 和 Light 外觀。

macOS 開發中選中任意一個 View ,你都可以在右邊欄的檢查器中找到 Appearance 屬性,通過這個屬性你可以為這個 View 及其子檢視設定固定的外觀顏色,且不會隨著使用者切換 Dark 和 Light 外觀而改變顏色。

4.2 捕獲活動的 Mac app(Capturing active Mac apps)

我們的 UI Hirerachey 同時只能顯示一個 UIWindow 的內容,所有在除錯的時候,彈出的 UIWindow 並不會和頁面內的 UI 結構一起展示給我們,像 UIAlertView 這種彈出 UIWindow 就無法一起顯示。

如果我們需要檢視彈出 UIWindow , 我們需要把左邊欄當前的檔案結構全部關閉收起,這個時候你會看到 ViewController 所在的 UIWindow 下面還有另外一個 UIWindow ,選中之後就可以檢視彈出的 UIWindow 的 UI 層級結構了。

WWDC 2018:效率提升爆表的 Xcode 和 LLDB 除錯技巧

4.3 在檢查器中檢視深色模式資訊(Named colors and NSAppearance details in inspector)

在 UI Hierarchy 除錯中我們可以在右邊欄的檢查器中檢視 Dark Mode 相關資訊,選中一個 UILabel 可以檢視該 label 的 Text Color 屬性。在 Dark Mode 下一共有 3 中型別顏色:

  • System Color: 系統推薦顏色 System Color ,可以根據當前外觀顏色自適應文字顏色。
  • Named Color:Named Color 需要開發者在 assets catalog 中設定,可以針對 Dark Light 設定不同色值。
  • 自定義 RGB 顏色:純手動設定的自定義 RGB 固定色值。

下圖中的 Text Color 就是在 assets catalog 中設定的 Named Color ,設定的名字為 titleColor,你可以根據場景為該設定設定合適的名字。

WWDC 2018:效率提升爆表的 Xcode 和 LLDB 除錯技巧

如下圖,檢查器偏下的位置 View 一欄中,我們可以找到 Appearance 和 Effective 屬性,Appearance 是表示該檢視下子檢視無法切換的固定的外觀顏色選擇,Effective 是當前生效的外觀顏色。

WWDC 2018:效率提升爆表的 Xcode 和 LLDB 除錯技巧

在 assets catalog 中設定 Named Color:

WWDC 2018:效率提升爆表的 Xcode 和 LLDB 除錯技巧

總結

功能強大的 LLDB ,特別是配合 BreakPoint 一起使用,讓我們有了更多的想象空間,加上越來越好用的 UI Hirerachey ,讓我們的除錯手段更加靈活。 這些內容雖然需要花一些時間去了解,但我相信掌握這些技巧將會為你節省下更多的時間。

從此你再也不用為下班前測出 bug 而焦慮了,早用上,早收工,最多幹到下午 3 點鐘。希望本文內容對每一位讀者有所幫助。

參考連結

檢視更多 WWDC 18 相關文章請前往 老司機x知識小集xSwiftGG WWDC 18 專題目錄

相關文章