當你寫一個應用程序,你將不可避免地犯錯誤。 更糟糕的是,您的應用程序設計中會時不時地出現錯誤。 Xcode
的調試器(稱為 LLDB
)是幫助您找到這些錯誤并修復它們的基本工具。 本章介紹了 Xcode
的調試器及其基本功能。
一個 Buggy 項目
您將使用一個簡單的項目來引導你使用 Xcode 調試器。 打開 Xcode 并用 iOS single view application
創建一個新項目。 將項目命名為 Buggy
,并確保將語言設置為 Swift
,設備設置為 iPhone
,并且 Use Core Data
,Include Unit Tests
和 Include UI Tests
均選中(圖9.1)。 點擊 Next
。
圖9.1 配置Buggy
當你寫這個應用程序的代碼時,請記住它是一個錯誤的項目。 您可能會被要求輸入您認為不正確的代碼。 在您輸入時不要修復它; 這些錯誤將幫助您了解調試技巧。
現在就開始吧,打開 Main.storyboard
并將 UIButton 拖動到 View Controller Scene
上。 雙擊新按鈕,將其標題更改為 Tap me!
。仍然選擇此按鈕,打開自動布局 Align
菜單。 選擇 Horizontally in Container
,然后單擊 Add 1 Constraint
。 接下來,打開 Add New Constraints
菜單。點擊到容器頂部的線,選擇 Width
和 Height
,然后單擊 Add 3 Constraints
。
您的結果應如圖9.2 所示,如果您的實際尺寸和間距有所不同,不要擔心。
圖9.2 Tap me 按鈕的自動布局約束!
現在,您需要實現一個方法來觸發此按鈕,然后將其連接到故事板中的按鈕。
打開 ViewController.swift 并實現一個使按鈕觸發的動作方法。
@IBAction func buttonTapped(_ sender: UIButton) {
}
現在打開 Main.storyboard
并 右擊從按鈕拖動到 View Controller
并將其連接到 buttonTapped:
選項。
返回到 ViewController.swift
中,將一個 print() 語句添加到 buttonTapped(_ :) 方法中,以確認該方法是否響應于按鈕點擊而被調用。
@IBAction func buttonTapped(_ sender: UIButton) {
??print("Called buttonTapped(_:)")
}
構建并運行應用程序。 確保按鈕在屏幕上正確顯示,您可以點擊它。 同時確認當您點擊按鈕時,buttonTapped(_ :)
被調用并且有消息打印到控制臺中。
調試基礎
最簡單的調試是使用控制臺。 當應用程序崩潰或需要將信息記錄到控制臺時,解釋控制臺中提供的信息可以讓您觀察并了解代碼的問題。 我們用一些例子來說明控制臺是如何支持你的無 bug 代碼追求的。
解釋控制臺消息
是時候給 Buggy 項目添加一些錯誤。 假設在考慮了一段時間之后,你覺得使用一個開關將比一個按鈕更好。 打開 ViewController.swift
并對 buttonTapped(_ :) 方法進行以下更改。
@IBAction func buttonTapped(_ sender: UIButton) {
@IBAction func switchToggled(_ sender: UISwitch) {
??print("Called buttonTapped(_:)")
}
隨著控件的更改,您重命名了 action 的名字,并將 sender 的類型更改為 UISwitch。
不幸的是,你忘了更新 Main.storyboard
中的界面。 構建并運行應用程序,然后點擊按鈕。 應用程序將崩潰,您將看到一個消息記錄到控制臺,類似于下面的內容。 (我們已經截斷了一些信息以適應頁面。)
2016-08-24 12:52:38.463 Buggy[1961:47078] -[Buggy.ViewController buttonTapped:]:
unrecognized selector sent to instance 0x7ff6db708870
2016-08-24 12:52:38.470 Buggy[1961:47078] *** Terminating app due to uncaught
exception 'NSInvalidArgumentException',
reason: '-[Buggy.ViewController buttonTapped:]: unrecognized selector sent to
instance 0x7ff6db708870'
*** First throw call stack:
(
0 CoreFoundation [...] __exceptionPreprocess + 171
1 libobjc.A.dylib [...] objc_exception_throw + 48
2 CoreFoundation [...] -[NSObject(NSObject) doesNotRecognizeSelector:] + 132
3 CoreFoundation [...] ___forwarding___ + 1013
4 CoreFoundation [...] _CF_forwarding_prep_0 + 120
5 UIKit [...] -[UIApplication sendAction:to:from:forEvent:] + 83
6 UIKit [...] -[UIControl sendAction:to:forEvent:] + 67
7 UIKit [...] -[UIControl _sendActionsForEvents:withEvent:] + 444
8 UIKit [...] -[UIControl touchesEnded:withEvent:] + 668
9 UIKit [...] -[UIWindow _sendTouchesForEvent:] + 2747
10 UIKit [...] -[UIWindow sendEvent:] + 4011
11 UIKit [...] -[UIApplication sendEvent:] + 371
12 UIKit [...] __dispatchPreprocessedEventFromEventQueue + 3248
13 UIKit [...] __handleEventQueue + 4879
14 CoreFoundation [...] __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION
15 CoreFoundation [...] __CFRunLoopDoSources0 + 556
16 CoreFoundation [...] __CFRunLoopRun + 918
17 CoreFoundation [...] CFRunLoopRunSpecific + 420
18 GraphicsServices [...] GSEventRunModal + 161
19 UIKit [...] UIApplicationMain + 159
20 Buggy [...] main + 111
21 libdyld.dylib [...] start + 1
)
libc++abi.dylib: terminating with uncaught exception of type NSException
控制臺中的信息看起來很可怕,難以理解,但它并不像第一次看起來那么槽糕。 真正有用的信息在最頂端。 從第一行開始吧。
2016-08-24 12:52:38.463 Buggy[1961:47078] -[Buggy.ViewController buttonTapped:]:
unrecognized selector sent to instance 0x7ff6db708870
這句話表明了一個時間戳,應用程序的名稱,以及 unrecognized selector sent to instance 0x7ff6db708870
(發送到實例 0x7ff6db708870 的無法識別的選擇器) 語句。 要了解這些信息,請記住,一個 iOS 應用程序可能是用 Swift 編寫的,但它也有可能是構建在 Cocoa Touch 之上,Cocoa Touch 是用 Objective-C 編寫的一系列框架。 Objective-C 是一種動態語言,當一個消息發送到一個實例時,Objective-C 運行時會根據它的選擇器(一種ID)在那個精確的時間點找到要調用的實際方法。
因此,unrecognized selector [was] sent to instance 0x7ff6db708870
的語句意味著應用程序嘗試調用在實例上不存在的方法。
哪一個實例呢? 你有兩條信息。 首先,它是一個 Buggy.ViewController。 (為什么不只是 ViewController? Swift 命名空間包括模塊的名稱,在這種情況下是應用程序的名稱)。其次,它位于內存地址 0x7ff6db708870
(您的實際地址可能會不同)。
表達式 - [Buggy.ViewController buttonTapped:]
是 Objective-C 代碼。 Objective-C 中的消息始終以[recipientselector] 的形式括在方括號中。 接收者(receiver
) 是發送消息的類或實例。 開始方括號前的破折號( - )表示接收器是 ViewController 的一個實例。 (加號(+)表示接收者是本身。)
簡而言之,控制臺的這一行告訴你,選擇器 buttonTapped:
被發送到 Buggy.ViewController
的一個實例,但是選擇器沒有被識別。
消息的下一行由于 “非捕獲的異常” 而添加應用程序終止的信息,并將異常的類型指定為 NSInvalidArgumentException。
控制臺消息的大部分是 堆棧跟蹤(stack trace
),這是調用到應用程序崩潰點的所有函數或方法的列表。 了解應用程序在崩潰之前采用的邏輯路徑可以幫助您解決和修復錯誤。 堆棧跟蹤中的任何一個調用都沒有機會返回,并且它們列在最上面的最近一次調用中。 這就是堆棧跟蹤:
*** First throw call stack:
(
?0 CoreFoundation [...] __exceptionPreprocess + 171
?1 libobjc.A.dylib [...] objc_exception_throw + 48
?2 CoreFoundation [...] -[NSObject(NSObject) doesNotRecognizeSelector:] + 132
?3 CoreFoundation [...] ___forwarding___ + 1013
?4 CoreFoundation [...] _CF_forwarding_prep_0 + 120
?5 UIKit [...] -[UIApplication sendAction:to:from:forEvent:] + 83
?6 UIKit [...] -[UIControl sendAction:to:forEvent:] + 67
?7 UIKit [...] -[UIControl _sendActionsForEvents:withEvent:] + 444
?8 UIKit [...] -[UIControl touchesEnded:withEvent:] + 668
?9 UIKit [...] -[UIWindow _sendTouchesForEvent:] + 2747
?10 UIKit [...] -[UIWindow sendEvent:] + 4011
?11 UIKit [...] -[UIApplication sendEvent:] + 371
?12 UIKit [...] __dispatchPreprocessedEventFromEventQueue + 3248
?13 UIKit [...] __handleEventQueue + 4879
?14 CoreFoundation [...] __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION
?15 CoreFoundation [...] __CFRunLoopDoSources0 + 556
?16 CoreFoundation [...] __CFRunLoopRun + 918
?17 CoreFoundation [...] CFRunLoopRunSpecific + 420
?18 GraphicsServices [...] GSEventRunModal + 161
?19 UIKit [...] UIApplicationMain + 159
?20 Buggy [...] main + 111
?21 libdyld.dylib [...] start + 1
)
列表中的第一行包括一個調用的數字,模塊名稱,一個內存地址(我們已經刪除其余部分以適應頁面),以及表示該函數或方法的符號。 如果從底部向上掃描堆棧跟蹤,您可以感覺到應用程序從 Buggy 的 main 方法開始,在標有 20 號調用的線路上,接收到一個在調用 9 識別為觸摸的事件,然后嘗試 在調用 7 將相應的動作發送到按鈕的目標。沒有找到動作的選擇器(調用2: - [NSObject(NSObject)doesNotRecognizeSelector:]
),導致引發異常(調用1:objc_exception_throw
)。
盡管控制臺消息的這種缺點是只特定于一種錯誤類型,但是從理解這些消息的基本結構可以幫助您了解將來遇到的錯誤消息。 隨著您開發經驗的積累,您會開始將錯誤消息與問題類型相關聯,并且您將更熟練調試代碼。
修復第一個bug
查看 ViewController.swift
,您發現您將您的動作方法從 buttonTapped(_ :) 更改為 switchToggled(_ :),這就是選擇器 buttonTapped:
未被識別的原因。
要修復錯誤,您有兩個選擇。 您可以更新連接到 Main.storyboard
上按鈕的 動作,以匹配您的新動作方法。 或者您可以在 switchToggled(_ :) 方法上還原名稱更改。 你決定反正不一定要用 switch,所以打開 ViewController.swift 并將你的方法改回它的早期實現。 (記住我們告訴你的是:盡可能完整地進行更改,即使您看到問題。)
@IBAction func switchToggled(_ sender: UISwitch) {
@IBAction func buttonTapped(_ sender: UISwitch) {
??print("Called buttonTapped(_:)")
}
構建并運行應用程序。 它運行正常嗎?還是有問題? 其實這里有一個問題,你將在下一節中解決。
Caveman 調試
ViewController 的 buttonTapped(_ :) 方法的當前實現只是將一個語句記錄到控制臺。 這是一個被稱為 caveman調試(caveman debugging
) 的技術的例子:在代碼中策略性地放置 print()
調用,以驗證函數和方法是否被調用(并以正確的順序調用),并將變量值記錄到控制臺以關注重要數據。
Caveman 調試 并不像名字(譯:穴居人)所暗示的那樣過時,現代開發人員仍然依賴于記錄到控制臺的消息。
為了探索 caveman調試 可以為您做什么,請在 ViewController.swift
中調用 buttonTapped(_ :) 時記錄 sender 控件的狀態。
@IBAction func buttonTapped(_ sender: UISwitch) {
??print("Called buttonTapped(_:)")
??// Log the control state:
??print("Is control on? \(sender.isOn)")
}
在本書中所寫的 @IBAction 方法中,您已經傳遞了一個稱為 sender 的參數。 此參數是發送消息的
控件(control
) 的引用??丶?UIControl 的一個子類; 您使用過幾個 UIControl 子類了,包括 UIButton,UITextField 和 UISegmentedControl。 在 buttonTapped(_ :) 的用法中可以看到,在這種情況下,sender
是 UISwitch 的一個實例。 isOn 屬性是一個布爾值,指示 switch 實例是否處于 on 狀態。
構建并運行應用程序。 嘗試點擊按鈕。哎呀! 您再次出現無法識別的選擇器錯誤。
Called buttonTapped(_:)
2016-08-30 09:30:57.730 Buggy[9738:1177400] -[UIButton isOn]:
unrecognized selector sent to instance 0x7fcc5d104cd0
2016-08-30 09:30:57.734 Buggy[9738:1177400] *** Terminating app due to uncaught
exception 'NSInvalidArgumentException', reason: '-[UIButton isOn]: unrecognized
selector sent to instance 0x7fcc5d104cd0'
控制臺消息以被調用的 buttonTapped(_ :) 行開頭,表示該操作確實被調用。 但是應用程序崩潰,因為 isOn 選擇器被發送到 UIButton 的實例。
你可能會看到這個問題:sender 在 buttonTapped(_ :) 中鍵入一個 UISwitch,但該操作實際上是附加到 Main.storyboard 中的 UIButton 實例。
要確認此假設,請在調用 isOn 屬性之前記錄 sender
在 ViewController.swift 中的地址。
@IBAction func buttonTapped(_ sender: UISwitch) {
??print("Called buttonTapped(_:)")
??// Log sender:
??print("sender: \(sender)")
??// Log the control state:
print("Is control on? \(sender.isOn)")
}
構建并運行應用程序一次。 點擊按鈕并崩潰應用程序后,請檢查控制臺日志的前幾行,其內容如下所示:
Called buttonTapped(_:)
sender: <UIButton: 0x7fcf8c508bb0; frame = (160 84; 55 30); opaque = NO;
autoresize = RM+BM; layer = <CALayer: 0x618000220ea0>>
2016-08-30 09:45:00.562 Buggy[9946:1187061] -[UIButton isOn]: unrecognized selector
sent to instance 0x7fcf8c508bb0
2016-08-30 09:45:00.567 Buggy[9946:1187061] *** Terminating app due to uncaught
exception 'NSInvalidArgumentException', reason: '-[UIButton isOn]: unrecognized
selector sent to instance 0x7fcf8c508bb0'
在調用 buttonTapped(_ :) 后的行中,您將獲得有關 sender
的信息。 如預期的那樣,它是 UIButton 的一個實例,它存在于地址為 0x7fcf8c508bb0
的內存中。 進一步下來,您可以確認這是您發送 isOn 消息的相同實例。 按鈕無法響應 UISwitch 屬性,因此應用程序崩潰。
要解決此問題,請更正 ViewController.swift
中的 buttonTapped(_ :) 定義。 在那個方法中,刪除額外的你不需要再次調用的 print() 方法。
@IBAction func buttonTaped(_ sender: UISwitch) {
@IBAction func buttonTaped(_ sender: UIButton) {
??print("Called buttonTapped(_:)")
??// Log sender:
??//print("sender: \(sender)")
??// Log the control state:
??//print("Is control on? \(sender.isOn)")
}
Swift 有四個文字表達式,可以幫助您將信息記錄到控制臺(表9.1):
表9.1用于調試的文字表達式
值 | 類型 | 說明 |
---|---|---|
#file | String | 表達式所在的文件名稱。 |
#line | Int | 表達式所在的行號。 |
#column | Int | 表達式開始的列號。 |
#function | String | 表達式所在的方法名稱。 |
為了說明這些文字表達式的使用,請在 ViewController.swift
中的 buttonTapped(_ :) 方法中更新對 print() 的調用。
@IBAction func buttonTapped(_ sender: UIButton) {
??print("Called buttonTapped(_:)")
??print("Method: \(#function) in file: \(#file) line: \(#line) called.")
}
構建并運行應用程序。 當您點擊按鈕時,您將看到一條記錄到控制臺的消息,類似于下面。
Method: buttonTapped in file: /Users/juampa/Desktop/Buggy/Buggy/ViewController.swift at line: 13 was called.
雖然 caveman 調試是有用的,但請注意,在構建您的項目以進行發布時,print()
語句依然在代碼中。
Xcode 調試器:LLDB
要繼續進行調試實驗,您將為應用程序添加另一個錯誤。 將下面的代碼添加到 ViewController.swift
。 請注意,您將使用一個 NSMutableArray,即 Objective-C 中類似于 Swift 的 Array 的對象,使錯誤更難找到。
@IBAction func buttonTapped(_ sender: UIButton) {
??print("Method: \(#function) in file: \(#file) line: \(#line) called.")
??badMethod()
}
func badMethod() {
??let array = NSMutableArray()
??for i in 0..<10 {
????array.insert(i, at: i)
??}
??// Go one step too far emptying the array (notice the range change):
??for _ in 0...10 {
????array.remove(at: 0)
??}
}
構建并運行應用程序以確認點擊按鈕會導致應用程序崩潰,并導致未知的 NSRangeException 異常。 使用您新獲得的知識盡可能多地學習和解釋錯誤信息。
如果您使用 Swift Array 類型來創建此 bug,Xcode將能夠突出顯示導致異常的代碼行。 因為你使用了一個 NSMutableArray,引發異常的代碼在 Cocoa Touch
框架之內。 調試時通常是這種情況; 問題不是很明顯,你需要做一些調查工作。
設置斷點
假設你不知道崩潰的直接原因。 您只需知道在點擊應用程序的按鈕后才會發生。 一個合理的方法是在點擊按鈕并逐步執行代碼直到應用程序終止,直到找到問題的線索。
打開 ViewController.swift
。 要停止代碼中指定位置的應用程序,您可以設置 斷點(breakpoint)
。 設置斷點的最簡單的方法是單擊編輯器窗格左邊的要在執行停止的行旁邊的溝槽。 嘗試一下:點擊 @IBAction func buttonTapped(_ sender:UIButton){
行的左邊 。 將出現一個指示新斷點的藍色標記(圖9.3)。
圖9.3設置斷點
設置斷點后,可以直接點擊藍色標記來切換。 如果您點擊標記一次,它將被禁用,由較淺的藍色指示(圖9.4)。
圖9.4禁用斷點
再一次點擊將重新啟用斷點。 您還可以通過右擊標記來啟用,禁用,刪除或編輯斷點。 將出現一個上下文菜單,如圖9.5所示。
圖9.5修改斷點
在Xcode的左窗格中打開斷點導航器選擇 Reveal in Breakpoint Navigator
,其中包含應用程序中所有斷點的列表(圖9.6)。 您還可以通過單擊導航器選擇器中的圖標來打開斷點導航器。
圖9.6斷點導航器
單步調試代碼
確保在 buttonTapped(_ :) 方法中設置了斷點并且在所有在上一節修改后所有斷點都處于啟用狀態。 運行應用程序,然后點擊按鈕。
您的應用程序命中斷點并停止執行,Xcode
會將您轉到接下來要執行的代碼行,以綠色突出顯示。 它還會打開一些新的信息區域(圖9.7)。
圖9.7 Xcode 在斷點處停止
您熟悉控制臺,并已看到調試導航器。 這里的新區域是變量視圖和調試欄,它們與控制臺一起組成調試區域。 (如果看不到變量視圖,請單擊調試區域右下角的
圖標。)
變量視圖可以幫助您發現斷點范圍內的變量和常量的值。 然而,試圖找到一個特定的值可能需要大量的挖掘。
首先,您將在變量視圖中列出的所有信息中看到傳遞給 buttonTapped(_ :) 方法的 sender
和 self
參數。 單擊 sender
的閉合三角形,您將看到它包含一個 UIKit.UIControl
屬性。 其中有一個 _targetActions
數組,其中包含按鈕的附加目標動作對。
打開 _targetActions
數組,打開第一項([0]
),然后選擇 _target
屬性。 在選擇 _target
的同時敲擊空格鍵,然后會打開快速查看窗口,顯示變量的預覽(這是 ViewController 的一個實例)。 快速查看如圖9.8所示。
圖9.8 檢查變量視圖中的變量
在與 _target
相同的部分中,您將看到 _selector
。 旁邊,你會看到 (SEL)“buttonTapped:”
。 (SEL)
表示這是一個選擇器, “buttonTapped:”
是選擇器的名稱。
在這個例子中,它并沒有幫助你挖掘找到 _target
和 _action
; 然而,一旦開始使用更大,更復雜的應用程序,使用變量視圖就可能特別有用。 您需要知道您正在尋找的內容,例如 _target
和 _action
,找到您感興趣的值可以幫助您跟蹤錯誤。
現在是開始調試代碼的時候了。 您可以使用調試欄上的按鈕進行此操作,如圖9.9所示。
圖9.9 調試欄
調試欄中的重要按鈕有:
-
Continue program execution
——恢復程序的正常執行 -
Step over
——執行一行代碼,而不需要輸入任何函數或方法調用 -
Step into
——執行下一行代碼,包括輸入函數或方法調用 -
Step out
——繼續執行,直到當前的函數或方法退出
單擊按鈕
badMethod()
行(不執行此行)。 請注意,您不要進入 print() 方法——因為它是一個 Apple 編寫的方法,那里不會出現問題。
當 badMethod() 突出顯示時,單擊按鈕
當您逐步執行代碼時,您可以暫停鼠標懸停在 i
和 array.remove
以查看其值更新(圖9.10)。
圖9.10 檢查變量的值
一旦應用程序崩潰,您將確認是在 badMethod() 方法中發生崩潰。 有了這個,您現在可以在 func buttonTapped(_ sender: UIButton)
行上刪除或禁用斷點。
要刪除斷點,右擊它并選擇 Delete Breakpoint。 您還可以通過將藍色標記拖出溝槽來刪除斷點,如圖9.11所示。
圖9.11 拖動標記以刪除斷點
有時候,您希望在觸發代碼行時收到通知,但您不需要任何其他信息或應用程序在遇到該行時暫停。 要完成此操作,您可以向斷點添加聲音,并在觸發后自動繼續執行。
在 badMethod() 方法的 array.insert(i,at:i)
行中添加一個新的斷點。 然后右擊,然后選擇 Edit Breakpoint...
。 單擊 Add Action
按鈕,然后從彈出菜單中選擇 Sound
。 最后,勾選 Automatically continue after evaluating actions
(圖9.12)。
圖9.12 啟用特殊操作
您已配置斷點來發出警報聲音,而不是每次遇到斷點就停止執行。 再次運行應用程序,然后點擊按鈕。 你應該聽到一連串的聲音,然后應用程序將崩潰。
似乎應用程序正在安全地完成 for 循環,但是您需要確定。 找到并右擊斷點標記,選擇 Edit Breakpoint...
。 在編輯器彈出窗口中,單擊聲音操作右側的 +
添加新動作。
從彈出窗口中選擇 Log Message
。 在 Text
字段中, 輸入 Pass number %H
(%H是 斷點命中計數(breakpoint hit count
),對遇到斷點次數的引用)。 最后,確保選中 Log message to console
(圖9.13)。
圖9.13 將多個動作分配給斷點
再次運行應用程序,然后點擊按鈕。 您將再次聽到一 連串的聲音,并且應用程序將像以前一樣崩潰。 但是這一次,如果您觀看控制臺(或者在應用程序崩潰后向上滾動),您將看到遇到斷點 10 次。 這證實您的代碼正確地完成循環。
刪除當前的斷點,并在 array.remove(at:0)
行上添加一個新的斷點。 編輯斷點以記錄 pass number,并像以前一樣自動繼續(圖9.14)。
圖9.14 添加日志斷點
運行應用程序并點擊按鈕。 當它崩潰時,在控制臺中向上滾動,您將看到第二個斷點執行了 11 次。 它還解釋了在應用程序崩潰時在控制臺上記錄的 NSRangeException。 仔細閱讀控制臺上的崩潰日志,盡可能多地了解它。
在解決問題之前,花點時間探索幾個更多的調試策略。 首先,禁用或刪除應用程序中的任何剩余斷點。
在這些簡單的例子中,您已經知道在哪里找到代碼中的錯誤,但是在現實世界的開發中,您通常不會知道應用程序中錯誤隱藏在哪里。 如果您可以知道哪一行代碼導致崩潰的未捕獲異常,那就很 牛叉 了。
異常斷點(exception breakpoint
) 是很有用的,你可以做到這一點。 打開斷點導航器,然后單擊窗口左下角的 +
。 從上下文菜單中 Exception Breakpoint...
。 創建一個新的異常斷點,并顯示一個彈出窗口。 確保它捕獲拋出的所有異常,如圖9.15所示。
圖9.15 添加異常斷點
按鈕,直到崩潰。
這個策略是開始處理一個新錯誤的策略。 事實上,許多程序員在開發過程中始終保持異常斷點激活。 為什么我們讓你等待這么久才能使用? 因為如果您已經開始使用異常斷點,那么您就不需要了解其他調試策略,也可以使用它們。 現在你可以把之前的斷點都刪了,你已經不再需要它們了。
你將嘗試一個最終的技術:symbolic breakpoint
(標志斷點?)。 這不是由行號指定的斷點,而是由函數或方法的名稱——(標志)symbol
所指定。 當標志被調用時,標志斷點被觸發——無論標志在代碼中還是在沒有代碼的框架中。
在斷點導航器中添加一個新的標志斷點,方法是單擊左下角的 +
按鈕,從上下文菜單中選擇 Symbolic Breakpoint...
。 在彈出窗口中,指定 “badMethod” 作為符號,如圖9.16所示。 這意味著每次調用 badMethod() 時,應用程序都將停止。
圖9.16 添加標志斷點
運行應用程序來測試斷點。 應用程序應該在你點擊 Tap Tap!按鈕后在 badMethod() 中停止。
在現實世界的應用程序中,您很少會在方法中使用標志斷點; 你可能會添加一個正常的斷點,就像你在本章前面看到的一樣。 標志斷點對于中止非你編寫的方法最為有用,例如 Apple 的框架之一。 例如,當您想知道應用程序中的任何視圖控制器是否都能觸發方法 loadView()。
最后修復bug。
func badMethod() {
??let array = NSMutableArray()
??for i in 0..<10 {
????array.insert(i, at: i)
??}
??Go one step too far emptying the array (notice the range change):
??for _ in 0...10 {
??for _ in 0..<10 {
????array.remove(at: 0)
??}
}
LLDB 控制臺
Xcode 的 LLDB調試器 的一個很大的特點是它有一個命令行界面。 控制臺區域不僅用于讀取消息,還可用于鍵入 LLDB 命令。 只要您在控制臺上看到藍色 (lldb)
提示,調試器命令行界面就會處于活動狀態。
確保 badMethod() 上的標志斷點仍然處于活動狀態,運行該應用程序,然后點擊該按鈕即可在該點斷開。 看看控制臺,你會看到 (lldb)
提示符(圖9.17)。 單擊提示旁邊,然后鍵入命令。
圖9.17 控制臺上的(lldb)提示符
最有用的 LLDB 命令之一是 print-object
,縮寫為 po
。 此命令用于打印實例。 嘗試通過在控制臺上輸入。
(lldb)
po self
<Buggy.ViewController: 0x7fae9852bf20>
對命令響應的 self
是 ViewController 的一個實例。 現在用命令 step;
執行一行代碼; 這將初始化數組常量引用。 用 po
打印引用的值。
(lldb)
step
(lldb)
po array
0 elements
0 elements
響應不是很有用,因為它不給你詳細信息。 print
命令縮寫為 p
,可以更詳細。 嘗試一下。
(lldb)
p array
(NSMutableArray) $R3 = 0x00007fae98517c00 "0 values" {}
通常,使用帶有 print
或 print-object
的控制臺來檢查變量比 Xcode 的變量視圖窗格更加方便。
另一個有用的LLDB命令是 expression
,縮寫為 expr
。 此命令允許您輸入 Swift 代碼來修改變量。 例如,將一些數據添加到數組,查看內容,然后繼續執行。
(lldb)
expr array.insert(1, at: 0)
(lldb)
p array
(NSMutableArray) $R5 = 0x00007fae98517c00 "1 value" {
?[0] = 0xb000000000000013 Int64(1)
}
(lldb)
po array
? 1 element
?- [0] : 1
?(lldb)
continue
也許更令人驚訝的是,您還可以使用 LLDB 表達式更改 UI。 嘗試將按鈕的 tintColor
更改為紅色。
(lldb)
expr self.view.tintColor = UIColor.red
(lldb)
continue
還有很多 LLDB 命令。 要了解更多信息,請在 (lldb)
提示符下輸入 help
命令。