WWDC 2018 Session 412 : Advanced Debugging with Xcode and LLDB程序員
在程序員寫 bug 的職業生涯中,只有 bug 會永遠陪伴着你,如何處理與 bug 之間的關係,是每一位程序員的必修課。特別是入門程序員常常受 bug 的影響,熬夜加班壓力大,長痘長胖還脫髮。express
每一位 iOS 和 macOS 開發者都是幸運的,由於蘋果的 Xcode 和 LLDB 調試工具,這是每一位開發者應該使用的調試神器,能夠幫助咱們更快地解決問題。本文將主要講解 Xcode 的 斷點調試 、LLDB 調試器 以及 視圖結構調試(UI Hierarchy)的使用技巧,這些技巧將大幅減小調試中從新編譯的次數,減小你的等待時間。xcode
這些技巧使用起來很是簡單,並且在開發場景很是實用,每一位開發者都有必要掌握這些技巧。bash
相信不少開發者在使用 Swift 的時候,調試過程當中的一些問題會讓你很頭痛。 好比說下面這個問題,LLDB 在 AST Context 重建編譯狀態時,有些時候在複雜的狀況下可能沒法檢測到部分模塊的變化,因而調試器提示Failed to get module from AST context
。app
在 Xcode 10 中,爲了應對這個問題,會爲當前的 frame 調用棧建立一個新的 expression evaluator 。ide
還有一些開發者會遇到在調試的時候沒法顯示變量類型、打印變量信息的問題以下圖:函數
蘋果針對大量的錯誤報告進行追蹤,在 Xcode 10 中修復了這個 bug ,調試信息中將再也不會出現此類錯誤。工具
想必你常常在看代碼的時候因爲執行到斷點而被強行切換到斷點所在的頁面,在斷點頁面和以前頁面進行切換的體驗是很是差的。如今你能夠設置在被斷點的時候自動新建一個標籤頁,經過切換標籤頁你能夠快速便捷地切回到以前瀏覽的頁面。佈局
設置自動新建 Debug Tab 方法:頂部導航欄 Xcode -> Behaviors -> Edit Behaviors... -> Runing -> Pauses -> ✅ Show Tab Name tab name
in active window
。post
在 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()
複製代碼
除了直接在控制檯經過 LLDB 調試器修改 App 狀態,你還能夠經過在斷點中添加命令來實現一樣的功能。並且經過斷點來設置調試命令的方式更加方便實用,幾乎是實時插入代碼的功能。
以下圖,設置一個斷點,經過 Edit Breakpoint... 打開編輯框,你能夠將多個不一樣的調試命令按順序填入 Action 中,就能實現以前一樣的功能。另外你能夠勾選 Automatically continue after evaluationg actions ,能夠自動繼續執行後續代碼,而不會停在這一行。
首先,咱們瞭解一下全局斷點,你能夠點擊在 Breakpoints Navigator 左下角 + 號,而後選擇 Symbolic Breakpoint... ,以下圖,你能夠在 Symbol 一欄輸入任何你想監聽的函數好比[UILabel setText:]
,以後全部頁面下的全部UILabel
類型對象在設置text
屬性的時候都會執行該斷點。(ps:我還不是最酷的😎)
在這個斷點的控制檯中,並無顯示變量屬性等信息,咱們怎麼能知道設置了什麼呢?接下來咱們能夠用$arg1
、$arg2
等命令來打印出咱們想要的信息。
以下圖,在這裏$arg1
是指對象自己,$arg2
是對象被調用的函數,po
命令沒法直接輸出函數名,須要加上(SEL)
,$arg3
是被賦給text
的值。
上面咱們介紹了全局斷點,它能監測到全局的函數調用,可是我想監測某一個函數內局部區域的函數調用,這個時候咱們可使用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"
是不會暫停的。
首先咱們看如何經過拖拽指令指針來,跳過一段代碼不執行。以下圖,直接拖拽紅色箭頭指向的按鈕,拖到哪從哪裏開始執行,往上拖能夠重複執行以前的代碼,往下拖將不執行中間被跳過的代碼。
咱們經過thread jump --by 2
命令,跳過了 2 行代碼,以下圖將只打印 1 和 4 。
上面咱們介紹了使用全局斷點和一次性斷點對[UILabel setText:]
函數監聽屬性的變化,其實咱們還有另外一個選擇, 使用 watchpoints 經過監測內存的變化來監聽屬性的變化。
咱們能夠在viewDidLoad
函數中設置一個斷點,而後再控制檯找到你須要監聽的屬性,以下圖:
選中你想要監聽的屬性後,點擊右鍵將彈出下圖窗口,點擊 Watch "count"便可監聽屬性 count 的值的改變,如執行count+=1
。須要注意的是每當從新編譯後指針發生變化,就須要從新設置 watchpoints 。
在平常調試中,使用 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
複製代碼
你能夠在控制檯經過 LLDB 調試器中改變 UI 的座標值,但你並不能當即看到頁面有任何改變。事實上你確實修改了它的值,你只是須要使用「expression CATransaction.flush()」
來刷新一下你的頁面。
配合修改 UI 座標值的命令一塊兒使用,你能看到你的模擬器正在發生使人振奮的一幕。
// 修改座標點
po unsafeBitCast(0x7fe439d13160, UILabel.self).center.y = 300
// 刷新頁面
expression CATransaction.flush()
複製代碼
當你對 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
複製代碼
調整模擬器中控件位置的效果:
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 調用棧的的全部變量。
在開發中咱們會頻繁使用到 Debug View Hierarchy 查看當前頁面視圖結構,正常狀況下導航欄的 UI 嵌套層級會很是多,讓咱們沒法快速準確找到咱們想查看的控件所在的層級。
其實 Xcode 已經有快捷方式可讓你快速定位到控件在導航欄中的位置,首先點擊選中你須要查看的控件,而後再導航欄中的 navigate 選項,展開後選擇 Reveal in Debug Navigator ,以下圖:
當咱們遇到這樣一個顯示不全的 bug 的時候,咱們能夠用到 Debug View Hierarchy 查看當前視圖具體狀況,進入調試頁面你會看到下面這種狀況:
我想個人 label 應該是完整的,可是超出頁面被裁剪掉了,這個時候我須要確認一下事實是否是和我想的同樣。以下圖,咱們須要開啓 Show Clipped Content 選項。
最後我看到了真相和我猜想的是一致的,我能夠根據真實狀況準確制定出解決方案。
在調試 Debug View Hierarchy 中查看控件的約束只須要啓動 Show Constraints 選項,選中任何一個控件都會顯示出其擁有的約束。
選中約束後能夠在右邊欄對象檢查器 Object 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 模式開啓此功能。
在視圖調試模式中,咱們有時候也會須要在 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) 🚽
複製代碼
在調試中,你要選擇的視圖被另外一個視圖遮擋住的狀況下,你能夠經過 3D 的查看模式,選中後背的視圖,以下圖。
可是這種方式實在難稱優雅,何況還有一些刁鑽的角度會讓你很是頭疼。在 2D 的狀況下,正確的選中方式應該是 ⌘-click 直接選中背後被遮擋的視圖,快去試試看吧。
在 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 外觀而改變顏色。
咱們的 UI Hirerachey 同時只能顯示一個 UIWindow 的內容,全部在調試的時候,彈出的 UIWindow 並不會和頁面內的 UI 結構一塊兒展現給咱們,像 UIAlertView 這種彈出 UIWindow 就沒法一塊兒顯示。
若是咱們須要查看彈出 UIWindow , 咱們須要把左邊欄當前的文件結構所有關閉收起,這個時候你會看到 ViewController 所在的 UIWindow 下面還有另一個 UIWindow ,選中以後就能夠查看彈出的 UIWindow 的 UI 層級結構了。
在 UI Hierarchy 調試中咱們能夠在右邊欄的檢查器中查看 Dark Mode 相關信息,選中一個 UILabel 能夠查看該 label 的 Text Color 屬性。在 Dark Mode 下一共有 3 中類型顏色:
下圖中的 Text Color 就是在 assets catalog 中設置的 Named Color ,設置的名字爲 titleColor,你能夠根據場景爲該設置設置合適的名字。
以下圖,檢查器偏下的位置 View 一欄中,咱們能夠找到 Appearance 和 Effective 屬性,Appearance 是表示該視圖下子視圖沒法切換的固定的外觀顏色選擇,Effective 是當前生效的外觀顏色。
在 assets catalog 中設置 Named Color:
功能強大的 LLDB ,特別是配合 BreakPoint 一塊兒使用,讓咱們有了更多的想象空間,加上愈來愈好用的 UI Hirerachey ,讓咱們的調試手段更加靈活。 這些內容雖然須要花一些時間去了解,但我相信掌握這些技巧將會爲你節省下更多的時間。
今後你不再用爲下班前測出 bug 而焦慮了,早用上,早收工,最多幹到下午 3 點鐘。但願本文內容對每一位讀者有所幫助。
查看更多 WWDC 18 相關文章請前往 老司機x知識小集xSwiftGG WWDC 18 專題目錄