作者介绍
2014年7月加入去哪儿网,参与过客户端hybird的迭代开发,以及酒店业务、酒店风控的开发,现负责抓取系统开发与维护。
随着 android 的蓬勃发展,相关的新技术层出不穷,像从 2014 年左右发展起来的插件化技术,热修复,自动化测试等等。以及逆向分析相关的 Art Hook 、多开、VirtualApp 、以及 ROM 定制等等,这些技术的学习与开发都离不开对 android 源码的熟悉了解。本文从源码环境配置,调试技巧,通过案例来阐述如何解决开发和学习过程中碰到的一些问题的思考方式以及技巧。
一、android源码目录简介
android 源码目录非常多,阅读源码绝不是从源码工程按顺序一个文件一个文件的看。好的阅读源码的方法应该是抓住某条主线,从上层往底层,不断地追溯,在各个模块、文件、方法之间来回跳转,反复地阅读,理清整个流程的逻辑。看源码的时候要带着思考去看,反复揣摩作者用意,理解代码的精妙与可能存在的缺陷,总结优秀代码的设计思想。下面简单介绍一下阅读 android 源码过程常用到的一些目录。
frameworks:android 最核心的目录,日常看得最多的目录;
bionic:libc 源码目录;
art:art 虚拟机;
kernel:内核目录,各种系统调用的实现,驱动等;
system/core :adb,debugger 等的源码目录;
packages/apps:android 系统 app ,比如 phone ,music 等等。
二、配置源码阅读环境
目前网上也有一些非常好用的查看源码的工具网站例如:http://aospxref.com/ ,但是在线查看源码,效率容易受网速影响,而且使用起来没有把代码下载到本地再借助一些工具查看方便。最重要的是本地的源码借助一些工具能直接 debug ,分析问题效率倍增。在本地查看源码该选用什么工具呢,对于 android 开发者首推 android studio 。使用 android studio 不仅更合乎 android 开发者的习惯,而且配置得好速度并不比别的工具差,更重要的是 android sudio 具有强大的调试功能。接下来说说如何把源码导入 android 以及如何优化配置。下载好源码 android 系统源代码后(关于如何下载源码,网上有很多教程可以参考,这里就不啰嗦了),为了方便把源码导入 android studio 阅读,我们需要利用 android 源码自带的 idegen 工具生成配置文件。具体步骤如下:
1、source build/envsetup.sh 进入源码的根目录执行设置环境脚本的命令
2、lunch AOSP 源码可以用 lunch ,如果是 LineageOS 源码用需要用对应的命令否则会报错。
3、make idegen -j4
4、sudo development/tools/idegen/idegen.sh 这一步需要加上 sudo ,否则过程中有些步骤会因为权限不够而失败。如果出现找不到命令,修改一下脚本把 java 路径加入就能顺利生成 android.iml android.ipr .classpath 这三个文件了。
5、修改 android.iml 由于 android 源码文件数量太多,android studio 导入源码创建索引需要花费很长时间,为了能加快导入与打开速度,需要对 android.iml 做些修改,一般第一次使用可以把除 frameworks 等几个关键的目录外的其它目录全部加入 excludeFolder 节点。
另外需要把除下面这两个 orderEntry 节点外的其它 orderEntry 节点全部删掉。
导入完成后 excludeFolder 与 orderEntry 节点也可以在 Project Structure面板修改跟直接修改 android.iml 文件效果一样。打开 Project Structure 面板 Module SDK 选择跟源码对应版本的 SDK ,这一步对接下来的源码调试非常重要。做完这一步接下来就可以愉快的阅读源码了。
三、调试源码
andriod 系统源码代码量非常大,光靠阅读想要弄清楚其中的原理是远远不够的,这时候就需要借助调试来最终代码运行逻辑。基础的调试相信大部分人都十分熟悉了,就不重复了,下面分享几个能提高效率的调试技巧。在开始介绍调试技巧之前,再啰嗦一句:调试之前一定要 attach 到正确的进程,选错进程是无法断点到对应的代码的或逻辑的。为什么要强调这个,因为有时候我们会面临代码会被几个进程执行的情况,所以开始调试之前一定要想想清楚该 attach 到哪个进程。以调试 Airtest 的 Yosemite Help 的 Accessibility 服务开关的开启为例,介绍一下如何调试 android 源码:
Accessibility 服务开关是在 Setting 中,所以我们第一步就要把 Setting 这个 app 打开,这时候才会显示com.android.setting这个进程,attach上这个进程,找到对应的源码打上断点,点击 Yosemite Help 的 Accessibility 服务开关就能触发断点了。
方法断点:
方法断点能让我们很容易观察函数的参数与返回值,非常实用。另外方法断点不同于普通断点,普通断点是以行为单位的,碰到行号对应不上的情况时,普通断点就无能为力了。这时我们可以使用方法断点,来处理行号对应不上的情况。
为了方便说明,我在源码中加入很多空行,并在 onCreateDialog 下个方法断点。
代码断在了源码修改前的位置,栈帧中显示了代码实际运行的方法与行号,这时候我们可以使用 Step Over 等命令观察方法内部的局部变量,可以结合源码与 Variables 来推测代码逻辑。
条件断点:
这里可以使用作用域为断点所在位置的各种变量,包括方法参数等。使用 condition 中有多个变量时, android studio 会提示错误,只要表达式的结果是布尔型就能正常工作,直接忽略报错就行。
日志断点:
在 breakpoint 选项框中把 stack trace 勾选上,能把代码调用堆栈用 log 的形式打印出来,能大大加快追踪代码的速度。
有时候我们需要使用日志来打印一些感兴趣的变量。常规的做法是在代码中加入 log 代码,对于普通的 app 这样或许还行得通,但是对于调试系统源码来说,这种做法效率就太低了。android studio 的调试功能给我们提供了一个非常好用的工具:我们可以在需要输出log的地方下断点,在 Breakpoints 面板里面把对应断点的 Suspend 的勾去掉,勾上 Evaluate and log 选项,在这里填入需要输出 log ,这样代码走到这个断点的时候只输出 log ,然后就继续往下执行了,比添加log 代码的方式效率提高了N 倍。
android studio 的调试功能非常强大,限于篇幅这里只介绍了一部分,感兴趣的可以自己去找资料深入学习一下。
四、代码分析
代码环境搭建好了,调试的技巧也清楚了,接下来我们要通过解决一个业务中出现的问题,来阐述遇到问题该如何分析与思考。我们都知道 android 6.0+对于一些应用需要的危险权限,比如写sdcard的的权限READ_EXTERNAL_STORAGE,是需要动态申请的,申请的时候需要弹框让用户确认。某些情况下(比如自动化测试时)我们想静默的替用户同意授予这些权限不再弹框该怎么办呢?这时候就要从源码中找答案了。
请求动态权限是通过调用 activity 的 requestPermissions 方法或者 ActivityCompat.requestPermissions 方法方法申请的,而 ActivityCompat.requestPermissions 最终也是调用 activity 的 requestPermissions 方法实现的。requestPermissions方法是通过 startActivityForResult 发送一个 action为“android.content.pm.action.REQUEST_PERMISSIONS”的隐式 intent 调起一个系统界面来给用户选择是同意还是禁止。
直接在源码中全局搜索这个action,能在packages/apps/PackageInstaller/AndroidManifest.xml 这个文件中找到响应这个 intent 是 GrantPermissionsActivity 这个 activity ,而这个 activity 是在 packageinstaller 这个系统 app 里面的。
attach这个进程,接下来我们通过调试,来简要分析一下GrantPermissionsActivity中处理授权的过程:
1、创建mAppPermissions对象,并在这个对象初始化的时候从 PackageManagerService 中获取目标 app 在 AndroidManifest.xml 中配置的权限并分组;
com/android/packageinstaller/permission/model/AppPermissions.java
2、循环目标 app 动态申请的权限,如果在第 1 步的某个组中且还未授权,就把这个组加入 mRequestGrantPermissionGroups 这个 map 中。
com/android/packageinstaller/permission/ui/GrantPermissionsActivity.java
3、给弹框页面设置 View ,并轮询第 2 步中得到 map ,按组挨个展示权限授予 UI ,给用户选择禁止还是允许,直到轮询完毕,调用 setResult 与 finish 方法。如果已经授予了权限目标 app 还继续申请,mRequestGrantPermissionGroups 中的结合为空,showNextPermissionGroupGrantRequest 会很快返回 false ,用户也几乎感知不到弹框。
com/android/packageinstaller/permission/ui/GrantPermissionsActivity.java
4、在第 3 步中按组轮询权限的时候,用户的每次选择都会回调到 onPermissionGrantResult 这个方法,如果用户同意会通过 grantRuntimePermissions 这个方法最终调用 PackageManagerService 的 grantRuntimePermission方法更新PermissionsState中的mPermissions集合中的PermissionData 对象的状态。最后在把授权的结果通过 setResult 回传给目标 app ,方便目标 app 根据授权结果执行业务逻辑。
com/android/packageinstaller/permission/ui/GrantPermissionsActivity.java
有了上面的基础,我们就要开始思考该怎么在阻止系统弹出授权框的同时授予目标 app 请求的权限,以及在哪个地方实现这个逻辑是最优的。这些两个问题没有标准答案,需要根据业务场景以及自己的系统架构逻辑确定。通常我们可以直接在 GrantPermissionsActivity 里面修改,启动的时候,直接把请求权限授予了,同时把结果通过 setResult 回传,这样操作用户几乎无感知,同时修改起来也非常简单,是一种不错的实现方案。这种实现方案跨越了三个进程,目标 app 进程 /system_process 进程 /packageinstaller 进程,有没有办法减少到只是目标 app 进程 /system_process 进程这两个进程交互呢。
下面我们针对这种情况来探讨一下实现方法:通过代码分析我们知道权限请求,会调用到 system_process 进程(系统进程)的 ActivityManagerService的startActivity ,这里是这次调用的系统进程执行比较早的方法,我们可以尝试在这里直接授予权限,然后把结果返回给目标 app ,是一个理想的修改点。确定了修改点,接下来就要考虑怎么修改了。
首先,我们可以把 GrantPermissionsActivity 中授予权限的逻辑直接挪过来。其次,我们要考虑到的就是如何把结果返回给目标 app ,目标 app 是通过 startActivityForResult 发起权限请求的,用的是系统 ActivityForResult 这套逻辑来交互数据的,但是此处并没有 activity ,该怎么办呢?
com/android/server/am/ActivityManagerService.java
这时又得从源码找解决办法了,我们追踪 for result 的逻辑发现,发现在系统进程中最终是通过 ApplicationThreadProxy 的 scheduleSendResult 方法来发送数据的,这里有两个关键对象 token 与 mRemote ,我们想要模仿这个逻辑就必须拿到这两个对象,通过断点中显示的值与 startActivity 断点中的参数值对比,我们发现 mRemote 跟 caller 中的 mRemote 是一样的, token 与 resultTo 是一样的,至此我们就能借助 caller 与 resultTo ,把授权结果告知目标 app 。app 能无感知的接收结果,处理业务逻辑了。
android/app/ApplicationThreadNative.java
除了上面介绍的 2 中方法外,还可以尝试在 app 进程中自动授予权限,这样就能不用在系统进程中做处理了,但是这样可能会碰到的问题是 app 进程没有权限来给自己授予权限,可能需要借助其它有权限的进程,或者修改系统权限逻辑来实现,有兴趣的可以自行尝试。一个问题的解决方案有非常非常多种,如何实现全凭你对源码的了解程度以及业务需求。
五、总结
本文主要介绍了 android 一些主要目录、环境的搭建、一些调试技巧,以及一个源码分析案例。希望能对源码学习的有兴趣的同学少走弯路甚至有些启发。在技术栈方面,学习 android 源码不仅需要会 java 也要会 c/c++ ,甚至是汇编。另外 android 是基于 linux kernel 的,许多的功能实现需要依赖系统调用,要想了解深入最好对 linux kernel 也有一定理解。平时学习的时候还可以多看看老罗的 blog ,虽然老罗的文章是基于比较老的 android 版本写的,但是细节写得非常好,一些原理还是值得参考的。另外多看官方文档也会有很大帮助。
参考文档:
https://blog.csdn.net/Luoshengyang?t=1
END