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 。
二、吐血推薦的調試小技巧(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 Nametab name
in active window
。
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
的值。
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()~~~~~^~~~ ~~~~~~~~~~~~~~~~~~~~
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
「expression CATransaction.flush()」
來刷新一下你的頁面。配合修改 UI 坐標值的命令一起使用,你能看到你的模擬器正在發生令人振奮的一幕。// 修改坐標點po unsafeBitCast(0x7fe439d13160, UILabel.self).center.y = 300// 刷新頁面expression CATransaction.flush()
~/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
<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 memory2. 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])
// 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) ??
- 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的團隊,造了捧在掌心的萌寵機器人