如何调试Android源码

关于如何调试Android源码问题,网上有很多文章,大概步骤就是:

  1. 下载Android源码
  2. 在本地编译整个源码
  3. 第二步编译成功后会生成一个idea工具,可以将Android源码转换成Idea工程(这一步会生成android.iml, android.ipr文件)
  4. 在Android Studio中导入第三步生成的两个文件,即可导入整个android源码

之后各种配置sdk路径,源码路径等等,非常麻烦。另外,最麻烦的是,下载Android源码比较缓慢,并且非常占存储空间,加上编译可能会占据150-200G的磁盘空间,对于一些个人电脑(例如我的mac存储空间就只有128G, 能用于放源码的空间就更少了)。鉴于此,想到两个问题:

  1. 是否需要下载整个Android 源码?
  2. 是否一定要将源码编译一次?

首先想到的一个问题是,通常我们会调试哪些代码?对大多数人而言,只是调试framework以及system server相关的代码,而这两部分代码都是在源码的frameworks/base项目下,那么是否可以只下载这一部分代码?

为了回答上述几个问题,先来看看android程序是怎么调试的?

Android是如何调试程序的?

调试机制

Android的调试机制采用java的远程调试,每个Android进程都有一个叫做jdwp的线程,本地debugger进程通过jdwp协议与运行在真实设备或者AVD上的android进程进行通行,主要通信内容包括两方面:

  1. debugger向被调试进程发送命令(如:在某一行上设置断点,单步执行,运行到下一个断点等等)
  2. debugger从被调试进程接收程序运行数据及状态等等(例如: 程序已经运行到某个断点,当前运行行号,变量值等等)

通过jdwp协议,debugger可以控制被调试进程到运行,了解当前运行状态,行号等等,通过行号结合源码,即可实现调试。

哪些进程可以被调试

要想调试一个进程,需要被调试进程满足两个条件:

  1. 进程执行到代码包含调试信息(主要就是行号)
  2. 本地的源码必须跟被调试进程所运行的代码完全一样,否则就牛头不对马嘴了
    因此,被调试进程需要是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中定位到具体到源文件呢?
这里可以有两个猜想:

  1. 从src/java目录下找。
    普通Android项目所有源码都是在src/java目录下,既然已经能拿到包
    名和文件名,应该也就能找到对应到源码文件了。
  2. Android studio会对所有到源码文件建立索引,并且同一个apk中不可能出现全限定名完全相同的类,猜测其会根据类全限定名,对源码文件建立索引。

为了验证猜想,我在src目录下新建一个子目录java2, 并建立相应对包名,然后将Test.java移动到对应到包目录下,然后启动进程,并attach到想应的进程,在Test.java中打上断点,其结果是,程序运行到断点处,依然能够停下。通过这个简单的实验,可以断定,猜想一必然是不正确的,那很可能就是猜想二了,鉴于没有好的方法去验证猜想二,暂且假定其就是对的。

开始调试源码

假定猜想二(基于类限定名建立索引)正确,也就是说,Android Studio会对工程目录下所有java文件建立索引,而不管其处于什么目录。

下载源码

绝大多数情况,我们是调试自己的程序,也就是frameworks代码,或者system server进程。这两部分代码主要都在frameworks/base项目下,因此只下载frameworks/base项目即可

导入源码
  1. 点击File->Close Project关闭当前项目,之后可以选择导入Project
如何调试Android源码_第1张图片
屏幕快照 2017-10-29 下午11.47.55.png

选择下载好的frameworks目录, 点击下一步,选择Create project from existing sources

如何调试Android源码_第2张图片
屏幕快照 2017-10-29 下午11.50.57.png

之后会让选择要导入的源码

如何调试Android源码_第3张图片
屏幕快照 2017-10-29 下午11.52.44.png

这里会列出frameworks下的所有源码目录,可以只选择自己感兴趣的,这一步可以看出Android Studio会维持一堆源码目录,有了类全限定名就可以在这些目录下找到具体堆源码文件。鉴于代码本身并不是很多,我这里选择全选。之后一直下一步即可。
导入完成后,切换到Project视图,可以看到

如何调试Android源码_第4张图片
屏幕快照 2017-10-29 下午11.55.37.png
开始调试

找到ActivityManagerService文件,在startActivityAsUser方法中打上断点(基于Android M源码 ):

如何调试Android源码_第5张图片
屏幕快照 2017-10-29 下午11.57.35.png

由于这里调试的是system_server, 因此我们attach到system_process进程


如何调试Android源码_第6张图片
屏幕快照 2017-10-30 上午12.00.28.png

然后,点击桌面任意一个app图标,可以看到进程在断点处停下了:

如何调试Android源码_第7张图片
屏幕快照 2017-10-29 下午11.59.19.png

至此,我们就完成了源码调试工作,看看相比其他调试方案的优点

  1. 不需要下载全部源码,只需要下载感兴趣的模块即可。
  2. 完全不需要任何本地编译
  3. 不需要任何的配置,只需一步步导入源码即可

你可能感兴趣的:(如何调试Android源码)