前言
上两篇博客中提到了构建过程的问题,之前毕业在准备面试的过程中,对这个部分有过较为认真的学习,也进行了博客记录,但是实际工作过程中,如果是在写业务逻辑上,那么这方面的问题接触的就会比较少了.逐渐的淡忘了,其次,之前所写的文章条理性也不是很强,同时,最近准备进行 Gradle 插件的一系列博客的产出,其中将会涉及到很多与项目构建相关的内容.所以此文也将成为后续文章的一个铺垫.
构建过程
项目的构建: 当我们打开一个项目,我们可以看到的是我们写的 Java Code 文件 or Other JVM Code,资源文件,Build 配置文件,但是通过 run the project,我们就可以得到一个在我们的 Andoid 设备上可以运行的 Apk,上线应用市场,还需要我们对其进行签名处理,来确保我们 App 的唯一性和安全性.整个过程就是所谓的项目构建.
如何实现整个构建的过程,对于每一个构建的步骤,都需要相应的功能模块来进行,比如 Java Code 编译,如何打成 dex 包等等,而这 Android 则为我们提供了相应的工具,在 Android Studio 命令行窗口中,我们可以通过相应的命令行来进行控制,但是,整个构建过程涉及到很多的步骤,很多的工具的使用,如果都通过命令行来进行控制,势必会相当麻烦,因此 Androd Studio 等 IDE 则对整个过程进行了一个打包,当我们在 Run project 的时候,底层的打包工具就会被调用,打包流程都会自动执行.然后我们只需要对构建文件按照自己的需求进行相应的配置,就可以构建出自己所需要的项目.
那么,整个 Andoid 项目的构建过程中,都执行了那些构建的任务呢?
首先看一下,Google 官方为我们提供的详细的构建过程图
如果你接触 Android 开发已经有一段时间了,我想当你看到这张图的时候,就会觉得很清晰.但是更多的可能会一头雾水,如果之前没有阅读相关的资料的话,那么,接下来,将针对上述的构建过程,先给出一个概述,这样你将会整个构建流程在心中有一个框架,然后针对其中具体的细节,进行进一步详细的讲解.
图中绿色标注为其中用到的相应工具,蓝色代表的是中间生成的各类文件类型.
首先 aapt 工具会将资源文件进行转化,生成对应资源 ID 的 R 文件和资源文件.
adil 工具会将其中的 aidl 接口转化成 Java 的接口
至此,Java Compiler 开始进行 Java 文件向 class 文件的转化,将 R 文件,Java 源代码,由 aidl 转化来的 Java 接口,统一转化成. class 文件.
通过 dx 工具将 class 文件转化为 dex 文件.
此时我们得到了经过处理后的资源文件和一个 dex 文件,当然,还会存在一些其它的资源文件,这个时候,就是将其打包成一个类似 apk 的文件.但还并不是直接可以安装在 Android 系统上的 APK 文件.
通过签名工具对其进行签名.
通过 Zipalign 进行优化,提升运行速度(原理后文会提及).
最终,一个可以安装在我们手机上的 APK 了.
通过上述讲解,我想对于 Android 项目的整个构建过程,应该有了一个很清晰的框架了,下面将针对其中的具体的细节,和前面挖的一些坑,来进行更细致的分析,下图是一个 Android 项目构建过程的详细步骤图.
接下来的分析,我们还是按照上述构建过程概述的顺序和流程,进行具体的分析.
第 1 步:aapt 打包资源文件,生成 R.java 和编译后的资源(二进制文件)
讲到资源文件的处理,我们先来看一下 Android 中的资源文件有那些呢? Android 应用程序资源可以分为两大类,分别是 assets 和 res: 1. assets 类资源放在工程根目录的 assets 子目录下,它里面保存的是一些原始的文件,可以以任何方式来进行组织.这些文件最终会被原装不动地打包在 apk 文件中.如果我们要在程序中访问这些文件,那么就需要指定文件名来访问.例如,假设在 assets 目录下有一个名称为 filename 的文件,那么就可以使用以下代码来访问它:
AssetManager am= getAssets();
InputStream is = assset.open("filename");
2. res 类资源放在工程根目录的 res 子目录下,它里面保存的文件大多数都会被编译,并且都会被赋予资源 ID.这样我们就可以在程序中通过 ID 来访问 res 类的资源.res 类资源按照不同的用途可以进一步划分为以下 10 种子类型: layout(布局文件),drawable,xml,value,menu,raw,color,anim,animator,mipmap. 为了使得一个应用程序能够在运行时同时支持不同的大小和密度的屏幕,以及支持国际化,即支持不同的国家地区和语言,Android 应用程序资源的组织方式有 18 个维度,每一个维度都代表一个配置信息,从而可以使得应用程序能够根据设备的当前配置信息来找到最匹配的资源来展现在 UI 上,从而提高用户体验.由于 Android 应用程序资源的组织方式可以达到 18 个维度,因此就要求 Android 资源管理框架能够快速定位最匹配设备当前配置信息的资源来展现在 UI 上,否则的话,就会影响用户体验.为了支持 Android 资源管理框架快速定位最匹配资源,Android 资源打包工具 aapt 在编译和打包资源的过程中,会执行以下两个额外的操作:
赋予每一个非 assets 资源一个 ID 值,这些 ID 值以常量的形式定义在一个 R.java 文件中.
生成一个 resources.arsc 文件,用来描述那些具有 ID 值的资源的配置信息,它的内容就相当于是一个资源索引表.包含了所有的 id 值的数据集合.在该文件中,如果某个 id 对应的是 string,那么该文件会直接包含该值,如果 id 对应的资源是某个 layout 或者 drawable 资源,那么该文件会存入对应资源的路径.
为什么要转化为二进制文件?
二进制格式的 XML 文件占用空间更小.这是由于所有 XML 元素的标签,属性名称,属性值和内容所涉及到的字符串都会被统一收集到一个字符串资源池中去,并且会去重.有了这个字符串资源池,原来使用字符串的地方就会被替换成一个索引到字符串资源池的整数值,从而可以减少文件的大小.
二进制格式的 XML 文件解析速度更快.这是由于二进制格式的 XML 元素里面不再包含有字符串值,因此就避免了进行字符串解析,从而提高速度. 有了资源 ID 以及资源索引表之后,Android 资源管理框架就可以迅速将根据设备当前配置信息来定位最匹配的资源了.
对于具体的一些操作流程,可以参考本人之前的一篇文章 APK 打包安装过程 或者更偏向于源码层级的老罗的文章.(文后参考文献链接)
第 2 步:aidl
aidl,全名 Android Interface Definition Language,即 Android 接口定义语言.是我们在编写进程间通信的代码的时候,定义的接口. 输入:aidl 后缀的文件.输出:可用于进程通信的 C/S 端 java 代码,位于 build/generated/source/aidl.
第 3 步:Java 源码编译
我们有了 R.java 和 aidl 生成的 Java 文件,再加上工程的源代码,现在可以使用 javac 进行正常的 java 编译生成 class 文件了.
输入:java source 的文件夹(另外还包括了 build/generated 下的:R.java, aidl 生成的 java 文件,以及 BuildConfig.java).输出:对于 gradle 编译,可以在 build/intermediates/classes 里,看到输出的 class 文件.
第 4 步:代码混淆(proguard)
源码编译之后,我们可能还会对其进行代码的混淆,混淆的作用是增加反编译的难度,同时也将一些代码的命名进行了缩短,减少代码占用的空间.混淆完成之后,会生成一个混淆前后的映射表,这个是用来在反应我们的应用执行的时候的一些堆栈信息,可以将混淆后的信息转化为我们混淆前实际代码中的内容. 而这个过程使用的工具就是 ProGuard,是一个开源的 Java 代码混淆器(obfuscation).ADT r8 开始它被默认集成到了 Android SDK 中. 其具备三个主要功能.
压缩 - 移除无效的类,属性,方法等
优化 - 优化 bytecode 移除没用的结构
混淆 - 把类名,属性名,方法名替换为晦涩难懂的 1 到 2 个字母的名字 当然它也只能混淆 Java 代码,Android 工程中 Native 代码,资源文件(图片,xml),它是无法混淆的.而且对于 Java 的常量值也是无法混淆的,所以不要使用常量定义平文的密码等重要信息.同时对于混淆,我们可以通过代码制定去混淆那些,不去混淆那些.
-keep public class com.rensanning.example.Test
第 5 步:转化为 dex
调用 dx.bat 将所有的 class 文件转化为 classes.dex 文件,dx 会将 class 转换为 Dalvik 字节码,生成常量池,消除冗余数据等.由于 dalvik 是一种针对嵌入式设备而特殊设计的 java 虚拟机,所以 dex 文件与标准的 class 文件在结构设计上有着本质的区别, 当 java 程序编译成 class 后,使用 dx 工具将所有的 class 文件整合到一个 dex 文件,目的是其中各个类能够共享数据,在一定程度上降低了冗余,同时也是文件结构更加经凑,实验表明,dex 文件是传统 jar 文件大小的 50% 左右.class 文件结构和 dex 文件结构比对.
第 6 步:apkbuilder
打包生成 APK 文件.旧的 apkbuilder 脚本已经废弃,现在都已经通过 sdklib.jar 的 ApkBuilder 类进行打包了.输入为我们之前生成的包含 resources.arcs 的. ap_文件,上一步生成的 dex 文件,以及其他资源如 jni,.so 文件. 大致步骤为 以包含 resources.arcs 的. ap_文件为基础,new 一个 ApkBuilder,设置 debugMode
apkBuilder.addZipFile(f);
apkBuilder.addSourceFolder(f);
apkBuilder.addResourcesFromJar(f);
apkBuilder.addNativeLibraries(nativeFileList);
apkBuilder.sealApk(); // 关闭apk文件
generateDependencyFile(depFile, inputPaths, outputFile.getAbsolutePath());
第 7 步:对 APK 签名
对 APK 文件进行签名.Android 系统在安装 APK 的时候,首先会检验 APK 的签名,如果发现签名文件不存在或者校验签名失败,则会拒绝安装,所以应用程序在发布之前一定要进行签名.签名信息中包含有开发者信息,在一定程度上可以防止应用被伪造.对一个 APK 文件签名之后,APK 文件根目录下会增加 META-INF 目录,该目录下增加三个文件:
MANIFEST.MF
[CERT].RSA
[CERT]
Android 系统就是根据这三个文件的内容对 APK 文件进行签名检验的.签名过程主要利用 apksign.jar 或者 jarsinger.jar 两个工具.将根据我们提供的 Debug 和 Release 两个版本的 Keystore 进行相应的签名.
MANIFEST.MF 中包含对 apk 中除了 / META-INF 文件夹外所有文件的签名值,签名方法是先 SHA1()(或其他 hash 方法) 在 base64().存储形式是:Name 加 [SHA1]-Digest.
[CERT].SF 是对 MANIFEST.MF 文件整体签名以及其中各个条目的签名.一般地,如果是使用工具签名,还多包括一项.就是对 MANIFEST.MF 头部信息的签名.
[CERT].RSA 包含用私钥对 [CERT].SF 的签名以及包含公钥信息的数字证书.
第 8 步:zipalign 优化
Zipalign 是一个 Android 平台上整理 APK 文件的工具,它首次被引入是在 Android 1.6 版本的 SDK 软件开发工具包中.它能够对打包的 Android 应用程序进行优化, 以使 Android 操作系统与应用程序之间的交互作用更有效率,这能够让应用程序和整个系统运行得更快.用 Zipalign 处理过的应用程序执行时间达到最低限度,当设备运行 APK 应用程序时占更少的 RAM.
Zipalign 如何进行优化的呢?
调用 buildtoolszipalign,对签名后的 APK 文件进行对齐处理,使 APK 中所有资源文件距离文件起始偏移为 4 字节的整数倍,从而在通过内存映射访问 APK 文件时会更快.同时也减少了在设备上运行时的内存消耗.如果对于为何提速不理解,那么可以看下 内存对齐的规则以及作用 该篇文章,对于内存对齐的好处有比较生动详细的解释.最终这样我们的 APK 就生成完毕了.
典型的 APK 中内容
AndroidManifest.xml 程序全局配置文件
classes.dex Dalvik 字节码
resources.arsc 资源索引表
META-INF 该目录下存放的是签名信息
res 该目录存放资源文件
assets 该目录可以存放一些配置或资源文件
总结
至此,对于 Andoid 项目构建过程的分析已经完成,当然,并没与深入到源码层级的分析,本文的旨在对于构建过程流程上的了解和其中一些优化的原因所在,为后续通过 Gradle 插件 hook 构建过程来做一定的操作,做一个铺垫.
参考文章
Android APK 签名原理及方法 改善 android 性能工具篇[zipalign] Android 应用程序资源的编译和打包过程分析 Android 资源管理框架(AssetManager)简要介绍和学习计划 Android 代码混淆之 ProGuard
来源: https://juejin.im/post/5a69c0ccf265da3e2a0dc9aa