转载请注明原文地址:http://blog.csdn.net/milado_nju
1. Android上的调试技术
在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调试”选项,开发人员须要打开该选项才干够通过开发机器调试该设备上的应用程序。
在“开发人员选项”中还有许多对开发人员很实用的子选项,比如“不锁定屏幕”。笔者很喜欢这一功能,它有助于开发人员调试程序而不会出现设备总是锁屏等烦人的情况。
当设置完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"等工具所提供。
在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来说是一个很重要的部分。下图是笔者理解的本地代码的调试基础框架。
同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中启动某应用而后应用直接停在开发人员设置的断点处。主要是由调试器本身启动应用,因而在启动应用的时候就会直接建立好调试所使用的环境设置。这一方法的长处就是能够设置随意的断点,而且都能够在断点处暂停执行。
上面说到的调试方法对于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,读者很easy在Chromium源码中找到),这样就创建了该Android项目。编译只是没有关系,开发人员仍然能够调试Content Shell应用。同一时候,该项目不会包括全部的Java代码,所以开发人员能够导入这些源码,这样就能够方便的调试Content Shell应用了。下图是调试Chrome Shell(相似于Content Shell)的演示样例。
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
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
通过在代码中打印出不同级别的日志信息,开发人员很easy知道应用成语在执行过程中的状态信息,而这并不须要各种各样的调试工具和“root”Android设备等麻烦步骤。更好的是,在Java代码和C/C++代码中都能够使用该功能:
Java代码使用:android.util.Log类,详情请见。
C/C++代码使用__android_log_print()函数。不要直接使用c的printf函数,那样在Android的控制台中看不到输出信息。
由此,开发人员能够使用Android的"logcat"命令来查看日志输出的结果信息,如以下所看到的的命令:
由此,开发人员能够使用Android的"logcat"命令来查看日志输出的结果信息,如以下所看到的的命令:
adb shell logcat -b main -v threadtime
3.2 Tombstone
当某个应用发生崩溃的时候,Android系统会为该应用生成一种称为“tombstone”的文件,该文件包括了该应用发生崩溃时候的现场信息,包括调用栈、寄存器信息、线程信息等等。只是,由于应用通常并没有包括符号信息的动态库,所以开发人员比較难以发现出错的未知。下图是一个tombstone文件:
比如在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系统会弹出一个对话框提示用户是等待还是杀死该应用程序。想要了解它背后的出错的具体信息和调用栈也很easy,开发人员能够使用以下的命令来获取:
adb shell dumpsys input
adb shell dumpsys input
3.4 其他
在Android提供的开发人员工具中,读者还能够发现许多其他用于调试程序的工具,比如用于收集内存使用、内存泄露信息的工具,它们能够帮助分析性能数据。比如DDMS提供的“hprof”,shell命令中的dumpsys等等。它们对于分析许多问题很实用。限于篇幅这里不再一一介绍。