讀古今文學網 > iOS編程基礎:Swift、Xcode和Cocoa入門指南 > 9.5 調試 >

9.5 調試

調試就是在應用運行時尋找其問題的技術。我將這種技術分為兩個大的類別:原始調試與暫停運行中的應用。

9.5.1 原始調試

原始調試需要修改代碼,這通常是臨時的,一般是添加一些代碼向控制台輸出一些信息。可以通過調試窗格查看控制台;第6章介紹了如何在自己的頁簽中顯示控制台的技術。

用於向控制台發送消息的標準Swift命令是print函數。借助Swift的字符串插值與CustomStringConvertible協議(需要一個description屬性;參見第4章),可以向print調用提供大量有價值的信息。Cocoa對像通常都有內建的description屬性實現。比如:


print(self.view)
  

控制台的輸出如下所示(我已經對輸出格式化了,便於查看):


<UIView: 0x79121d40;
  frame = (0 0; 320 480);
  autoresize = RM+BM;
  layer = <CALayer: 0x79121eb0>>
  

從中可以看到對像所屬的類,其內存地址(用於判斷兩個實例是否是相同的實例),以及其他一些屬性的值。

如果導入了Foundation(在實際的iOS編程中都會導入的),那就可以使用NSLog C函數了。它接收一個NSString作為格式化字符串,後跟格式化參數。格式化字符串是個包含符號的字符串,這裡的符號叫作格式化說明符,其值(格式化參數)會在運行期被替換。所有的格式化說明符都以一個百分號(%)開頭,因此在格式化字符串中輸入百分號字面值的唯一方法就是使用兩個百分號(%%)。百分號後面的字符指定了運行期需要提供的值類型。最常見的格式化說明符是%@(對像引用)、%d(int)、%ld(long),以及%f(double)。比如:


