首页 » iOS编程基础:Swift、Xcode和Cocoa入门指南 » iOS编程基础:Swift、Xcode和Cocoa入门指南全文在线阅读

《iOS编程基础:Swift、Xcode和Cocoa入门指南》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不行。你需要终止应用,并按照正常方式运行它(包括构建)才能看到修改效果。