Android apk打包流程
APK 是什么
一种定义 APK 的方法是将它们称为只能在 Android 操作系统中打开的应用程序的打包版本。更准确地说,APK 是一种用于安装和分发移动应用程序的文件格式。
APK 文件的内容
APK 文件由什么组成?我们都知道 APK 是移动应用程序的压缩和打包形式。它们包含许多代码,这些代码组合在一起,形成 Android 设备上的移动应用程序。
以下 apk 文件改成 zip 文件解压后的样子,涉及典型 APK 文件中的主要文件。
- META-INF 目录 -- 元信息目录包含一系列资源,例如清单文件、应用证书等。
- LIB -- 文件夹里面存放的是 so 动态链接库,so 动态链接库是不需要做处理做 apk 打包一些列压缩处理的。
- Resources.arsc -- 包含开发人员已编译的所有资源的文件。包括每一个资源名称、类型、值、ID 以及所配置的维度信息。我们可以将这个 resources.arsc 文件想象成是一个资源索引表,这个资源索引表在给定资源 ID 和设备配置信息的情况下,能够在应用程序的资源目录中快速地找到最匹配的资源。这是为了确保这些资源在应用上线时随时可用。例如,二进制 XML 文件作为预编译资源保存在此文件中。
- Res -- 资源文件夹,包含尚未编译的资源的文件,这些资源仍保留在 resources.arsc 之外。里面还会分 animator,anim,color,drawable,layout,menu 和 raw 这几个文件夹。
- Assets -- 应用需要许多资源来执行某些关键任务。Assets 文件包含使应用能够执行某些关键任务的代码。
- Classes.dex -- 此 DEX 文件包含 Android Runtime 执行的最终代码。APK 文件中的每个小代码或资产都会转换为位并在 classes.dex 文件中执行。Android 虚拟机 Dalvik 支持的字节码文件格式 Google 在新发布的 Android 平台上使用了自己的 Dalvik 虚拟机来定义, 这种虚拟机执行的并非 Java 字节码, 而是另一种字节码: dex 格式的字节码。在编译 Java 代码之后,通过 Android 平台上的工具可以将 Java 字节码转换成 Dex 字节码。虽然 Google 称 Dalvik 是为了移动设备定做的,但是业界很多人认为这是为了规避向 sun 申请 Javalicense。这个 DalvikVM 针对手机程式/CPU 做过最佳化,可以同时执行许多 VM 而不会占用太多 Resource。classes.dex 也是由 java 的 class 文件重新编排而来,我们也可以通过反编译工具把 dex 文件转换成 class 文件。如果做了拆包那么会有 classes1.dex,classes2.dex ...多个 classes.dex 文件。
- AndroidManifest.xml -- 此清单文件提供应用的名称、版本号、权限信息等信息。AndroidManifest.xml 是每个 android 程序中必须的文件。它位于整个项目的根目录,描述了 package 中暴露的组件(activities, services, 等等),他们各自的实现类,各种能被处理的数据和启动位置。 除了能声明程序中的 Activities, ContentProviders, Services, 和 Intent Receivers,还能指定 permissions 和 instrumentation(安全控制和测试)。这个文件是很重要的,里面有我们的 Android 四大组件和申请的权限。
APK 构建需求
项目源文件
Android 通用项目结构如下
c
└── MyApp/ # Project
├── gradle/
│ └── wrapper/
│ └── gradle-wrapper.properties
├── build.gradle(.kts)
├── settings.gradle(.kts)
└── app/ # Module
├── build.gradle(.kts)
└── build/
├── libs/
└── src/
└── main/ # Source set
├── java/
│ └── com.example.myapp
├── res/
│ ├── drawable/
│ ├── values/
│ └── ...
└── AndroidManifest.xml
构建工具
名称 | 功能介绍 | 在操作系统中的路径 |
---|---|---|
aapt | Android 资源打包工具 | ${ANDROID_SDK_HOME}/platform-tools/appt |
aidl | Android 接口描述语言转化为.java 文件的工具 | ${ANDROID_SDK_HOME}/platform-tools/aidl |
javac | Java Compiler | ${JDK_HOME}/javac 或/usr/bin/javac |
dex | 转化.class 文件为 Davik VM 能识别的.dex 文件 | ${ANDROID_SDK_HOME}/platform-tools/dx |
apkbuilder | 生成 apk 包 | ${ANDROID_SDK_HOME}/tools/opkbuilder |
jarsigner | .jar 文件的签名工具 | ${JDK_HOME}/jarsigner 或/usr/bin/jarsigner |
zipalign | 字节码对齐工具 | ${ANDROID_SDK_HOME}/tools/zipalign |
自动化构建
为什么需要自动化构建工具
多数情况,用户在命令行执行一个脚本,脚本定义了任务执行的顺序,比如:编译源代码、从 A 路径复制文件到 B 路径、装配交付,这种自动化构建过程一天可能执行数次。
项目自动化的优势
- 避免手工介入
- 创建可重复的构建过程
- 使得构建非常便捷
自动化构建工具 gradle
gradle 就是一套工具,能把你自动化构建的需求表示成可执行的顺序的任务(tasks),比如编译源代码,拷贝生成的 class 文件,组装交付。每一个任务都是一个工作单元,任务的顺序很重要,我们把任务和相互之间的依赖建模成一种有向无环图,比如下面这个:
有向无环图
包含两个部分:
-
节点(node):一个工作单元,在这里就是一个任务,比如编译源代码
-
边(edge): 一个有方向的边,表示相邻节点之间的依赖关系,如果一个任务定义了依赖,这个依赖的任务要在这个任务之前执行。
构建工具的组成
- Build File. 包含构建需要的配置,定义了项目的依赖关系,比如第三方库的,以及以任务的形式存在的指令,定义了任务之间的先后顺序。
- Build inpus and outputs: 任务把输入经过一系列步骤后产生输出。
- 依赖管理。
构建流程总览
典型 Android 应用模块的构建流程通常依循下列步骤:
-
编译器将您的源代码转换成 DEX(Dalvik Executable) 文件(其中包括 Android 设备上运行的字节码),将所有其他内容转换成已编译资源。
-
APK 打包器将 DEX 文件和已编译资源合并成单个 APK。 不过,必须先签署 APK,才能将应用安装并部署到 Android 设备上。
-
APK 打包器使用调试或发布密钥库签署您的 APK:
- 如果您构建的是调试版本的应用(即专用于测试和分析的应用),打包器会使用调试密钥库签署您的应用。 Android Studio 自动使用调试密钥库配置新项目。
- 如果您构建的是打算向外发布的发布版本应用,打包器会使用发布密钥库签署您的应用。 要创建发布密钥库,请阅读在 Android Studio 中签署您的应用。
-
在生成最终 APK 之前,打包器会使用 zipalign 工具对应用进行优化,减少其在设备上运行时占用的内存。
具体构建流程
c
//aidl 转换aidl文件为java文件
> Task :app:compileDebugAidl
//生成BuildConfig文件
> Task :app:generateDebugBuildConfig
//获取gradle中配置的资源文件
> Task :app:generateDebugResValues
// merge资源文件
> Task :app:mergeDebugResources
// merge assets文件
> Task :app:mergeDebugAssets
> Task :app:compressDebugAssets
// merge所有的manifest文件
> Task :app:processDebugManifest
//AAPT 生成R文件
> Task :app:processDebugResources
//编译kotlin文件
> Task :app:compileDebugKotlin
//javac 编译java文件
> Task :app:compileDebugJavaWithJavac
//转换class文件为dex文件
> Task :app:dexBuilderDebug
//打包成apk并签名
> Task :app:packageDebug
1. 处理 aidl files
如果有 aidl 文件,会通过 aidl 工具(源碼位于 system/tools/aidl)打包成 java 接口类
AIDL(Android Interface Definition Language),是 Android 接口定义语言。目的是为了方便实现进程间通信,尤其是在涉及多进程并发情况下的进程间通信。它的本质是对 Binder 通信的封装.
2. 打包资源文件
资源文件(res 文件夹下的文件)通过 AAPT(Android Asset Packaging Tool)打包生成 R.java
类(资源索引表)以及 .arsc
资源文件。
2.1 生成 BuildConfig 文件
在项目中配置了 buildConfigField
等信息,会在 BuildConfig class 类里以静态属性的方式展示:
2.2 合并 Resources、assets、manifest、so 等资源文件
我们的项目中会依赖不同的库、组件,也会有多渠道的需求,所以 merge
这一步操作就是将不同地方的资源文件进行整合。 多个 manifest 文件也需要整理成一个完整的文件,所以如果有属性冲突这一步就会报错。资源文件也会整理分类到不同的分辨率目录中。
AAPT 工具负责编译项目中的这些资源文件,所有资源文件会被编译处理,XML 文件(drawable 图片除外)会被编译成二进制文件,所以解压 apk 之后无法直接打开 XML 文件。但是 assets 和 raw 目录下的资源并不会被编译,会被原封不动的打包到 apk 压缩包中。 资源文件编译之后的产物包括两部分:resources.arsc 文件和一个 R.java。前者保存的是一个资源索引表,后者定义了各个资源 ID 常量。这两者结合就可以在代码中找到对应的资源引用。
aapt 生成的 .arsc
资源文件对应我们将 apk 解压(apk 本质是一个 zip 压缩包)得到的 Resources.arsc,它实际上就是 App 的资源索引表。简单来说,通过 R.java 文件与 Resources.arsc 就可以定位到资源的内存地址。
aapt 编译源码的入口在 frameworks/base/tools/aapt/Main.cpp
,其中对 assert 文件夹路径、res 文件夹路径、AndroidManifest 文件等会采取不同的策略
对 asset 目录下的资源不进行编译,assets 目录下的资源会被原封不动的打入 apk 中,也就是说 assets 不会被压缩;aapt 会对 res 下 drawable 资源进行压缩处理(raw 目录下除外)
aapt 将文本 xml 资源文件编译成二进制资源文件的方法 buildResources 函数在 frameworks/base/tools/aapt/Resource.cpp
3. 编译(Compilers)
R.java+ 工程源码 +aidl.java 通过 javac 生成.class 文件。
Javac 编译过程大致可以分为 3 个阶段
-
解析与填充符号表过程 解析的步骤包括词法分析与语法分析两个过程
词法分析
是将源代码的字符串流转变为标记(Token)集合,单个字符是程序编写过程的最小元素,而标记是编译过程的最小元素。关键字、变量名、运算符等都可以成为标记 语法分析
是根据 Token 序列构造抽象语法树的过程,抽象语法树是一种用来描述程序代码语法结构的树形表示方式,语法树的每一个节点都代表程序中的一个语法结构,如包、类型、修饰符、接口等 -
插入式注解处理器的注解处理过程 插入式注解处理器,可以读取、修改、添加抽象语法树中的任意元素。Android 中的 APT(Annotation Processing Tool)就是在这个阶段工作的。
-
语义分析与字节码生成过程 语法分析后,编译器获得了程序代码的抽象语法树表示,语法树能表示一个结构正确的源程序抽象,但是无法保证源程序是符合逻辑的。而语义分析主要是对结构正确的进行上下文有关性质的审查。语义分析一般要经历标注检查、数据及控制流分析、解语法糖等过程,然后才会走到 javac 编译的最后一个阶段:字节码生成。大致流程如下:
标注检查 -> 数据及控制流分析 -> 解语法糖 -> 字节码生成
Javac 编译动作的入口是 com.sun.tools.javac.main.JavaCompiler
类,主要逻辑集中在 compile()和 compile2()方法中,感兴趣的可以去看看
4. dex(生成 dex 文件)
源码.class 文件和第三方 jar 或者 library 通过 dx 工具打包成 dex 文件。
上面生成的 .class 文件虽然已经可以在 JVM 环境中运行,但是如果要在 Android 运行时环境中执行还需要特殊的处理,那就是 dx 处理,它会对 .class 文件进行翻译、重构、解释、压缩等操作。。
AndroidStudio 有提供 proguard、D8、R8 等工具来处理这一流程。Android 还会针对 Dalvik 虚拟机和 Art 虚拟机对 dex 进行优化
-
dexopt 是对 dex 文件 进行 verification 和 optimization 的操作,其对 dex 文件的优化结果变成了 odex 文件,这个文件和 dex 文件很像,只是使用了一些优化操作码(譬如优化调用虚拟指令等)。
-
dex2oat 是对 dex 文件的 AOT 提前编译操作,其需要一个 dex 文件,然后对其进行编译,结果是一个本地可执行的 ELF 文件,可以直接被本地处理器执行。
5. apkbuilder/zipflinger(生成未签名 apk)
这一步就是生成 APK 文件,将 manifest文件、resources文件、dex文件、assets文件
等等打包成一个压缩包,也就是 apk 文件。 在老版本使用的工具是 apkbuilder
,新版本用的是 zipflinger
。 而在 AGP3.6.0
之后,使用 zipflinger
作为默认打包工具来构建 APK
,以提高构建速度。
6. zipalign(对齐)
所谓对齐,主要过程是将 APK 包中所有的资源文件距离文件起始偏移为 4 字节整数倍,这样通过内存映射访问 apk 文件时的速度会更快。对齐的作用主要是为了减少运行时内存的使用。
使用工具 zipalign 对 apk 中的未压缩资源(图片、视频等)进行对齐操作,让资源按照 4 字节的边界进行对齐。这种思想同 Java 对象内存布局中的对齐空间非常类似,主要是为了加快资源的访问速度。如果每个资源的开始位置都是上一个资源之后的 4n 字节,那么访问下一个资源就不用遍历,直接跳到 4n 字节处判断是不是一个新的资源即可。
7. apk(签名)
jarsigner 工具会对未签名的 apk 验证签名。得到一个签名后的 apk(signed.apk)
没有签名的 apk 无法安装,也无法发布到应用市场。
大家比较熟知的签名工具是 JDK
提供的 jarsigner
,而 apksigner
是 Google
专门为 Android
提供的签名和签证工具。
其区别就在于 jarsigner
只能进行 v1
签名,而 apksigner
可以进行 v2
、v3
、v4
签名。
v1签名
v1 签名方式主要是利用 META-INFO
文件夹中的三个文件。
首先,将 apk 中除了 META-INFO 文件夹中的所有文件进行进行摘要写到 META-INFO/MANIFEST.MF;然后计算 MANIFEST.MF 文件的摘要写到 CERT.SF;最后计算 CERT.SF 的摘要,使用私钥计算签名,将签名和开发者证书写到 CERT.RSA。
所以 META-INFO 文件夹中这三个文件就能保证 apk 不会被修改。
v2签名
Android7.0
之后,推出了 v2 签名,为了解决 v1 签名速度慢以及签名不完整的问题。
apk 本质上是一个压缩包,而压缩包文件格式一般分为三块:
文件数据区,中央目录结果,中央目录结束节。
而 v2 要做的就是,在文件中插入一个 APK 签名分块,位于中央目录部分之前,如下图:
这样处理之后,文件就完成无法修改了,这也是为什么 zipalign(对齐处理)
要在签名之前完成。
v3签名
Android 9
推出了 v3 签名方案,和 v2 签名方式基本相同,不同的是在 v3 签名分块中添加了有关受支持的 sdk 版本和新旧签名信息,可以用作签名替换升级。
v4签名
Android 11
推出了 v4 签名方案。