天天看點

WWDC 2018:效率提升爆表的 Xcode 和 LLDB 調試技巧

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

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

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

還有一些開發者會遇到在調試的時候無法顯示變量類型、列印變量資訊的問題如下圖:

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

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

2.1 自動建立調試标簽頁(Configure behaviors to dedicate a tab for debugging)

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

設定自動建立 Debug Tab 方法:頂部導航欄 Xcode -> Behaviors -> Edit Behaviors... -> Runing -> Pauses -> ✅ Show Tab Name

tab name

in

active window

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

在 LLDB 中通過

expression

指令可以改變程式目前的各種狀态,

e

expr

作為簡寫也可以實作同樣的功能。我們用一個簡單的

UILabel

來舉例,為

myLabel

設定一個值 hello , 正常來講視圖上的

myLabel

就應該顯示 hello 。

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

你可以在

myLabel.text = "hello"

這句代碼後設定一個斷點,運作程式執行斷點後,在控制台的 LLDB 調試器 中輸入下面的表達式改變它的值,在繼續運作程式之後,相信你在界面上看到的值一定是 hello world 。

// 改變 myLabel 文案
expr myLabel.text = "hello world"
複制代碼
           

除了改變

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 ,可以自動繼續執行後續代碼,而不會停在這一行。

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

首先,我們了解一下全局斷點,你可以點選在 Breakpoints Navigator 左下角 + 号,然後選擇 Symbolic Breakpoint... ,如下圖,你可以在 Symbol 一欄輸入任何你想監聽的函數比如

[UILabel setText:]

,之後所有頁面下的所有

UILabel

類型對象在設定

text

屬性的時候都會執行該斷點。(ps:我還不是最酷的?)

在這個斷點的控制台中,并沒有顯示變量屬性等資訊,我們怎麼能知道設定了什麼呢?接下來我們可以用

$arg1

$arg2

等指令來列印出我們想要的資訊。

如下圖,在這裡

$arg1

是指對象本身,

$arg2

是對象被調用的函數,

po

指令無法直接輸出函數名,需要加上

(SEL)

$arg3

是被賦給

text

的值。

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

上面我們介紹了全局斷點,它能監測到全局的函數調用,但是我想監測某一個函數内局部區域的函數調用,這個時候我們可以使用

breakpoint set --one-shot true

指令動态生成一個斷點,這個斷點将是一次性的,執行一次後将被自動删除。

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

上圖這個斷點到底幹了什麼?當執行到圖中第 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” )

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

我們通過

thread jump --by 2

指令,跳過了 2 行代碼,如下圖将隻列印 1 和 4 。

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

上面我們介紹了使用全局斷點和一次性斷點對

[UILabel setText:]

函數監聽屬性的變化,其實我們還有另一個選擇, 使用 watchpoints 通過監測記憶體的變化來監聽屬性的變化。

我們可以在

viewDidLoad

函數中設定一個斷點,然後再控制台找到你需要監聽的屬性,如下圖:

選中你想要監聽的屬性後,點選右鍵将彈出下圖視窗,點選 Watch "count"即可監聽屬性 count 的值的改變,如執行

count+=1

。需要注意的是每當重新編譯後指針發生變化,就需要重新設定 watchpoints 。

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

為這句指令建立一個别名,之後就可以通過别名來使用相關操作。

再另一種方式,我們可以使用

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
複制代碼
           

調整模拟器中控件位置的效果:

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 嵌套層級會非常多,讓我們無法快速準确找到我們想檢視的控件所在的層級。

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

3.2 顯示被裁剪的視圖内容(View clipped content)

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

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

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

3.3 在調試中檢視自動布局資訊(Auto Layout debugging)

在調試 Debug View Hierarchy 中檢視控件的限制隻需要啟動 Show Constraints 選項,選中任何一個控件都會顯示出其擁有的限制。

選中限制後可以在右邊欄對象檢查器 Object Inspector 中檢視限制的詳細資訊。

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 一欄你可以看到一個調用棧的清單。如下圖,點選右邊小箭頭可以跳轉到建立該對象的代碼處。

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

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 的檢視模式,選中後背的視圖,如下圖。

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

四、調試深色模式(Debugging Dark Mode)

4.1 切換深色模式(Appearance overrides)

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

在 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 層級結構了。

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,你可以根據場景為該設定設定合适的名字。

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

在 assets catalog 中設定 Named Color:

總結

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

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

參考連結

  • 視訊位址:WWDC 2018 Session 412 - Advanced Debugging with Xcode and LLDB
  • PDF位址:WWDC 2018 Session 412 - Advanced Debugging with Xcode and LLDB
檢視更多 WWDC 18 相關文章請前往 老司機x知識小集xSwiftGG WWDC 18 專題目錄