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

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

來自專欄掘金專欄5 人贊了文章

作者:謝濤 天天果園iOS高級開發工程師

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 在 LLBD 中修改 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"

除了改變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 recursiveDescriptionself.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.pycommand 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.pyThe "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

CommandAlias ForSteps TO Evaluatepo <expression>expression --object-description -- <expression>1. Expression: evaluate

2. Expression: debug descriptionpexpression --1. Expression: evaluate

2. Outputs LLDB-formatted descriptionframe variablenone1. Reads value of from memory

2. Outputs LLDB-formatted descriptionp 和 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:0x7fdb1c70a710WWDC 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 2018的文章,請訪問:WWDC18 - 掘金專題頁。

推薦閱讀:

WWDC 2018 Summary
WWDC18 觀後感
英語流利說團隊帶你看 WWDC 2017
一張圖帶你看完 ?WWDC 2018
備受蘋果青睞,曾亮相WWDC的團隊,造了捧在掌心的萌寵機器人

TAG:Xcode | WWDC | 軟體調試 |