调试 Android 系统上的 Chromium

优质
小牛编辑
136浏览
2023-12-01
在Android系统上,开发者可以使用两种不同的语言来开发应用程序,一种是Java语言,开发者使用的是Android SDK来配置和编译这些代码,生成Java语言的class文件,也就是Java虚拟机运行的二进制代码。Android系统使用.dex文件将一系列的class文件压缩在一起。另外一种是C/C++语言,使用Android NDK来配置和编译这些代码。这些代码经过NDK编译后就是汇编码并合成动态链接库,也就是.so文件。调试Java代码和C/C++代码需要不同的技术和方法,下面分别来介绍它们。

1.1 前提条件

因为Android是基于Java语言的,所以任何应用程序都离开不开Java代码。而对于一个Android应用程序,开发者是否使用C++代码来编写部分逻辑则是可选的。所以,对于很多Android应用程序来说,仅仅Java语言就已经足够了。但是对于Chromium来说,调试C++代码是必需的。

想要调试Android的Java代码,首先需要的是Oracle JDK(现在也可以使用OpenJDK了)和Android SDK,它们分别是Java的运行和调试工具,Android开发和调试的各种工具。事实上,为了开发和调试上的方便,开发者通常也需要下载Eclipse。Android团队为了方便开发者,将这些工具都打包在一起,详情见这里:http://developer.android.com/tools/index.html。

因为Android设备是小型设备,不适合用来作为开发者的开发和调试机器,所以调试Android应用程序通常是使用PC来完成的,笔者偏向使用我的Linux开发机器来调试Android应用程序。所以,调试Android应用程序实际上需要调试机器和被调试的Android设备。上面说到的那些JDK和SDK工具都是安装在开发机器上的。开发者可以使用实际的设备来调试应用程序,同时,也可以通过创建一个虚拟设备来模拟Android设备运行应用程序。读者可以通过下面的链接来获取创建虚拟设备的相关方法:http://developer.android.com/tools/devices/index.html

从上面可以看出,调试Android应用程序实际上涉及两个硬件设备(或者虚拟)。不同于调试Linux上的应用程序,因为开发者通常就是使用本地调试器来调试当前机器上运行的程序。在调试Android环境中的应用,开发者通常需要使用到“远程调试”的技术。远程调试需要涉及到两个不同的设备或者机器,而调试工具和被调试程序两者可以通过其它辅助设施和套接字来通信,以完成调试功能。

1.2 开启/关闭调试功能

想要调试某个Android设备上的应用程序,必须首先要打开该设备上的“开发者选项”。在一些设备中,该选项是缺省不显示的,需要用户在“设置”中的“关于手机”中的Android信息上连续点击多次之后才会出现“开发者选项”。通常,“开发者选项”会出现“USB调试”选项,开发者需要打开该选项才能够通过开发机器调试该设备上的应用程序。 在“开发者选项”中还有很多对开发者非常有用的子选项,例如“不锁定屏幕”。笔者很喜欢这一功能,它有助于开发者调试程序而不会出现设备总是锁屏等烦人的情况。 enter image description here 当设置完Android设备之后,后面面临的问题就是被调试程序,这个对于调试Java代码来说也要细细的说一下。 首先来说一个没有被“root”的Android设备,也就是通常大家能够看到的设备。在这种情况下,在编写被调试程序的时候,需要在AndroidManifest.xml中作相应的设置,也就是"Application"元素的属性“debuggable”需要设为“true”,这样在上面说到的“开发者选项中”的子菜单“选择调试应用”就能够找到需要被调试程序的名称。关于调试的属性,见下面的文章: http://developer.android.com/guide/topics/manifest/application-element.html 而后,对于一个被"root"过的Android设备来说,开发者可以调试任何应用程序,所以不需要上面涉及的设置“debuggable”属性等过程。所有的应用程序都能够被DDMS(davilk debugger monitor service)看到,所以开发者可以使用调试器来调试它们。

1.3 Java代码调试基础

首先来看调试Android应用程序中Java代码的工作方式。下图是Android官方网站上给出的调试应用的架构图,其主要包括两个部分,也就是Android设备(或者Android模拟器)和远程调试机器,两者直接通过USB来连接,而负责调试的框架则是"adb"等工具所提供。

enter image description here

在Android设备中,当需要调试某个应用程序的时候,设备端的“adbd”会建立和应用之间的通信方式,并将通过USB建立同调试端的"adb host daemon"的连接。“adb host daemon”就是在开发机器中运行的后台服务进程,在安装Android SDK之后,使用“adb”会触发创建该服务进程。