NSLog(\"the view: %@\", self.view)
  

在該示例中,self.view是第一個,也是唯一一個格式化參數,因此在將格式化字符串輸出到控制台時,其值會被第一個,也是唯一一個格式化說明符%@所替換:


2015-01-26 10:43:35.314 Empty Window[23702:809945]
  the view: <UIView: 0x7c233b90;
    frame = (0 0; 320 480);
    autoresize = RM+BM;
    layer = <CALayer: 0x7c233d00>>
  

我喜歡NSLog的輸出,因為它提供了當前的時間與日期,還有進程名、進程ID以及線程ID(有助於確定兩條日誌語句是否由相同的線程所調用)。此外,NSLog是線程安全的,而print則不是。

要想查詢格式化字符串中可用的全部格式化說明符,請閱讀Apple的文檔String Format Specifiers(在String Programming Guide中)。格式化說明符在很大程度上是基於C printf標準庫函數的。

使用NSLog(或其他格式化字符串)時常犯的錯誤就是提供的格式化參數數量與字符串中格式化說明符的數量不一致,或提供的參數值與相應的格式化說明符所聲明的類型不一致。我常發現初學者說日誌輸出的值沒有意義,而實際上卻是其NSLog調用是沒有意義的;比如,格式化說明符是%d,而相應的參數值卻是個浮點型。另一個常犯的錯誤是將NSNumber看作它所包含的數字類型;NSNumber並不是任何一種數字類型,它是個對象(%@)。諸如有符號與無符號整數、32位與64位數字之類的問題都很棘手。

C結構體並非對象,因此它們無法提供description。不過,Swift擴展了最常見的一些C結構體,並形成了Swift結構體,這樣就可以使用print輸出了。比如,下面這樣做是可以的:


print(self.view.frame) // (0.0,0.0,320.0,480.0)
  

不過,你不能對NSLog這麼做。出於這個原因,常見的Cocoa結構體通常都帶有一些便捷函數,用於將其轉換為字符串。比如:


NSLog(\"%@\", NSStringFromCGRect(self.view.frame)) // {{0, 0}, {320, 480}}   

Swift定義了4個特殊的字面值,這在記錄日誌時非常有用,因為它們描述了自己在外部文件中的位置:__FILE__、__LINE__、__COLUMN__與__FUNCTION__。

在發佈應用時需要刪除日誌調用,因為不能讓最終的應用向控制台輸出不必要的信息。一個技巧就是將自定義的全局函數放到Swift的print函數前:


func print(object: Any) {
    Swift.print(object)
}
  

如果不需要記錄日誌,那麼只需註釋掉第2行即可:


func print(object: Any) {
    // Swift.print(object)
}
  

如果希望這一切是自動進行的,那麼可以使用條件編譯。Swift的條件編譯還不夠強大,不過對於這件事已經足夠了。比如,我們可以讓函數體依賴於DEBUG標記:


func print(object: Any) {
    #if DEBUG
        Swift.print(object)
    #endif
}
  

上述代碼依賴於並不存在的DEBUG標記。請在目標構建設置中創建它,位於Other Swift Flags下。定義DEBUG標記的值是-D DEBUG。如果為Debug配置定義它,但不為Release配置定義(如圖9-5所示),那麼調試構建(在Xcode中構建並運行)就會通過print輸出日誌,但發佈構建(歸檔並提交到App Store)則不會。

圖9-5:定義Swift標記

原始調試另一種很有用的形式是有意中止應用,因為某些地方出現了嚴重的問題。請參見第5章關於assert、precondition與fatalError的介紹。precondition與fatalError甚至可以用於發佈構建。在默認情況下,assert在發佈構建中永遠不會失敗,因此在發佈應用時將其留在代碼中是不會產生什麼問題的;當然,那時你應該胸有成竹地說,斷言所檢測的各種問題都已經在調試階段解決掉了,不會再發生了。

純粹主義者可能會嘲笑原始調試,不過我會經常用到它:它很簡單,給出的信息量足夠大,並且輕量級。有時它也是唯一的辦法。與調試器不同,控制台日誌可用於任何構建配置(調試或發佈),無論應用運行在哪裡都可以(模擬器或物理設備)。即便沒法暫停,它也可以正常使用(比如,由於線程問題)。它甚至可用在物理設備上,比如,測試人員的設備上。對於測試者,查看控制台並將信息發給你可能有點麻煩,不過也是可以做到的:比如,測試者可以將設備連接到計算機上並在Xcode的設備窗口中查看日誌。

9.5.2  Xcode調試器

當在Xcode中構建和運行時,你可以在調試器中暫停並使用Xcode的調試功能。重點在於,如果想要使用調試器,那麼你應該使用調試構建配置來構建應用(這也是方案中Run動作的默認配置)。如果使用了發佈構建配置來構建應用,那麼調試器就沒什麼用了,因為編譯器優化會破壞編譯後的代碼與代碼行之間的對應關係。

1.斷點

在Xcode中調試和運行之間並沒有什麼大的差別;主要的不同在於斷點是有效的,還是會被忽略。斷點的效果可以在兩個層級間切換:

全局(激活與未激活)

總的來說,斷點分為激活與未激活兩種狀態。如果斷點處於未激活狀態,那就無法暫停任何斷點。

個體(啟用與禁用)

任何給定的斷點要麼是啟用的,要麼是未啟用的。即便斷點是激活的,但如果其被禁用了,那我們也無法暫停下來。可以通過禁用斷點在未來需要的地方放置好斷點,從而無須每次需要時再來暫停。

要創建斷點(如圖9-6所示),請在編輯器中選擇你想要暫停的行,然後選擇Debug→Breakpoints→Add Breakpoint at Current Line(Command-組合鍵)。該鍵盤快捷鍵會在為當前行添加斷點與刪除斷點間切換。斷點通過邊列上的箭頭表示。此外,單擊邊列也會添加斷點;要想除斷點,請將其拖曳出邊列即可。

圖9-6:斷點

要禁用當前行的斷點,請單擊邊列上的斷點以修改其啟用狀態。此外,還可以按住Control鍵並單擊斷點,然後從上下文菜單中選擇Disable Breakpoint。深色斷點處於啟用狀態,而淺色斷點則處於禁用狀態(如圖9-7所示)。

圖9-7:禁用的斷點

要整體性地切換斷點的激活狀態,請單擊調試窗格頂部的斷點按鈕,或選擇Debug→Activate/Deactivate Breakpoints(Command-Y組合鍵)。整體的斷點激活狀態並不會影響每個斷點的啟用或禁用狀態;如果斷點是未激活的,那麼它們會被忽略,這樣斷點處就不會暫停了。如果斷點處於激活狀態,那麼斷點箭頭就是藍色的;如果處於未激活狀態,那麼箭頭就是灰色的。

一旦在代碼中設定了斷點,就可以管理這些斷點了。這正是斷點導航器的目的所在。可以導航到斷點處,通過單擊導航器中的箭頭來啟用或禁用斷點,或刪除斷點。

還可以編輯斷點的行為。在邊列或斷點導航器的斷點上按住Control鍵並單擊,然後選擇Edit Breakpoint,或按住Command與Option鍵並單擊斷點。這是個非常強大的功能:可以在某種情況下或執行了某些次數後才在斷點處暫停;可以在遇到斷點時執行一個或多個動作,比如,發出調試命令、記錄日誌、播放聲音、朗讀文本,或運行一段腳本。

可以配置斷點,在遇到斷點並執行完其動作後再自動繼續執行。這是比原始調試更為強大的功能:相比於插入print或NSLog調用(需要插入到代碼中,並在發佈應用時再將其刪除),可以設定用於記錄日誌和繼續執行的斷點。根據定義,這種斷點只在調試項目時才會起作用;當應用運行在用戶設備上時,它不會向控制台輸出任何信息,因為用戶設備上是沒有斷點的。

可以在斷點導航器中創建某些特殊類型的斷點(單擊導航器底部的「+」按鈕,然後從彈出菜單中選擇)或從Debug→Breakpoints層次菜單中選擇:

異常斷點

異常斷點會讓應用在異常拋出或捕獲時暫停,而不考慮該異常是否會在後面導致應用崩潰。建議你創建異常斷點以便在異常拋出時能夠暫停,因為這樣就可以在異常發生的時刻查看到調用堆棧和變量值了(而不必等到後面出現崩潰時再查看);可以查看到在代碼中的位置,並且可以檢查變量值,這有助於你理解問題的原因所在。如果創建了這種異常斷點,那麼還建議你使用上下文菜單Move Breakpoint To→User,這會持久化該斷點並且讓所有項目都可以使用它。

有時,Apple的代碼會有意拋出異常並將其捕獲。這並不會導致應用崩潰,也不會出現什麼問題;不過,如果創建了異常斷點,那麼應用就會暫停,這可能會對你造成困擾。

符號斷點

符號斷點會在調用某個方法或函數時讓應用暫停,不管是什麼對像調用的方法或消息發給哪個對象都是如此。方法可以通過兩種方式來指定:

使用Objective-C符號

實例方法或類方法符號(-或+),後跟方括號,裡面是類名與方法名。比如:


-[UIApplication beginReceivingRemoteControlEvents]
  

根據方法名

只有方法名。調試器會針對所有可能的類-方法對進行解析,就好像使用上面提到的Objective-C符號輸入的一樣。比如:


beginReceivingRemoteControlEvents
  

如果進入了不正確的方法名或類名,那麼符號斷點就不會做任何事情。一般來說,如果對了,自己應該是知道的,因為你會看到解析後的斷點以層次化的結構列在了你的斷點的下面。

2.在斷點處暫停

激活斷點並運行應用,如果應用遇到了啟用的斷點(假設滿足了斷點的條件),那麼應用就會暫停。在活動項目窗口中,編輯器會顯示出包含了執行點的文件,這通常就是包含了斷點的文件。執行點會顯示為綠色的箭頭;這是將要執行的代碼行(如圖9-8所示)。根據Behaviors首選項窗格中對Running→Pauses的設置,調試導航器與調試窗格會出現。

圖9-8:在斷點處暫停

下面是應用在斷點處暫停下來後你可能想要執行的動作:

查看所在何處

設置斷點的一個常見原因就是確保執行路徑通過了某一行。調試導航器的調用堆棧中所列出的函數如果帶有User圖標,其文本又是空的,那就表明這是自定義的方法;可以單擊函數查看在方法中的哪一行暫停了(灰色文本的函數與方法是沒有源代碼的,因此單擊這些方法是沒什麼意義的,除非你瞭解彙編語言)。還可以通過調試窗格頂部的跳轉欄查看並導航調用堆棧。

查看變量值

在調試窗格中,當前作用域中的變量值(對應於調用堆棧中所選的變量)會顯示在變量列表中。可以通過展開三角箭頭查看到額外的對象特性,比如,集合元素、屬性,甚至是某些私有信息。(局部變量值甚至會在暫停處顯示出來,這些變量尚未初始化;這種值是沒有意義的,請忽略。)

可以通過搜索框根據名字或值來過濾變量。如果格式化的摘要信息還不夠,那麼可以向對像變量發送description(如果對像使用了CustomDebugStringConvertible,那就發送debugDescription),並在控制台查看輸出:從上下文菜單選擇Print Description of[Variable],或選中變量並單擊變量列表下方的Info按鈕。

還可以以圖形化方式查看變量值:選中某個變量,單擊變量列表下的Quick Look按鈕(一隻眼睛的圖標),或按下空格鍵。比如,對於CGRect來說,其圖形化表示是個成比例的矩形。可以按照相同方式創建自定義類的實例;聲明如下方法,並返回所允許的一個類型的實例(參見Apple的Quick Look for Custom Types in the Xcode Debugger):


@objc func debugQuickLookObject -> AnyObject {
    // ... create and return your graphical object here ...
}
  

還可以直接在代碼中查看變量值,只需查看其數據提示即可。要想查看數據提示,請將鼠標指針懸浮在代碼中的變量名之上。數據提示非常類似於變量列表中所顯示的值:有一個小三角,可以打開它查看更多信息,此外還有一個Info按鈕,顯示了這裡與控制台上的值的描述,Quick Look按鈕則以圖形化形式顯示了一個值(如圖9-9所示)。

圖9-9:數據提示

查看視圖層次

可以在調試器暫停時查看視圖層次。單擊調試窗格頂部欄中的Debug View Hierarchy按鈕,或選擇Debug→View Debugging→Capture View Hierarchy。視圖會以大綱形式列在調試導航器中。編輯器會顯示出你的視圖;這是個可旋轉的三維投影。對像查看器與尺寸查看器會顯示出關於當前所選視圖的信息。

管理表達式

表達式是添加到變量列表中的代碼,每次暫停時都會進行計算求值。可以從變量列表上下文菜單中選擇Add Expression來添加表達式。表達式會在代碼的當前上下文中進行求值,因此請小心其副作用。

與調試器通信

可以通過控制台直接與調試器通信。Xcode的調試器界面是真正的調試器LLDB(http://lldb.llvm.org)的一個前端;通過直接與LLDB通信,可以完成Xcode調試器界面所能做的任何事情,甚至還可以做到更多。常見的命令有:

fr v(frame variable的簡稱)

輸出作用域中的所有局部變量,類似於在變量列表中顯示。此外,其後還可以跟著你想要查看的變量名。

po(表示「print object」)

後跟作用域中對像變量的名字,類似於Print Description:根據description或debugDescription顯示對像變量值一樣。

p(或expression、expr、e)

計算當前上下文中當前語言的任何表達式。

操控斷點

可以在應用運行時自由創建、刪除、編輯、啟用、禁用和管理斷點,這是非常有用的,因為下一次暫停的位置可能取決於現在暫停的位置。事實上,這是斷點相比於原始調試的一個主要優勢。要修改原始調試,需要停止應用、編輯、重新構建,然後再次運行應用。不過,通過操縱斷點,無須停止應用;甚至都不需要暫停!如果某個操作出錯了(但不會導致應用崩潰),那麼它可以實時地重複多次;這樣,只須添加一個斷點並重試即可。比如,如果輕拍按鈕會生成錯誤的結果,那麼你就可以向動作處理器添加一個斷點,並再次輕拍按鈕;執行的代碼都是一樣的,但這次可以看到問題出在了哪裡。

步進或繼續

要讓暫停的應用繼續執行,可以繼續運行,直到遇到了下一個斷點(Debug→Continue),或步進並再次暫停。此外,可以選中一行,然後選擇Debug→Continue to Current Line(或從上下文菜單中選擇Continue to Here),這會在所選行上設置一個斷點,繼續執行並刪除該斷點。步進命令如下所示(位於Debug菜單中):

Step Over

在下一行暫停。

Step Into

如果當前行調用了函數,那麼就會在該函數中暫停;否則,在下一行暫停。

Step Out

從當前函數返回處暫停。

可以通過調試窗格頂部的快捷按鈕使用這些命令。即便調試窗格被收起,在應用運行時,包含按鈕的工具欄也會呈現出來。

重新開始,或終止

要終止運行著的應用,請單擊工具欄中的Stop(Product→Stop,Command-Period組合鍵)。單擊模擬器或設備中的Home按鈕(Hardware→Home)並不會停止運行著的應用,這是因為在iOS 4及之後的系統中都是多任務運行了。要終止運行著的應用,但在不重新構建的情況下還要重新啟動它,請按住Control鍵並單擊工具欄中的Run(Product→Perform Action→Run Without Building,Command-Control-R組合鍵)。

可以在應用運行或暫停時修改代碼,不過這些修改並不會對運行著的應用起作用;有一些編程環境會讓這一美夢成真,但Xcode不行。你需要終止應用,並按照正常方式運行它(包括構建)才能看到修改效果。