現在你已經學習了如何創建斷點, 因此調試器會在你的代碼里停下來, 現在是時候從你調試的程序里獲取一些有用的信息了.
你應該會經常想要查看對象的實例變量. 但是, 你知道嗎你甚至可以通過LLDB執行任意代碼?詳細說就是通過Objective-C的運行時你可以聲明,初始化,并且注入代碼來幫助你理解應用程序.
在本章中你將會學習到expression
命令.這條命令允許你在調試器中執行任意代碼.
格式化p 和 po
你可能很熟悉go-to
這個調試命令.po
這個命令經常用來打印出對象的信息.可以是一個對象的實例變量, 也可以是一個對象的引用, 還可以是一個寄存器里的對象.它甚至可以是內存里任意對象的內存地址.
如果你在LLDB控制臺中查看po
的快速幫助, 你會發現po
實際是一個表達式expression -O --
的縮寫. -o
參數用來打印出對象的description
.po
的另一個兄弟指令p
是省略掉-o
選項的表達式的縮寫expression --
.
p
打印出的信息取決于LLDB type system
.LLDB的值的格式化類型決定了它的輸出并且是完全可以自定義的(后面你就會看到).
是時候學習一下如何用p
和po
獲取他們的內容了.在本章中你依然后使用Signals
項目.
在Xcode中打開Signals
項目.接下來打開MasterViewController.swift
并且在這個類的上方加上下面的代碼:
override var description: String {
return "Yay! debugging " + super.description
}
在viewDidLoad
中的super.viewDidLoad()
下面加上下面的代碼:
print("\(self)")
現在在MasterViewController.swift中在你剛剛添加的打印方法的下面創建一個斷點.
構建并運行APP:
當
Signals
在viewDidLoad()
中停下來的時候, 在LLDB控制臺中輸入下面的代碼:
(lldb) po self
你應該會看到下面這些輸出:
Yay! debugging <Signals.MasterViewController: 0x7f8a0ac06b70>
注意一下print語句的輸出和它與你在調試器中執行po self
輸出的匹配度.
你也可以更進一步. NSObject
有另外一個用來調試的description
方法叫debugDescription
.現在來嘗試實現一下. 在description
變量定義的下面添加以下代碼:
override var debugDescription: String {
return "debugDescription: " + super.debugDescription
}
構建并運行應用程序.當調試器在斷點處停下來的時候, 再次打印self
:
(lldb) po self
LLDB控制臺的輸出看起來應該是下面的樣子:
debugDescription: Yay! debugging <Signals.MasterViewController:
0x7fb71fd04080>
注意看po self
和print self
在你實現了debugDescription
之后的輸出有什么不同. 當你在LLDB中打印一個對象的時候調用的是debugDescription
而不是description
, 注意到了嗎!
正如你看到了, 當NSObject
類或者它的子類有一個description
或者debugDescription
方法的時候會影響到po的輸出.
那么哪些對象需要重寫description
方法呢?你可以簡單的通過image lookup
命令加一個正則表達式捕獲那些重寫了此方法的對象.你在前面章節中學到的內容將要派上用場了.
例如, 如果你想要知道哪些Objective-C類重寫了debugDescription
方法, 你可以通過下面的命令查詢所有這些方法:
(lldb) image lookup -rn '\ debugDescription\]'
根據輸出的內容可以看到, Foundation
框架的作者已經在許多foundation
類型(例如:NSArray)里面添加了debugDescription
, 讓我們在調試的時候更簡單. 此外還有一些私有的類也重寫了debugDescription
方法.
你可以能會注意到在列表里有CALayer
類. 讓我們看一下在CALayer
類中description
和debugDescription
有哪些不同.
在LLDB控制臺中輸入下面的內容:
(lldb) po self.view!.layer.description
你將會看到類似下面的輸出:
"<CALayer: 0x61000022e980>"
只有一點點的信息. 現在輸入下面的內容:
(lldb) po self.view!.layer
你將會看到下面這些輸出:
<CALayer:0x61000022e980; position = CGPoint (187.5 333.5); bounds =
CGRect (0 0; 375 667); delegate = <UITableView: 0x7fdd04857c00; frame =
(0 0; 375 667); clipsToBounds = YES; autoresize = W+H; gestureRecognizers
= <NSArray: 0x610000048220>; layer = <CALayer: 0x61000022e980>;
contentOffset: {0, 0}; contentSize: {375, 0}>; sublayers = (<CALayer:
0x61000022d480>, <CALayer: 0x61000022da60>, <CALayer: 0x61000022d8c0>);
masksToBounds = YES; allowsGroupOpacity = YES; backgroundColor = <CGColor
0x6100000a64e0> [<CGColorSpace 0x61800002c580> (kCGColorSpaceICCBased;
kCGColorSpaceModelRGB; sRGB IEC61966-2.1; extended range)] ( 1 1 1 1 )>
這里有更多的能容-而且更加有用!很顯然Core Animation
的開發者需要通過引用用description
獲取更多更清楚的信息. 但是如果你在調試器中, 你將會看到更多信息.我們并不清楚他們為什么要制造這些不同.可能這些debug description
需要執行大量的計算, 因此他們只在絕對必要的時候才用到.
接下來, 你應該還停留在調試器中, 嘗試執行p self
:
(lldb) p self
你應該會得到一些類似下面的信息:
(Signals.MasterViewController) $R2 = 0x00007fb71fd04080 {
UIKit.UITableViewController = {
baseUIViewController@0 = <extracting data from value failed>
_tableViewStyle = 0
_keyboardSupport = nil
_staticDataSource = nil
_filteredDataSource = 0x000061800024bd90
_filteredDataType = 0
}
detailViewController = nil
}
這看起來可能有點嚇人, 但是讓我們來解析一下.
首先, LLDB輸出了self的類名. 在這里就是Signals.MasterViewController
.緊跟著是一個你可以在LLDB中用來引用這個對象的指針. 在上面的例子中就是$R2
.你的可能是不同的因為這個數字在你使用LLDB的時候是遞增的.
當你在后面的LLDB會話中想要回到這個對象的時候這個引用是非常有用的, 也許你在不同的范圍里self不再是同一個對象.在這里你可以通過R2
引用這個對象. 想知道如何引用, 接著往下看:
(lldb) p $R2
你將會看到同樣的輸出.你會在本章后面的內容里學到更多這種LLDB變量的用法.
在LLDB變量名字的后面是這個對象的地址, 后面跟著一些這個類的明確信息. 在這里, 它顯示UITableViewController
相關的詳情, MasterViewController
的父類, 緊跟著是 detailViewController
的實例變量.
正如你看到的, p
命令輸出的信息和po
命令輸出的信息是不同的.p
的輸出依賴于類型格式, LLDB作者已經添加到Objective-C, Swift, 和其他語言中的每一個內部的數據結構.需要重點注意的是Swift的輸出格式在不同的Xcode發行版中可能有少許的不同.
鑒于類型格式化是LLDB處理的, 如果你想的話你有能力改變它們. 在你的LLDB會話中, 輸入以下命令:
(lldb) type summary add Signals.MasterViewController --summary-string
"Wahoo!"
你已經告訴了LLDB在你打印一個MasterViewController
類的實例的時候你僅僅只想返回靜態字符串, Wahoo!
.Signals
前綴的實質是為Swift
類的鑒于Swift
包含這個模塊類名來防止命名空間沖突. 現在再次嘗試輸出self, 像這樣:
(lldb) p self
輸出看起來應該像下面這個樣子:
(lldb) (Signals.MasterViewController) $R3 = 0x00007fb71fd04080 Wahoo!
這個格式在通過APP啟動的時候會被LLDB記住, 因此要確保你練習完p
命令之后刪除它. 可以用下面的指令在LLDB會話中刪除:
(lldb) type summary clear
輸入p self
將會回到LLDB作者默認的實現方式.類型格式化是一個值得我們在后面章節中詳細討論的話題.因為它可以在你沒有源代碼的情況下幫助你詳細的調試應用程序.
Swift vs Objective-C調試環境
注意到這里有兩個調試環境在調試你的代碼的時候是非常重要的:一個非Swift調試環境和一個swift調試環境.默認情況下, 當你在 Objective-C代碼中停下來的時候, LLDB將會使用非Swift(Objective-C)調試環境, 當你在Swift代碼中停下來的時候, LLDB將會使用Swift調試環境.聽起來很合邏輯, 對吧?
如果你將調試器停在了藍色斷點的外面, LLDB默認情況下將會選擇Objective-C環境.確保你之前在GUI里面創建的斷點依然存在并仍然可用然后構建并運行APP. 當斷點觸發的時候, 在你的LLDB會話里輸入下面的內容:
(lldb) po [UIApplication sharedApplication]
LLDB將會拋出一個錯誤給你:
error: <EXPR>:3:16: error: expected ',' separator
[UIApplication sharedApplication]
^ ,
你已經在Swift代碼中停下來了, 所以你在swift環境中.但是你卻嘗試運行Objective-C的代碼.那是行不通的.類似的在Objective-C環境中運行Swift代碼也是行不通的.
你可以用-l
選項選擇一個語言強制讓表達式使用 Objective-C環境.然而, 由于po
是expression - O --
的縮寫, 你將因為提供在--
后面的參數而不能夠使用po
命令, 這就意味著你將不得不輸入expression
. 在LLDB中, 輸入下面的內容:
(lldb) expression -l objc -O -- [UIApplication sharedApplication]
這里你已經告訴LLDB為Objective-C使用objc
語言.如果必要的話你還可以為 Objective-C++用objc++
.
LLDB將會輸出shared application的引用.嘗試在Swift里做同樣的事情. 既然你已經停在了Swift環境里, 嘗試用Swift的語法打印出UIApplication的引用, 想下面這樣:
(lldb) po UIApplication.shared
你將會得到在Objective-C環境通樣的輸出.輸入continue
繼續運行應用程序, 然后在藍色斷點的外面暫停Signals
項目.
在這里, 按下上箭頭按鈕得到你剛才執行的swift命令并看看發生了什么:
(lldb) po UIApplication.shared
LLDB將會再次拋出一個錯誤:
error: property 'shared' not found on object of type 'UIApplication'
記住, 在藍色斷點外面停下會讓LLDB進入Objective-C環境. 這就是為什么在你嘗試執行Swift代碼的時候會拋出一個錯誤.
你因該時刻注意調試器當前停在什么樣的語言環境里.
用戶定義的變量
正如你之前看到的, LLDB在打印出對象的時候會自動維護局部變量.你同樣也可以創建自己的變量.
從程序里移除所有的斷點構建并運行APP.在藍色斷點外面停止調試器所以默認的是Objective-C環境.在這里輸入:
(lldb) po id test = [NSObject new]
LLDB將會執行這段代碼, 這將會創建一個新的NSObject對象并存儲在test變量里.現在嘗試打印這個對象:
(lldb) po test
你將會得到一個類似下面的錯誤:
error: use of undeclared identifier 'test'
這是因為你需要讓LLDB記住這個變量你就要用到$
修飾符.
再次嘗試聲明test變量在前面加上$
:
(lldb) po id $test = [NSObject new]
(lldb) po $test
<NSObject: 0x60000001d190>
這個變量被創建為Objective-C對象.但是如果你想在swfit環境中訪問這個變量會發生什么呢?嘗試輸入下面的內容:
(lldb) expression -l swift -O -- $test
到現在為止,一直都還不錯.現在嘗試在這個Objective-C類上執行swift風格的代碼.
(lldb) exppression -l swift -O -- $test.description
你將會得到一個類似下面的錯誤:
error: <EXPR>:3:1: error: use of unresolved identifier '$test'
$test.description
^~~~~
如果你在Objective-C環境中創建了一個LLDB變量, 然后轉到了swift環境中, 不要期望一切都會照常工作.隨著時間的流失我們會看到swift和Objective-C通過LLDB橋接的改善.
那么如何在LLDB中創建應用與真實環境的引用?你可以將引用存在一個對象里并執行你選擇的任意代碼. 要看實際效果, 可以在MasterViewController的父視圖控制器里創建一個符號性的斷點, MasterContainerViewController
使用了一個Xcode的符號斷點在viewDidLoad方法里.
在Symbol部分輸入:
Signals.MasterContainerViewController.viewDidLoad () -> ()
要注意參數和返回值之間的空格, 否則斷點是不生效的.
你的斷點看起來應該是下面這個樣子:
構建并運行APP.Xcode現在將會斷點在MasterContainerViewController.viewDidLoad().From there, type the following:
(lldb) p self
鑒于這是你再swift調試環境執行的第一個參數, LLDB將會創建一個變量$R0
.在LLDB中輸入continue
繼續執行程序.
因為執行移到更大和更好的運行循環事件并停留在了viewDidLoad()
里, 所以所以現在你還不能通過使用self
來引用 MasterContainerViewController實例.
但是你仍然有$R0
這個變量!現在你可以應用MasterContainerViewController
甚至可以執行任意代碼來幫助你調試.
手動的將APP暫停在調試器中, 然后鍵入下面內容:
(lldb) po $R0.title
不幸的是, 你將會得到:
error: use of undeclared identifier '$R0'
你將調試器停在了藍色斷點的外面!記住, LLDB默認的是Objective-C環境. 你需要使用-l
選項進入swift環境:
(lldb) expression -l swift -- $R0.title
這將會輸出下面的內容:
(String?) $R1 = "Quarterback"
當然, 這是顯示在導航欄上的視圖控制器的標題.
現在, 輸入下面的內容:
(lldb) expression -l swift -- $R0.title = "! ! ! ! ! "
輸入continue
繼續運行APP.
正如你看到的你可以輕松的操控你想操控的變量.
此外, 你也可以在代碼里創建一個斷點, 執行代碼, 并且在斷點觸發的時候暫停. 如果你正在調試一些事情并且想用特定的輸入執行一個函數看看它是如何執行的時候這是很有用的.
例如, 你仍然有在
viewDidLoad()
里的符號斷點, 所以嘗試執行那個方法去檢查代碼. 暫停程序的執行, 然后輸入:
(lldb) expression -l swift -O -- $R0.viewDidLoad()
什么事都沒有發生. 沒有觸發斷點. 怎么會這樣?事實上, MasterContainerViewController
已經執行了這個方法, 但是在默認情況下, LLDB在執行命令的時候會忽略任何斷點.你可以用-i
選項經用這個功能.
在LLDB控制臺中輸入下面的內容:
(lldb) expression -l swift -O -i 0 -- $R0.viewDidLoad()
現在LLDB會在你之前創建的符號斷點處停下來.這種策略是測試方法邏輯的絕佳方法.例如, 你可以實現測試驅動的調試, 通過給一個函數不同的參數來看看它如何處理不同的輸入.
類型格式化
LLDB中一個好的選項是你可以執行基本數據類型的輸出格式. 這讓LLDB成為了一個學習編譯器是格式化基本的C類型的偉大工具.這在你學習后面的匯編章節的時候是必須知道的.
在LLDB控制臺中輸入下面的命令:
(lldb) expression -G x -- 10
-G
選項告訴LLDB你想要輸出什么樣的格式. G代表著GDB格式. 你可能不知道, GDB是LLDB前一代的調試器. 在這里, 使用的x
代表著十六進制格式.
你將會看到下面的輸出:
(int) $0 = 0x0000000a
這是將十進制的10作為十六進制輸出.
LLDB還有一種指定輸出格式的更短的語法. 輸入下面的命令:
(lldb) p/x 10
你將會看到和前面一樣的輸出. 但是這一次你輸入的內容更少了!
這對于學習C數據類型的表示形式非常有幫助. 例如, 如何用二進制表示數字10呢?
(lldb) p/t 10
/t
指明了二進制形式.你將會看到十進制的10是如何用二進制表示的.
負10又是如何表示的呢?
(lldb) p/t -10
由兩部分組成的10進制的10.夠清楚吧!
浮點數10.0又如何用二進制表示呢?
(lldb) p/t 10.0
這可能派上用場!
字符D
的ASCII值是怎樣的呢?
(lldb) p/d 'D'
所以D
是68!/d
指定的是十進制形式.
最后, 整數后面隱藏的縮寫是什么?
(lldb) p/c 1430672467
/c
指明了字符形式. 它將數字轉換成二進制, 每8位作為一個整體(1字節), 然后將每一個字節都轉換成ASCII字符.在這里, 它有4個字符代碼STFU.
下面是所有輸出格式的列表(可以參考:[https://sourceware.org/gdb/ onlinedocs/gdb/Output-Formats.html](https://sourceware.org/gdb/ onlinedocs/gdb/Output-Formats.html)):
? x: hexadecimal
? d: decimal
? u: unsigned decimal
? o: octal
? t: binary
? a: address
? c: character constant
? f: float
? s: string
如果這個格式對你來說不夠用, 你可以使用LLDB拓展的格式, 但是你將不能夠使用GDB格式語法.
LLDB的格式可以像下面這樣使用:
(lldb) expression -f Y -- 1430672467
這將會輸出下面的內容:
(int) $0 = 53 54 46 55 STFU
這解釋了之前的FourCC代碼!
LLDB擁有下面的格式(可以參考[http://lldb.llvm.org/
varformats.html](http://lldb.llvm.org/
varformats.html)):
? B: boolean
? b: binary
? y: bytes
? Y: bytes with ASCII
? c: character
? C: printable character
? F: complex float
? s: c-string
? i: decimal
? E: enumeration
? x: hex
? f: float
? o: octal
? O: OSType
? U: unicode16
? u: unsigned decimal
? p: pointer
我們為什么要學習這些?
嘗試通過執行help expression
查看其他的expression
選項并查看你可以用它們來做什么.