Android NDK开发详解之调试和性能分析的通过Android Studio调试
- 启用调试功能
- 在设备上启用调试功能。
- 运行可调试的 build 变体。
- 更改调试程序类型
- Auto
- Java
- Native(仅适用于 C/C++ 代码)
- 设备支持 run-as。
- 设备启用了 ptrace。
- Dual(仅适用于 C/C++ 代码)
- 使用系统日志
- 在代码中写入日志消息
- 查看系统日志
- 使用断点
- 行断点
- 方法断点
- 字段断点
- 异常断点
- 查看和配置断点
- 调试窗口帧
- 检查变量
- 添加观察点
- 查看和更改资源值显示格式
Android Studio 提供了一个调试程序,用来执行以下操作及其他任务:
选择要在哪个设备上调试应用。
在 Java、Kotlin 和 C/C++ 代码中设置断点。
在运行时检查变量和对表达式求值。
本页包含基本调试程序操作的说明。如需查看更多文档,另请参阅 IntelliJ IDEA 调试文档。
启用调试功能
您需要先执行以下操作,然后才能开始调试:
在设备上启用调试功能。
如果您使用的是模拟器,则默认情况下会启用调试功能。但是,对于已连接的设备,您需要在设备开发者选项中启用调试功能。
运行可调试的 build 变体。
使用 build 配置中包含 debuggable true(Kotlin 脚本中 isDebuggable = true)的build 变体。
通常,您可以选择每个 Android Studio 项目中都包含的默认“debug”变体(即使它在 build.gradle 文件中不可见)。不过,如果您想将新 build 类型定义为可调试,则必须将 debuggable true 添加到该 build 类型中:
Groovy
android {
buildTypes {
customDebugType {
debuggable true
...
}
}
}
Kotlin
android {
buildTypes {
create("customDebugType") {
isDebuggable = true
...
}
}
}
此属性也适用于包含 C/C++ 代码的模块。
注意:jniDebuggable 属性已废弃。
如果您的应用依赖于您也想调试的某个库模块,则该库也必须使用 debuggable true 进行打包,以便保留其调试符号。为了确保应用项目的可调试变体接收库模块的可调试变体,请发布库的非默认版本。
开始调试
您可以按如下方式启动调试会话:
在应用代码中设置一些断点。
在工具栏中,从目标设备菜单中选择用于调试应用的设备。
目标设备菜单。
图 1. 目标设备菜单。
如果您未配置任何设备,则需要通过 USB 连接设备、通过 Wi-Fi 连接设备或创建 AVD 以使用 Android 模拟器。
在工具栏中,点击 Debug 图标 。
如果您的应用已在设备上运行,系统会显示一个对话框,询问您是否要从“Run”切换为“Debug”。您需要重启设备才能开始调试。如需让同一应用实例保持运行状态,请点击 Cancel Debug,改为将调试程序连接到正在运行的应用。否则,Android Studio 会构建一个 APK,使用调试密钥为其签名,将其安装在您选择的设备上,然后运行它。
如果您向项目添加 C 和 C++ 代码,Android Studio 还会在“Debug”窗口中运行 LLDB 调试程序来调试您的原生代码。
如果“Debug”窗口未打开,请依次选择 View > Tool Windows > Debug,或点击工具窗口栏中的 Debug 图标 。
点击“Debugger”标签页,如图 1 所示。
图 2. “Debug”窗口,其中显示的是某变量的当前线程和对象树
将调试程序连接到正在运行的应用
如果您的应用已在设备上运行,则无需重启应用即可开始调试,具体操作步骤如下:
1、点击 Attach debugger to Android process 图标 。
2、在 Choose Process 对话框中,选择您想将调试程序连接至其上的进程。
a、如果您使用的是模拟器或已取得 root 权限的设备,则您可以勾选 Show all processes 以查看所有进程。在已取得 root 权限的设备上,执行此操作会显示设备上在运行的所有进程。不过,在未取得 root 权限的设备上,执行此操作只会显示可调试的进程。
b、从 Use Android Debugger Settings from 菜单中,您可以选择现有的运行/调试配置。对于 C 和 C++ 代码,您可以重复使用现有配置中的 LLDB 启动命令、LLDB 连接后命令和符号目录。
c、如果您没有现有的运行/调试配置,请选择 Create New。此选项会启用 Debug Type 菜单,您可以在其中选择其他调试类型。默认情况下,Android Studio 会使用“Auto”调试类型来选择最适合您的调试程序选项,具体取决于您的项目是包含 Java 代码还是 C/C++ 代码。
3、点击 OK。
随后将显示“Debug”窗口。
设备浏览器中的 Processes 标签页 (View > Tool Windows > Device Explorer) 内还包含可调试进程的列表。您可以从中选择一个进程并执行终止 或强行停止 操作,或将调试程序附加至给定进程 。
注意:Android Studio 调试程序与垃圾回收器采用松散集成。Android 虚拟机可保证在调试程序断开连接之前不会对调试程序发现的任何对象进行垃圾回收。这可能会导致在调试程序处于连接状态时,出现对象堆积情况。例如,如果调试程序发现了某个正在运行的线程,那么在调试程序断开连接之前,不会对关联的 Thread 对象进行垃圾回收,即使该线程已终止。
更改调试程序类型
由于调试 Java/Kotlin 代码和 C/C++ 代码需要不同的调试程序工具,因此 Android Studio 调试程序允许您选择要使用的调试程序类型。默认情况下,Android Studio 会根据在项目中检测到的语言来决定要使用哪种调试程序(使用 Auto 调试程序类型)。
如需在调试配置中手动选择调试程序,请依次点击 Run > Edit Configurations。您还可以在依次点击 Run > Attach debugger to Android process 后显示的对话框中选择调试程序。
可用的调试类型包括:
Auto
如果您希望 Android Studio 自动为您要调试的代码选择最合适的选项,请选择此调试类型。例如,如果您的项目包含任何 C 或 C++ 代码,Android Studio 会自动使用“Dual”调试类型。否则,Android Studio 会使用“Java”调试类型。
Java
如果您只想调试使用 Java 或 Kotlin 编写的代码,请选择此调试类型。Java 调试程序会忽略您在原生代码中设置的任何断点或监视点。
Native(仅适用于 C/C++ 代码)
如果您只想使用 LLDB 来调试代码,请选择此调试类型。使用此调试类型时,Java 调试程序会话视图不可用。默认情况下,LLDB 只检查原生代码,而会忽略 Java 代码中的断点。如果您还想调试 Java 代码,则应切换到“Auto”或“Dual”调试类型。
原生调试仅适用于满足以下要求的设备:
设备支持 run-as。
如需检查设备是否支持 run-as,请在连接到设备的 ADB shell 上运行以下命令:
run-as your-package-name pwd
将 your-package-name 替换为您应用的软件包名称。如果设备支持 run-as,该命令应返回,不会出现任何错误。
设备启用了 ptrace。
如需检查是否启用了 ptrace,请在连接到设备的 ADB shell 上运行以下命令:
sysctl kernel.yama.ptrace_scope
如果启用了 ptrace,则该命令会输出值 0 或出现 unknown key 错误。如果未启用 ptrace,则该命令会输出 0 以外的值。
Dual(仅适用于 C/C++ 代码)
如果您想在同时调试 Java 代码和原生代码与不同时调试这两种代码之间切换,请选择此调试类型。Android Studio 会将 Java 调试程序和 LLDB 都连接到您的应用进程,这样一来,您不必重启应用或更改调试配置,便可同时对 Java 代码和原生代码中的断点进行检查。
在图 2 中,请注意“Debug”窗口标题右侧的两个标签页。由于应用同时包含 Java 代码和 C++ 代码,因此一个标签页用于调试原生代码,另一个用于调试 Java 代码(如 -java 所示)。
图 3. 用于调试原生代码的标签页和用于调试 Java 代码的标签页。
注意:在调试由编译器优化的原生代码时,您可能会收到以下警告消息:
This function was compiled with optimizations enabled. Some debugger features may not be available。使用优化标记时,编译器会对编译的代码进行更改,以使其更加高效地运行。这可能会导致调试程序报告意外或不正确的信息,因为调试程序很难将优化的编译代码映射回原始源代码。因此,您应该在调试原生代码时停用编译器优化。
使用系统日志
系统日志会在您调试应用时显示系统消息。这些消息包括运行在设备上的应用产生的信息。如果您要使用系统日志调试应用,请确保您的代码在应用处于开发阶段时写入日志消息并输出有关异常的堆栈轨迹。
在代码中写入日志消息
如需在代码中写入日志消息,请使用 Log 类。日志消息会在您与应用交互时收集系统调试输出,从而帮助您了解执行流程。日志消息还可以告诉您应用的哪个部分出现了故障。如需详细了解日志记录,请参阅使用 Logcat 写入和查看日志。
以下示例展示了如何添加日志消息,以确定您的 activity 启动时先前的状态信息是否可用:
Kotlin
import android.util.Log
...
private val TAG: String = MyActivity::class.java.simpleName
...
class MyActivity : Activity() {
...
override fun onCreate(savedInstanceState: Bundle?) {
...
if (savedInstanceState != null) {
Log.d(TAG, "onCreate() Restoring previous state")
/* restore state */
} else {
Log.d(TAG, "onCreate() No saved state available")
/* initialize app */
}
}
}
Java
import android.util.Log;
...
public class MyActivity extends Activity {
private static final String TAG = MyActivity.class.getSimpleName();
...
@Override
public void onCreate(Bundle savedInstanceState) {
...
if (savedInstanceState != null) {
Log.d(TAG, "onCreate() Restoring previous state");
/* restore state */
} else {
Log.d(TAG, "onCreate() No saved state available");
/* initialize app */
}
}
}
在开发期间,您的代码还可以捕获异常并将堆栈轨迹写入系统日志:
Kotlin
fun someOtherMethod() {
try {
...
} catch (e : SomeException) {
Log.d(TAG, "someOtherMethod()", e)
}
}
Java
void someOtherMethod() {
try {
...
} catch (SomeException e) {
Log.d(TAG, "someOtherMethod()", e);
}
}
注意:当您准备好发布应用时,应从代码中移除调试日志消息和堆栈轨迹输出调用。为此,请设置一个 DEBUG 标记,并将调试日志消息放入条件语句。
查看系统日志
您可以在“Logcat”窗口中查看和过滤调试消息及其他系统消息,如图 4 所示。例如,您可以看到发生垃圾回收时显示的消息,或您使用 Log 类添加到应用的消息。
如需使用 Logcat,请开始调试并选择“Logcat”标签页。
图 4. 包含过滤器设置的“Logcat”窗口。
有关 Logcat 及其过滤选项的说明,请参阅使用 Logcat 写入和查看日志。
使用断点
Android Studio 支持一些断点,这些断点可触发不同的调试操作。断点有以下几种类型:
行断点
最常见的类型是行断点,用于在指定的代码行暂停应用的执行。暂停时,您可以检查变量,对表达式求值,然后继续逐行执行,以确定运行时错误的原因。
方法断点
方法断点会在进入或退出特定方法时暂停应用的执行。暂停时,您可以检查变量,对表达式求值,然后继续逐行执行,以确定运行时错误的原因。
字段断点
字段断点会在对特定字段执行读取或写入操作时暂停应用的执行。
异常断点
异常断点会在对特定字段执行读取或写入操作时暂停应用的执行。
您可以设置条件断点,此类断点仅会在满足特定条件时才暂停应用的执行。您还可以设置日志记录断点,此类断点会向 Logcat 执行写入操作而不暂停执行。这有助于避免在日志语句中散放代码。
如需添加行断点,请按以下步骤操作:
1、找到您要暂停执行的代码行。
2、点击该代码行的左侧边线,或将光标置于该行上并按 Ctrl+F8(在 macOS 上,按 Command+F8)。
3、如果您的应用已在运行,请点击 Attach debugger to Android process 图标 。否则,如需开始调试,请点击 Debug 图标 。
当您设置断点时,相应的代码行旁边会出现一个红点,如图 5 所示。
图 5. 当您设置断点时,相应的代码行旁边会出现一个红点。
当您的代码执行到达该断点时,Android Studio 会暂停应用的执行。
如需确定应用的状态,请使用“Debugger”标签页中的工具:
如需检查变量的对象树,请在“Variables”视图中将其展开。如果“Variables”视图不可见,请点击 Layout Settings 图标 ,并确保 variables 已选中。
如需在当前执行点对表达式求值,请点击 Evaluate Expression 图标 。
如需前进到代码中的下一行(而不进入方法),请点击 Step Over 图标 。
如需前进到方法调用内的第一行,请点击 Step Into 图标 。
如需前进到当前方法之外的下一行,请点击 Step Out 图标 。
如需继续正常运行应用,请点击 Resume Program 图标 。
如果您的项目使用了任何原生代码,默认情况下,“Auto”调试类型会将 Java 调试程序和 LLDB 作为两个单独的进程都连接到您的应用。这样一来,您无需重启应用或更改设置,便可在检查 Java 断点与 C/C++ 断点之间进行切换。
注意:要使 Android Studio 检测到 C 或 C++ 代码中的断点,您需要使用支持 LLDB 的调试类型,如 Auto、Native 或 Dual。您可以通过修改调试配置,更改 Android Studio 使用的调试类型。如需详细了解不同的调试类型,请阅读有关使用其他调试类型的部分。
当 Android Studio 将应用部署到目标设备时,系统会打开“Debug”窗口,在该窗口中为每个调试程序进程显示一个标签页或调试会话视图,如图 6 所示。
图 6. 使用 LLDB 调试原生代码。
当 LLDB 调试程序遇到 C/C++ 代码中的断点时,Android Studio 会切换到 标签页。还会显示“Frames”“Variables”和“Watches”窗格,其作用与您调试 Java 代码时完全相同。
虽然 LLDB 会话视图中未显示“Threads”窗格,但您可以使用“Frames”窗格中的列表访问您的应用进程。您可以在有关如何调试窗口帧和检查变量的部分中详细了解这些窗格。
注意:检查原生代码中的断点时,Android 系统会挂起运行应用的 Java 字节码的虚拟机。这意味着,检查原生代码中的断点时,您无法与 Java 调试程序进行交互,或从 Java 调试程序会话检索任何状态信息。
当 Java 调试程序遇到 Java 或 Kotlin 代码中的断点时,Android Studio 会切换到 -java 标签页。
使用 LLDB 进行调试时,您可以利用 LLDB 会话视图中的 LLDB 终端向 LLDB 传递命令行选项。如果您想让 LLDB 在您每次开始调试应用时都执行某些命令(在调试程序连接到您的应用进程之前或之后执行),您可以将这些命令添加到您的调试配置中。
调试 C/C++ 代码时,您还可以设置特殊类型的断点,这类断点称为“监视点”,可以在您的应用与特定内存块进行交互时挂起您的应用进程。如需了解详情,请阅读有关如何添加监视点的部分。
查看和配置断点
如需查看所有断点并配置断点设置,请点击“Debug”窗口中的 View Breakpoints 图标 。随后将显示“Breakpoints”窗口,如图 7 所示。
图 7. “Breakpoints”窗口列出了当前的所有断点,并包括每个断点的行为设置。
您可以从“Breakpoints”窗口的列表中启用或停用每个断点。如果某个断点被停用,Android Studio 不会在应用遇到该断点时将其暂停。
从列表中选择某个断点即可配置其设置。您可以将某个断点配置为最初处于停用状态,让系统在遇到其他断点后将其启用。您还可以配置在遇到某个断点后是否应将其停用。如需为任何异常设置断点,请在断点列表中选择 Exception Breakpoints。
调试窗口帧
在“Debugger”窗口的“Frames”窗格中,您可以检查导致遇到当前断点的堆栈帧。这样,您就可以浏览和检查堆栈帧,同时还可以检查 Android 应用中的线程列表。
如需选择线程,请使用线程选择器菜单并查看其堆栈帧。点击帧中的元素会在编辑器中打开源代码。您还可以按检查帧指南中的说明自定义线程呈现和导出堆栈帧。
检查变量
系统将您的应用停止在某个断点处并且您从“Frames”窗格中选择某个帧后,您可以在“Debugger”窗口的“Variables”窗格中检查变量。此外,您还可以在“Variables”窗格中使用选定帧内提供的静态方法和/或变量对临时表达式求值。
“Watches”窗格提供的功能类似,不同的是添加到“Watches”窗格的表达式可跨调试会话存留。为自己经常访问或者提供的状态对当前调试会话有帮助的变量和字段添加监视点。“Variables”和“Watches”窗格如图 8 所示。
如需向“Watches”列表中添加变量或表达式,请按以下步骤操作:
1、开始调试。
2、在“Watches”窗格中,点击 Add 图标 。
3、在随后显示的文本框中,输入您要监视的变量或表达式的名称,然后按 Enter 键。
如需从“Watches”列表中移除某一项,请选择该项,然后点击 Remove 图标 。
如需对“Watches”列表中的元素重新排序,请选择某一项,然后点击 Up 图标 或 Down 图标 。
图 8. “Debug”窗口中的“Variables”和“Watches”窗格。
添加观察点
调试 C/C++ 代码时,您可以设置特殊类型的断点,这类断点称为“监视点”,可以在您的应用与特定内存块进行交互时挂起应用进程。例如,如果您为某个内存块设置了两个指针并为其分配了一个监视点,则使用任一指针访问该内存块都会触发该监视点。
在 Android Studio 中,您可以通过选择特定变量在运行时创建监视点,但 LLDB 只会将该监视点分配给系统分配给该变量的内存块,而不会分配给变量本身。这与将变量添加到“Watches”窗格不同,将变量添加到该窗格后,您可以监视变量的值,但当系统读取或更改内存中的变量值时,不允许您挂起应用进程。
注意:当应用进程退出某个函数,并且系统从内存中解除分配其局部变量时,您需要重新分配为这些变量创建的所有观察点。
如需设置监视点,您必须满足以下要求:
1、您的目标物理设备或模拟器使用 x86 或 x86_64 CPU。如果设备使用 ARM CPU,则您必须将变量的内存地址边界分别按照 4 字节(对于 32 位处理器)或 8 字节(对于 64 位处理器)对齐。如需在原生代码中对齐变量,请在变量声明中指定 attribute((aligned(num_bytes))),如下所示:
// For a 64-bit ARM processor
int my_counter __attribute__((aligned(8)));
2、您已经分配了不超过三个监视点。Android Studio 在 x86 或 x86_64 目标设备上最多只支持四个监视点。其他设备支持的监视点数量可能更少。
注意:使用 32 位 ARM ABI 调试应用时,添加监视点或将鼠标悬停在代码内的变量上以检查其值可能会导致崩溃。为了解决此问题,请使用 64 位 ARM、x86 或 x86_64 二进制文件进行调试。此问题将在即将推出的 Android Studio 版本中得到解决。
如果您满足上述要求,便可以添加监视点,具体操作步骤如下:
1、在应用在某个断点处挂起的情况下,转到 LLDB 会话视图中的“Variables”窗格。
2、右键点击占用您要跟踪的内存块的变量,然后选择 Add Watchpoint。
图 9. 向内存中的变量添加监视点。
3、随后将显示一个用来配置监视点的对话框,如图 9 所示。
使用下列选项配置您的监视点:
Enabled:如果您要告知 Android Studio 在您更改此设置之前暂时忽略相应的监视点,可以取消选择此选项。Android Studio 会保存您的监视点,以便您稍后访问。
Suspend:默认情况下,Android 系统会在其访问您分配给监视点的内存块时挂起应用进程。如果您不希望出现此行为,可以取消选择此选项。这样会显示一些附加选项,即 Log message to console 和 Remove when hit,您可以使用这些选项自定义系统与监视点进行交互时的行为。
Access Type:选择当应用尝试对系统分配给变量的内存块执行 Read 或 Write 操作时是否应触发您的监视点。如需在执行读取或写入操作时触发您的监视点,请选择 Any。
4、点击 Done。
如需查看所有监视点并配置监视点设置,请点击“Debug”窗口中的 View Breakpoints 图标 。随后将显示“Breakpoints”对话框,如图 10 所示。
图 10. “Breakpoints”对话框列出了当前的监视点,并包括每个监视点的行为设置。
添加监视点后,点击“Debug”窗口中的 Resume Program 图标 可继续执行应用进程。默认情况下,如果应用尝试访问您设置了监视点的内存块,Android 系统会挂起应用进程,应用最后执行的那行代码旁边会出现一个监视点图标 ,如图 11 所示。
图 11. Android Studio 会指示应用在即将触发监视点之前执行的那行代码。
查看和更改资源值显示格式
在调试模式下,您可以查看 Java 或 Kotlin 代码中的变量的资源值并为其选择其他显示格式。在显示了“Variables”标签页并选择了帧的情况下,执行以下操作:
1、在“Variables”列表中,右键点击资源行的任意位置以显示列表。
2、在列表中选择 View as,然后选择您要使用的格式。
可供选择的格式取决于选定资源的数据类型。您可能会看到以下一个或多个选项:
Class:显示类定义。
toString:显示字符串格式。
Object:显示对象(类实例)定义。
Array:以数组格式显示。
Timestamp:按 yyyy-mm-dd hh:mm:ss 格式显示日期和时间。
Auto:Android Studio 根据数据类型选择最合适的格式。
Binary:显示使用 0 和 1 表示的二进制值。
MeasureSpec:从父级传递给选定子级的值。请参阅 MeasureSpec。
Hex:显示为十六进制值。
Primitive:显示为使用原始数据类型表示的数值。
Integer:显示 Integer 类型的数值。
如需创建自定义格式,请执行以下操作:
1、右键点击资源值。
2、选择 View as。
3、选择 Create。
4、随后将显示 Java Data Type Renderers 对话框。按照 Java 数据类型渲染程序中的说明进行操作。