实际上这一过程还是基于Java的远程调试协议来进程的,Android设计者们将它们直接拿过来使用。特别之处在于,Android建立了Java调试器和被调试程序之间的连接,这些连接是Android特别设计的。上图能够帮助大家理解这一过程。 “adb”是Android SDK中非常重要的工具,它可以用来安装、卸载应用程序,执行各种命令等,并且用来帮助调试Android应用。而DDMS称为 Dalvik Debug Monitor Server,能够提供port forwarding、屏幕截取、获取应用程序各种运行信息的一中工具,非常有用。在Eclipse中,开发者可以使用它对应用进行深层次的分析,细节不在这里的讨论范围内。

1.4 C/C++代码调试基础

因为并非所有的Android应用都使用C/C++代码和Android NDK来编写,所以这部分的内容对于调试某些应用其实是可选的。但是由于Chromium的绝大部分代码都是使用C++来编写的,所以调试本地代码是对学习Chromium来说是一个非常重要的部分。下图是笔者理解的本地代码的调试基础框架。

enter image description here

同Java远程调试类似的是也是需要Android设备和开发机器的协同工作。首先看Android设备上的应用。不同于Java远程调试,这里主要使用gdbserver来辅助调试,gdbserver是包含在NDK中并需要运行在Android设备上的。然后看开发机器上的gdb。读者应该不会陌生,gdb就是一个调试工具,不同于通常见到的,这里它可以调试一个远程的应用,不过它首要连接的是一个gdbserver。通过ADB所提供的port forwarding技术,gdb就能够同gdbserver进行通信,整个调试过程就自然串联起来。 要调试Android应用的本地代码同样需要上面类似的过程,也就是开启USB调试功能。不同点在于,只有被“root"后的Android设备才能被使用GDB调试本地代码,当然还有许多其它的调试技术不需要“root”设备,但是就使用gdb调试代码而言,开发者依然需要该Android设备被“root”过,这一条件显得太过于苛刻,但是笔者目前没有发现更好的办法。

1.5 各种调试技术

除了使用Java调试器和GDB调试器调试代码,还有一些其它的技术用来帮助开发者调试应用的一些问题,典型的例如Android的Logging机制、ANR、tombstone、内存分析等等,后面逐步介绍它们。

2. 调试Chromium代码

因为本身Chrome浏览器的代码没有全部开源出来,所以下面以调试Android系统上的Content Shell为例来说明如何调试Chromium代码的Java代码和本地代码。

2.1 调试Content Shell 的Java代码

值得注意的是,当使用虚拟设备来运行Chromium应用程序的时候,一定需要打开"-gpu on"这个选项,也就是使用开发机器的GPU来加速虚拟设备的图形操作,否则,Chromium不能在虚拟设备中运行。 下面这种是直接从Eclipse中启动某应用而后应用直接停在开发者设置的断点处。主要是由调试器本身启动应用,因而在启动应用的时候就会直接建立好调试所使用的环境设置。这一方法的优点就是可以设置任意的断点,并且都能够在断点处暂停执行。 enter image description here 上面说到的调试方法对于Chromium来说并不适用,原因在于Chromium不是直接由Eclipse创建并且编译的项目,Chromium使用自己一套复杂的脚本来编译自己的生成结果,虽然仍然依赖Android SDK和NDK。那么如果调试这种类型的应用呢?对于Content Shell来说,通常使用到的技术就是成为一种"post mortem"的技术,也就是说直接在Android中点击启动某应用,然后再使用调试工具连接(attach)上该应用。这是因为目前无法使用eclipse来编译Content Shell而生成应用的APK。那么如果想要调试一个Java代码,可能开发者来不及连接上该应用,代码已经被执行过了,有什么办法呢? Android提供了接口,也就是android.os.Debug.waitForDebugger()。Content Shell已经支持该机制,所以只需要在开发及其上执行如下命令即可: adb shell echo "chrome --wait-for-java-debugger" >>/data/local/tmp/content-shell-command-line 这样在Content Shell启动的时候,该应用首先读入上述“content-shell-command-line"文件,并且等待调试器连接上然后由调试器决定何时继续往下执行。在某些设备上,由于“开发者选项”中已经包含了“等待调试器”子选项,所以开发者并不需要上面复杂的设置,而是直接打开该选项即可。 那么读者可能要问了,Eclipse和Android调试工具需要怎么设置才能附着并连接上Content Shell应用程序呢?答案是在Eclipse中创建一个已有的Android项目,该项目指向Content Shell所在的根目录(该目录包含了Content Shell所使用的AndroidManifest.xml,读者很容易在Chromium源代码中找到),这样就创建了该Android项目。编译不过没有关系,开发者仍然可以调试Content Shell应用。同时,该项目不会包含所有的Java代码,所以开发者可以导入这些源代码,这样就可以方便的调试Content Shell应用了。下图是调试Chrome Shell(类似于Content Shell)的示例。

