65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 Xcode 8 进行调试

starIconstarIconstarIconstarIconstarIcon

5.00/5 (2投票s)

2017年4月12日

CPOL

7分钟阅读

viewsIcon

13458

高级调试功能,以提高开发人员的生产力

引言

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

当我们处理 NSArrayNSDictionary 时,我们使用“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
© . All rights reserved.