关于如何调试Android源码问题,网上有很多文章,大概步骤就是:
- 下载Android源码
- 在本地编译整个源码
- 第二步编译成功后会生成一个idea工具,可以将Android源码转换成Idea工程(这一步会生成android.iml, android.ipr文件)
- 在Android Studio中导入第三步生成的两个文件,即可导入整个android源码
之后各种配置sdk路径,源码路径等等,非常麻烦。另外,最麻烦的是,下载Android源码比较缓慢,并且非常占存储空间,加上编译可能会占据150-200G的磁盘空间,对于一些个人电脑(例如我的mac存储空间就只有128G, 能用于放源码的空间就更少了)。鉴于此,想到两个问题:
- 是否需要下载整个Android 源码?
- 是否一定要将源码编译一次?
首先想到的一个问题是,通常我们会调试哪些代码?对大多数人而言,只是调试framework以及system server相关的代码,而这两部分代码都是在源码的frameworks/base项目下,那么是否可以只下载这一部分代码?
为了回答上述几个问题,先来看看android程序是怎么调试的?
Android是如何调试程序的?
调试机制
Android的调试机制采用java的远程调试,每个Android进程都有一个叫做jdwp的线程,本地debugger进程通过jdwp协议与运行在真实设备或者AVD上的android进程进行通行,主要通信内容包括两方面:
- debugger向被调试进程发送命令(如:在某一行上设置断点,单步执行,运行到下一个断点等等)
- debugger从被调试进程接收程序运行数据及状态等等(例如: 程序已经运行到某个断点,当前运行行号,变量值等等)
通过jdwp协议,debugger可以控制被调试进程到运行,了解当前运行状态,行号等等,通过行号结合源码,即可实现调试。
哪些进程可以被调试
要想调试一个进程,需要被调试进程满足两个条件:
- 进程执行到代码包含调试信息(主要就是行号)
- 本地的源码必须跟被调试进程所运行的代码完全一样,否则就牛头不对马嘴了
因此,被调试进程需要是debug的,通常编译debug的apk即可。另外,需要有与源码对应的手机(例如Google 的Nexus)。
有了行号信息,及源码,那Android Studio又是如何把他们关联起来都呢?
Android Studio如何关联源码与被调试程序?
要搞清这个问题,先来看看编译生成的debug apk是如何包含行号信息的。apk的代码最终都被打包到了dex文件中,因此,先来dex文件中看下能否找到相关信息。 下面是一个Android项目中普通Java类
package log.test.com.myapplication;
public class Test {
public void sayHello() {
android.util.Log.d("test", "hello");
}
}
通过dexdump查看dex中Test类都相关信息
Class #1097 -
159016 Class descriptor : 'Llog/test/com/myapplication/Test;'
159017 Access flags : 0x0001 (PUBLIC)
159018 Superclass : 'Ljava/lang/Object;'
159019 Interfaces -
159020 Static fields -
159021 Instance fields -
159022 Direct methods -
Llog/test/com/myapplication/Test;
159037 Virtual methods -
159038 #0 : (in Llog/test/com/myapplication/Test;)
159039 name : 'sayHello'
159040 type : '()V'
159041 access : 0x0001 (PUBLIC)
159042 code -
159043 registers : 3
159044 ins : 1
159045 outs : 2
159046 insns size : 8 16-bit code units
159047 catches : (none)
0f430c: |[0f430c] log.test.com.myapplication.Test.sayHello:()V
244434 0f431c: 1a00 cd44 |0000: const-string v0, "test" // string@44cd
244435 0f4320: 1a01 822c |0002: const-string v1, "hello" // string@2c82
244436 0f4324: 7120 a43c 1000 |0004: invoke-static {v0, v1}, Landroid/util/Log;.d:(Ljava/lang/String;Ljava/lang/String;)I // method@3ca4
244437 0f432a: 0e00 |0007: return-void
244438 catches : (none)
244439 positions :
244440 0x0000 line=5
244441 0x0007 line=6
244442 locals :
244443 0x0000 - 0x0008 reg=2 this Llog/test/com/myapplication/Test;
244444
244445 source_file_idx : 6457 (Test.java)
可以看到方法sayHello对应的positions
positions :
159049 0x0000 line=5
159050 0x0007 line=6
源文件的第五行对应字节码第一行(从常量池读取"test"字符串),源文件第六行对应字节码第七行(字节码从0计数,且上述代码为简化展示,去除了构造函数相关信息), 即return-void。
从上述dex文件内容还可以看到这个类对应第源文件是Test.java,以及
Class descritor(包含包名). 那么问题来了,有了行号和文件名,是否就能在Android Studio中定位到具体到源文件呢?
这里可以有两个猜想:
- 从src/java目录下找。
普通Android项目所有源码都是在src/java目录下,既然已经能拿到包
名和文件名,应该也就能找到对应到源码文件了。 - Android studio会对所有到源码文件建立索引,并且同一个apk中不可能出现全限定名完全相同的类,猜测其会根据类全限定名,对源码文件建立索引。
为了验证猜想,我在src目录下新建一个子目录java2, 并建立相应对包名,然后将Test.java移动到对应到包目录下,然后启动进程,并attach到想应的进程,在Test.java中打上断点,其结果是,程序运行到断点处,依然能够停下。通过这个简单的实验,可以断定,猜想一必然是不正确的,那很可能就是猜想二了,鉴于没有好的方法去验证猜想二,暂且假定其就是对的。
开始调试源码
假定猜想二(基于类限定名建立索引)正确,也就是说,Android Studio会对工程目录下所有java文件建立索引,而不管其处于什么目录。
下载源码
绝大多数情况,我们是调试自己的程序,也就是frameworks代码,或者system server进程。这两部分代码主要都在frameworks/base项目下,因此只下载frameworks/base项目即可
导入源码
- 点击File->Close Project关闭当前项目,之后可以选择导入Project
选择下载好的frameworks目录, 点击下一步,选择Create project from existing sources
之后会让选择要导入的源码
这里会列出frameworks下的所有源码目录,可以只选择自己感兴趣的,这一步可以看出Android Studio会维持一堆源码目录,有了类全限定名就可以在这些目录下找到具体堆源码文件。鉴于代码本身并不是很多,我这里选择全选。之后一直下一步即可。
导入完成后,切换到Project视图,可以看到
开始调试
找到ActivityManagerService文件,在startActivityAsUser方法中打上断点(基于Android M源码 ):
由于这里调试的是system_server, 因此我们attach到system_process进程
然后,点击桌面任意一个app图标,可以看到进程在断点处停下了:
至此,我们就完成了源码调试工作,看看相比其他调试方案的优点
- 不需要下载全部源码,只需要下载感兴趣的模块即可。
- 完全不需要任何本地编译
- 不需要任何的配置,只需一步步导入源码即可