enter image description here

2.2 调试Content Shell的本地代码

Chromium的所有代码都会编译成一个动态库,该动态库会被包含在最终的Android APK中。实际上该动态库的大小非常大,超过1G大小,所以通常是将所有的符号信息去除之后才被包含到最终生成的APK中的。

调试Content Shell中的本地代码需要如下步骤: I. 使用ADB命令从设备中将app_process和系统库下载到本地开发机器上,放入目录“/absolute-source-path/out/target/product/product-name/symbols/system/lib”中。 II. 建立基于USB的连接也就是使用port forward机制,例如 adb forward tcp:5039 tcp:5039 III. 启动需要被调试的Android应用。 IV. 将gdbserver从NDK中拷贝到Android设备上,并在Android设备中启动“gdbserver”:gdbserver :5039 /system/bin/executable or gdbserver :5039 --attach pid V. 在开发机器上启动gdb并执行如下操作: file app_process directory /absolute-source-path/src set solib-absolute-prefix /absolute-source-path/out/target/product/product-name/symbols set solib-search-path /absolute-source-path/out/target/product/product-name/symbols/system/lib target remote :5039 shared 上面建立了调试工具和被调试应用之间的连接,开发者可以像传统使用gdb的方式来调试应用程序了。实际上调试Content Shell等Chromium编译出来的应用并不需要开发者重复上面复杂的过程,因为源代码中已经包含了各种调试脚本,见$CHROMIUM_SRC/build/android/中的以“adb_gdb*"开头的各个脚本,例如调试Content Shell直接使用adb_gdb_content_shell。这些脚本的目的就是执行上面所说的各个步骤,并且包含了灵活的扩展功能,可以设置参数来调试不同类型的编译结果和应用的不同进程(例如browser进程和sandboxed的renderer进程)。 要调试这些应用的本地代码,只能使用前面说的"post mortem"技术。Chromium使用了wait-for-debugger来等待gdb连接到该应用上,方法就是类似于上面提到的“等待调试器”技术,只不过不是Java Debugger而已: adb shell echo "chrome --wait-for-debugger" >>/data/local/tmp/content-shell-command-line

3. 其它调试技术

3.1 Logging

通过在代码中打印出不同级别的日志信息,开发者很容易知道应用成语在执行过程中的状态信息,而这并不需要各种各样的调试工具和“root”Android设备等麻烦步骤。更好的是,在Java代码和C/C++代码中都可以使用该功能:
Java代码使用:android.util.Log类,详情请见。
C/C++代码使用__android_log_print()函数。不要直接使用c的printf函数,那样在Android的控制台中看不到输出信息。
由此,开发者可以使用Android的"logcat"命令来查看日志输出的结果信息,如下面所示的命令:
adb shell logcat -b main -v threadtime

3.2 Tombstone

当某个应用发生崩溃的时候,Android系统会为该应用生成一种称为“tombstone”的文件,该文件包含了该应用发生崩溃时候的现场信息,包括调用栈、寄存器信息、线程信息等等。不过,由于应用通常并没有包含符号信息的动态库,所以开发者比较难以发现出错的未知。下图是一个tombstone文件:

enter image description here 例如在Chromium的Android版中,带有符号信息的动态库超过1G大小,这显然不利于把它直接放在APK文件中,那么怎么办呢? 笔者自己基于他人的开源代码编写了一个python脚本,使用它,开发者可以轻易的输入tombstone文件和带有符号信息的动态库,得到崩溃发现时候的调用栈,里面包含了详细的代码调用过程,非常方便并且有利于发现问题。该脚本的主要思想是利用NDK提供的arm-eabi-addr2line工具来将地址转换为源代码中的位置信息。主要的命令如下: cmd = "arm-linux-androideabi-addr2line" + " -f -e " + SYMBOLS_DIR + lib + " 0x" + addr

3.3 ANR

ANR的含义是指Application not responding,主要是指UI在指定的时间内没有响应,这时候Android系统会弹出一个对话框提示用户是等待还是杀死该应用程序。想要了解它背后的出错的详细信息和调用栈也非常容易,开发者可以使用下面的命令来获取:
adb shell dumpsys input

3.4 其它

在Android提供的开发者工具中,读者还可以发现更多用于调试程序的工具,例如用于收集内存使用、内存泄露信息的工具,它们能够帮助分析性能数据。例如DDMS提供的“hprof”,shell命令中的dumpsys等等。它们对于分析很多问题非常有用。限于篇幅这里不再一一介绍。

4. 参考资料

4.1 https://sites.google.com/a/itspaclub.com/www/android-debug/android-debug-theory/1-4-debugging-techniques

4.2 http://www.kandroid.org/online-pdk/guide