使用 Xcode 8 进行调试
高级调试功能,以提高开发人员的生产力
引言
Apple 在 Xcode 8 中为开发者添加了新的调试技巧和命令,以提高工作效率。我们都知道 LLDB(Low-Level Debugger)是 Xcode 后端用于 iOS 和 Mac OS 的调试器。它与 LLVM 编译器紧密协作,提供了更多功能。我们通过 Xcode 调试器控制台与其进行交互。
本文介绍了用于提高开发者生产力的先进调试功能。以下是更好进行调试的技巧。
- 在调试控制台中声明新变量、函数或闭包
- Xcode 8 中的“使用终端”功能
- 设置别名命令
- 排查 App 崩溃
- parray 和 poarray 命令
表达式解析器
让我们考虑一个场景:一个应用程序下载了一个 JSON 文件或一些 Web 内容。需要检查大型 JSON 文件中的某些特定数据,或者从 Web 内容中过滤一些数据。例如,应用程序下载了某个产品的维护历史记录,你想知道本月是否发生过维护。你可能会停止运行应用程序,编写一个可以过滤内容并显示它的函数,然后重新开始调试应用。这不仅耗时,而且效率低下。仅仅是为了更好地理解和调试,我们就停止执行并编写方法。如果我们可以在调试时添加自定义谓词并使用程序数据执行它呢?
Xcode 提供了在调试控制台中添加新变量、方法或闭包的功能。我们可以通过表达式解析器来实现这一点。
例如,在调试控制台中声明一个新变量,并在调试时与程序对象一起使用。这些变量、函数或闭包将一直保留,直到调试会话结束。
让我们看一些例子。
在 Swift 中
在调试控制台中声明变量
(lldb) (lldb) expr let $USDollorValue=64;
(lldb) expr $USDollorValue
(Int) $R1 = 64
变量 USDollorValue
表示数据 64
,可用于在调试控制台中与程序值进行任何计算。变量必须以“$
”符号声明才能重复使用。
将变量与程序值一起使用
在下面的示例中,USDollorValue
将乘以名为 totalAmt
的程序变量。
1472
在调试控制台中声明函数
在 lldb 提示符下,键入 expr,然后命令将进入多行表达式模式以声明用户定义的函数。在函数前添加“$
”符号,然后我们可以通过传递参数来重用它。下面是一个示例,展示了格式化的学生姓名和 totalMark
。其中 totalMark
是程序变量。
(lldb) expr
Enter expressions, then terminate with an empty line to evaluate:
1 func $studentReport(sname:String) -> Void {
2 let tlAmt = String(totalMark)
3 let StudentReport:String = "Reported Student Name is"+sname+" with total mark "+tlAmt;
4 print(StudentReport);
5 }
在调试控制台中调用函数。
(lldb) expr $studentReport(sname:"John")
Reported Student Name isJohn with total mark 23
在 C、C++ 和 Objective C 中
在这种情况下,expr
命令无法直接用于声明变量、函数或闭包。在这里,我们必须使用顶层表达式模式。
顶层表达式模式允许控制器退出当前函数以全局声明变量或函数。
声明名为 USDollorValue 的变量
(lldb) expr --top-level --
Enter expressions, and then terminate with an empty line to evaluate:
1 $USDollorValue=64;
2
将变量 USDollorValue 与名为 totalAmt 的对象变量一起使用
(lldb) expr $USDollorValue *self.totalAmt;
3328
在调试控制台中声明一个用于排序的 C 函数
(lldb) expr --top-level --
Enter expressions, then terminate with an empty line to evaluate:
1 void $printInSortedOrder(int *number,int n){
2 for (int i = 0; i < n; ++i){
3 for (int j = i + 1; j < n; ++j){
4 if (number[i] > number[j]){
5 int a = number[i];
6 number[i] = number[j];
7 number[j] = a;
8 }}}
9 for (int i=0;i<n; i++) {
10 printf("\n%i",number[i]);}
11 }
12
在调试控制台中调用函数,传递名为 rateValue 的数组,数字位数为 4
(lldb) expr $printInSortedOrder(rateValue, 4)
2
7
9
10
声明一个 C 函数,该函数使用 Math 库函数计算给定数字的平方根。
(lldb) expr --top-level --
Enter expressions, then terminate with an empty line to evaluate:
1 void $squareRoot(int num)
2 {
3 printf("Using Math function in C to calculate square root \n");
4 float answer=sqrt(num);
5 printf("square root = %f",answer);
6 }
7
函数的输出将是
(lldb) expr $squareRoot(16)
Using Math function in C to calculate square root
square root = 4.000000
在运行时声明自定义谓词
考虑一个如下的 JSON 文件
{
"modelno": "MD4563",
"maintenance":
[
{
"month":"january",
"values":["lcddisplay","filament"]
},
{
"month":"february",
"values":["Nozzle diameter","Automatic grade"]
}
]
}
让我们创建一个新的谓词,它可以过滤出一月份的维护详情。
让我们在 lldb 控制台中声明自定义谓词。
(lldb) expression
Enter expressions, then terminate with an empty line to evaluate:
1 NSPredicate *$predicateM =
[NSPredicate predicateWithFormat:@"month == %@",@"january"];
2
调用谓词
(lldb) po [[modelNo valueForKey:@"maintenance"]
filteredArrayUsingPredicate:$predicateM];
Output will be
<__NSSingleObjectArrayI 0x17000ecf0>(
{
month = january;
values = (
lcddisplay,
filament
);
}
)
Xcode 8 中的“使用终端”功能
Xcode 8 的一项新功能是启动一个新的独立控制台,用于应用程序的输入和输出,同时使用 Xcode 的默认控制台仅用于调试。从模式选项中启用“使用终端”。
因此,我们现在可以在独立的终端中处理应用程序的输入和输出,同时可以在 Xcode 的默认控制台中调试应用程序。此功能的好处在于,开发人员可以在单独的控制台中管理应用程序的 I/O。如果我们不想在同一个 Xcode 默认控制台中混淆应用程序的输入、输出和调试器输出,可以使用此选项。
命令别名
如果调试器命令可自定义,开发人员的效率会更高。如果我们能为经常使用的命令分配一个别名,那会怎么样?
此命令帮助开发人员为经常使用的命令设置别名,并以更高效的方式继续执行。
例如,我们使用命令“thread continue
”从断点恢复并继续执行应用程序。这里“thread continue
”被频繁使用。
此外,在 Xcode 8 中,可以为别名命令添加帮助文本。
让我们为命令“thread continue
”设置一个名为“tc
”的别名。
(lldb) command alias -h"continue the current thread" -- tc thread continue
这里“command alias
”是设置别名的 lldb
命令。跟在 –h
后面的文本是帮助文本,描述了新的别名命令实际的功能。后面跟“--
”的“tc
”是命令“thread continue
”的别名。此后,我们可以在 lldb
控制台中键入“tc
”而不是 thread continue
。
执行命令“tc
”
(lldb) tc
Resuming thread 0x6753 in process 1838
Process 1838 resuming
要获取有关命令“tc
”的所有详细信息,请键入
(lldb) help tc
continue the current thread
Syntax: tc <thread-index> [<thread-index> [...]]
'tc' is an abbreviation for 'thread continue
别名命令的有效性仅限于调试会话的持续时间。在开始调试之前反复设置命令的别名是一项繁琐的任务。LLDB 有一个解决方案:LLDB 有一个名为“.lldbinit ”的初始化文件。我们可以将所有必需的别名命令添加到 .lldbinit 文件中,以便所有命令在调试器启动时初始化,然后别名命令将跨项目可用。
排查 App 崩溃
当发生运行时崩溃时,大多数情况下 Xcode 可以显示其堆栈跟踪。但有些情况下 Xcode 无法显示有关崩溃的详细信息。有时,它会发生诸如“EXE_BAD_ACCESS
”之类的崩溃,或者是由第三方库引起的崩溃。
让我们看一些调试此类崩溃的场景。
让我们通过读取机器寄存器来研究如何排查应用程序崩溃。寄存器是处理器的一部分,用于保存少量数据。一旦 Xcode 命中崩溃点,我们就可以通过“Register read
”命令列出所有寄存器,输出如下所示
General Purpose Registers:
rax = 0x0000000000000000
rbx = 0x00006080000792c0
rcx = 0x0000000000000004
rdx = 0x0000000000000000
rdi = 0x000060000000d260
rsi = 0x0000000110e8aaea "initWithData:encoding:"
rbp = 0x00007fff55a603f0
rsp = 0x00007fff55a602d0
r8 = 0x0000000000000040
r9 = 0x00006000000b4c10
r10 = 0x000000000000000a
r11 = 0x000000010bbd4a98 Foundation`-[NSPlaceholderString initWithData:encoding:]
r12 = 0x000000010c110ac0 libobjc.A.dylib`objc_msgSend
r13 = 0x000000010c11497a "release"
r14 = 0x000060800027c800
r15 = 0x0000000000000000
rip = 0x000000010a1c2b78
MedicationAssistant`-[MedVPAViewController didReceiveVoiceResponse:] +
104 at MedVPAViewController.m:180
rflags = 0x0000000000000246
cs = 0x000000000000002b
fs = 0x0000000000000000
gs = 0x0000000000000000
在这里,我们需要找出是哪个寄存器中的哪个参数导致了崩溃。为此,我们可以探索堆栈帧的每一帧。让我们从第 0 帧读取到最顶部的帧,直到我们能指出导致错误的具体函数。
例如,我有一个堆栈调用如下
Eg:- Stack Frame
UIApplicationMain -Frame4
DidFinishLaunching -Frame3
Function1 -Frame2
Function2 -Frame1
Function3 -Frame0
我们可以通过在 lldb 控制台中键入“Frame Select 0
”来指向最年轻的帧。我们将收到帧详细信息,如下所示
(lldb) frame select 0
frame #0: 0x0000000109e35b4b MedicationAssistant`-
[MedVPAViewController didReceiveVoiceResponse:withValuePointer:]
(self=0x00007fb2b0e10b60, _cmd="didReceiveVoiceResponse:withValuePointer:",
data=0x0000000000000000, x=110) + 123 at MedVPAViewController.m:180
177
178 NSString *responseString =
[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
179 int *status = NULL;
-> 180 *status = x;
181 NSError *jsonError;
182 NSRange range ;
183 NSString *parsedData=nil;
上面的输出表示崩溃发生在 didReceiveVoiceResponse:withValuePointer
:的第 180 行,你正在将 x
的值赋给一个名为 status
的指针变量。现在让我们检查 x
的值是从哪里传递过来的。为此,让我们移到上一级帧。
我们可以使用以下命令之一
Lldb) frame select 1
Or
Lldb) up
使用“up
”和“down
”命令在堆栈帧中将指针移到更年轻或更老的帧。
上述命令帮助调试器指向上一级帧。
结果将如下所示
frame #1: 0x0000000109ecea1b MedicationAssistant`-
[SpeechToTextModule gotResponse:](self=0x00006080001dd3d0,
_cmd="gotResponse:", jsonData=0x0000000000000000) +
75 at SpeechToTextModule.m:280
277
278 - (void)gotResponse:(NSData *)jsonData {
279 [self cleanUpProcessingThread];
-> 280 [delegate didReceiveVoiceResponse:jsonData withValuePointer:framesize];
281 }
282
283 - (void)requestFailed:(NSError *)error
从上面的输出,我们可以得出结论,didReceiveVoiceResponse:withValuePointer
: 是由 SpeechToTextModule::GotResponse
方法调用的,并且“framesize
”中的数据有问题。这样,通过探索堆栈帧,我们可以几乎找出崩溃的原因。
parray 和 poarray
当我们处理 NSArray
或 NSDictionary
时,我们使用“p
”或“po
”命令来显示其值。“p
”和“po
”是最常用的强大命令。
但是,如果我们尝试显示一个 C 数组指针,它不会显示数组中的所有值。它只会打印对象指针值。如果您正在处理 C 代码,并在调试时想要打印 C 指针数组中的所有值,在 Xcode 8 之前,我们可能需要编写代码来遍历数组并打印每个值。但从 Xcode 8 开始,我们有了两个新命令来在调试控制台中打印 C 指针数组。这里调试器引入了 2 个新命令,它们可以与 C 指针数组一起使用。
parray <count> <arrayName>
poarray <count> <arrayName>
(lldb) parray 10 transArray
(lldb) poarray 3 transArray