Android 10 源码分析:APK 是如何生成的
- 如果评论区没有及时回复,欢迎来公众号:ByteCode 咨询
- 公众号:ByteCode。致力于分享最新技术原创文章,涉及 Kotlin、Jetpack、算法、译文、系统源码相关的文章
前言
- 这是 Android 10 源码分析系列的第 1 篇
- 分支:android-10.0.0_r14
- 全文阅读大概 5 分钟
APK 的文件可以分为 代码 和 资源 两部分,接下来源码分析系列,会完全围绕着,这两部分内容来分析,而今天这篇文章是 Android 10 源码分析系列的第 1 篇。
本文预计会分为两篇文章来分析 APK 是如何生成的:
- 从原理的角度分析 APK 是如何生成的
- 如果不使用 AndroidStudio 如何生成 APK
我们很多时候都是直接点击 Android Studio 中直接点击 Run ‘app’
,就可以在 build/outputs/apk
目录下生成 APK 文件,那么 Android Studio 是如何做到的呢?
接下来我们一起来分析一下 APK 的构建过程,APK 的文件可以分为 代码 和 资源 两部分,那么构建 APK 的过程中,也会对 代码 和 资源 做分别的处理。
我们先来看看 Google提供的流程图 大概了解一下 APK 的构建过程
新版构建流程图
APK 打包的内容主要有:
- 应用模块用到的源代码、资源文件、aidl 接口文件等等
- 依赖模块即源代码即第三方依赖库如:aar、jar、so 文件等等
新版构建流程图只是描述了大概的过程,为了能够清楚的了解 APK 是如何生成的, 在来看一下老版构建流程图。
老版构建流程图
我们先来了解一下图中所示各个工具的作用。
名字 | 功能 |
---|---|
AAPT/APT2 | Android 资源打包工具 |
AIDL | 将所有的 AIDL 接口转化为 Java 接口 |
Javac(Java Compiler) | 将所有的 Java 代码编译成 Class文件 |
Dex | 将 Class 文件编译成 Dex 文件 |
Apkbuilder | 将处理后的资源和代码打包生成 APK 文件 |
Jarsigner/Apksigner | 对未签名的 APK 文件进行签名 |
Zipalign | 优化签名后的 APK,减少运行时所占用的内存 |
构建过程
Apk 的构建过程大概分为如下几步:
- 使用 AAPT 工具生成 R.java 文件
- 所有的 AIDL 接口转化为 Java 接口
- 将 Java 代码编译成 Class 文件
- 将 Class 文件编译成 Dex 文件
- 打包生成 APK 文件
- 对 APK 文件签名
- 优化 APK 文件
1. 使用 AAPT 工具生成 R.java 文件
AAPT(Android Asset Packaging Tool)android 资源打包工具,将资源文件(包括AndroidManifest.xml、布局文件、各种 xml 资源等)打包生成 R.java 文件,将 AndroidManifest.xml 生成二进制的 AndroidManifest.java 文件
aapt p -M AndroidManifest.xml -S output/res/ -I android.jar -J ./ -F input/out.apk |
但是从 Android Studio 3.0 开始,google 默认开启了 AAPT2 作为资源编译的编译器,AAPT2 的出现为资源的增量编译提供了支持,aapt2 主要分两步,compile 和 link
compile
aapt2 compile -o res.apk --dir output/res/ |
link
aapt2 link -o input/out.apk -I tools/android.jar --manifest output/AndroidManifest.xml -A res.apk --java ./ |
2. 所有的 AIDL 接口转化为 Java 接口
使用 AIDL(Android Interface Denifition Language),位于 sdk\build-tools 目录下的 aidl 工具,将源码文件、aidl 文件、framework.aidl 等所有的 AIDL 文件,生成相应的 Java 文件,命令如下:
aidl -Iaidl -pAndroid/Sdk/platforms/android-29/framework.aidl -obuild aidl/com/android/vending/billing/IInAppBillingService.aidl |
3. 将 Java 代码编译成 Class 文件
使用 Javac(Java Compiler)把项目中所有的 Java 代码编译成 class 文件, 包括 Java 源文件、AAPT 生成的 R.java 文件 以及 aidl 生成的 Java 接口文件,命令如下:
javac -target 1.8 -bootclasspath platforms/android-28/android.jar -d ./java/com/testjni/*.java |
4. 将 Class 文件编译成 Dex 文件
使用 DX 工具将所有的 Class 文件(包括第三方库中的 class 文件)转换成 Dex 文件(Dalvik 可执行文件,其中包括在 Android 设备上运行的字节码),该过程主要完成 Java 字节码转换成 Dalvik 字节码, 命令如下:
java -jar dx.jar --dex --ouput=classes.dex ./java/com/testjni/*.class |
5. 打包生成 APK 文件
使用 Apkbuilder(主要用到的是 sdk/tools/lib/sdklib.jar 文件中的 ApkBuilderMain 类)将所有的 Dex 文件、Resource.arsc、Res 文件夹、Assets 文件夹、AndroidManifest.xml 打包生成 APK 文件(未签名)
6. 对 APK 文件签名
使用 Apksigner(Android官方针对 APK 签名及验证工具)或 Jarsigner(JDK提供针对 jar 包签名工具)对未签名的 APK 文件进行签名
ps:如果使用 Apksigner 签名需要(7. 优化 APK 文件)放到(6. 对 APK 文件签名)签名前面,为什么?请查看关于 Apksigner 和 Jarsigner 的区别,请移步到文末
7. 优化 APK 文件
使用 zipalign 对签名后的 APK 文件进行对齐处理,对齐的主要过程是将 APK 包中所有的资源文件距离文件起始偏移为 4 字节整数倍,这样通过内存映射访问 APK 文件时的速度会更快,减少其在设备上运行时所占用的内存
总结
上述打包过程都是 AndroidStudio 编译时,调用各种编译命令自动完成的, 总结一下上述打包过程:
- 除了 assets 和 res/raw 资源被原装不动地打包进 APK 之外,其它的资源都会被编译或者处理
- 除了 assets 资源之外,其它的资源都会被赋予一个资源 ID
- 打包工具负责编译和打包资源,编译完成之后,会生成一个 resources.arsc 文件和一个 R.java,前者保存的是一个资源索引表,后者定义了各个资源 ID 常量
- 应用程序配置文件 AndroidManifest.xml 同样会被编译成二进制的 xml 文件,然后再打包到 APK 里面去
- 应用程序在运行时通过 AssetManager 来访问资源,或通过资源 ID 来访问,或通过文件名来访问
APK 文件大概可以分为两个部分:代码和资源, 代码部分通过 Javac 将 Java 代码编译成 Class 文件, 然后通过 DX 工具将 Class 文件编译成 Dex 文件,接下来我们主要来分析一下资源的编译和打包
资源的编译和打包
在分析资源的编译和打包之前,我们需要了解一下 Android 都有哪些资源,其实 Android 资源大概分为两个部分:assets 和 res
当我们使用 AAPT 对资源进行编译的时候,会采用两种模式 Deflate(压缩模式)/Stored(存储模式),而具体使用模式,取决于文件后缀类型,AAPT 会对以下文件后缀类型的资源采用存储模式(即不会被压缩)
/* these formats are already compressed, or don't compress well */ |
通过 aapt l -v xxx.apk
或 unzip -l xxx.apk
来查看 APK 内文件使用的什么模式
1. assets 资源
assets 资源放在 assets 目录下,它里面保存一些原始的文件,可以以任何方式来进行组织,AAPT 会对指定文件后缀类型的资源进行压缩,其余的文件最终会原封不动的被打包进 APK 文件中,通过 AssetManager 来获取 asset 资源,代码如下
AssetManager assetManager = context.getAssets(); |
2. res 资源
res 资源放在主工程的 res 目录下,这类资源一般都会在编译阶段生成一个资源ID供我们使用,res 目录包括 animator、anim、 color、drawable、layout、menu、raw、values、xml 等
上述资源文件除了 raw 类型资源,以及 drawable 文件夹下的 Bitmap 资源之外,其它的资源文件均会被编译成二进制格式的 XML 文件,生成的二进制格式的 XML 文件分别有一个字符串资源池,用来保存文件中引用到的每一个字符串
这样原来在文本格式的 XML 文件中的每一个放置字符串的地方在二进制格式的XML文件中都被替换成一个索引到字符串资源池的整数值,将整数值保存在 R.java 类中,R.java 会和其他源文件一起编译到 APK 中去
将资源编译成二进制文件,都是由 AAPT 工具来完成的,资源打包主要有以下几个流程:
- 解析 AndroidManifest.xml,获得应用程序的包名称,创建资源表
- 添加被引用资源包,被添加的资源会以一种资源 ID 的方式定义在 R.java 中
- 资源打包工具创建一个 AaptAssets 对象,收集当前需要编译的资源文件,收集到的资源保存在 AaptAssets 对象对象中
- 将上一步 AaptAssets 对象保存的资源,添加到资源表 ResourceTable 中去,用于最终生成资源描述文件 resources.arsc
- 编译 values 类资源,这类资源包括数组、颜色、尺寸、字符串等值
- 给 style、array 这类资源分配资源 ID
- 编译 XML 资源文件,编译的流程分为:① 解析 XML 文件 ② 赋予属性名称资源 ID ③ 解析属性值 ④ 将 XML 文件从文本格式转换为二进制格式
- 生成资源索引表 resources.arsc
2.1 资源 ID
AAP 工具会所有的资源都会生成一个 R.java 文件,并且每个资源都对应 R.java 中的十六进制整数变量,其实这些十六进制的整数是由三部分组成:PackageId + TypeId + ItemValue,代码所示:
public final class R { |
最高字节是 Package ID 表示命名空间,标明资源的来源,Android 系统自己定义了两个 Package ID,系统资源命名空间:0x01 和 应用资源命名空间:0x7f
正因为应用资源命名空间:0x7f,我们在做插件化的时候就会出现一个问题,宿主和插件包,合并资源后资源 ID 冲突。通过上面分析要解决这个问题,就要为不同的插件设置不同的 PackageId,而宿主可以保留原来 0x7f 不变,这样就永远不会有冲突发生了
如何解决资源冲突
- 制定一个不用冲突的命名规范
- library Module 的 build.gradle 中设置资源前缀(推荐)
android {
resourcePrefix "<前缀>"
}
2.2 资源索引(resources.arsc)
最终生成的是资源索引表 resources.arsc ,resources.arsc 是一个编译后的二进制文件, 在 AndroidStudio 打开 resources.arsc 文件,如下所示
Android 正是利用这个索引表根据资源 ID 进行资源的查找,为不同语言、不同地区、不同设备提供相对应的最佳资源。查找和通过 Resources 和 AssetManger 来完成的
在文中提到了两个工具 Apksigner 和 Jarsigner,下面一起来了解一下 Apksigner 和 Jarsigner 的区别
Apksigner 和 Jarsigner 的区别
在 Android Studio 中点击菜单 Build->Generate signed apk… 打包签名过程中,可以看到两种签名选项 V1(Jar Signature) 和 V2(Full APK Signature)
- Jarsigner 是 JDK 提供的针对 JAR 包签名的通用工具
- Apksigner 是 Google 官方提供的针对 Android APK 签名及验证的专用工具
在 Android 11 以上使用 V4 签名,Android 9.0 以上使用 V3 签名,Android 7.0 开始使用 V2 签名,但在 Android 7.0 以下版本, 只能用旧签名方案 V1 签名
V1 签名:
Android 7 以下使用 V1 签名,V1 签名会对 ZIP 压缩包的每个文件进行验证, 签名后还能对压缩包修改(移动/重新压缩文件),对 V1 签名的 APK/JAR 解压,在 META-INF 存放签名文件(MANIFEST.MF, CERT.SF, CERT.RSA), 其中 MANIFEST.MF 文件保存所有文件的 SHA1 指纹(除了 META-INF 文件), 由此可知: V1 签名是对压缩包中单个文件签名验证
V2 签名:
Android 7 开始增加了 V2 签名,V2 签名会对 ZIP 压缩包的整个文件验证, 签名后不能修改压缩包(包括 zipalign), 对 V2 签名的 APK 解压, 没有发现签名文件, 重新压缩后 V2 签名就失效, 由此可知: V2 签名是对整个 APK 签名验证
V3 签名:
Android 9 增加了 V3 签名,V3 签名在 V2 的基础上,仍然采用检查整个压缩包的校验方式,支持 APK 密钥轮替,这使应用能够在 APK 更新过程中更改其签名密钥
v3 签名新增的新块(attr) 会记录我们之前的签名信息以及新的签名信息,支持 APK 密钥轮替方案,来做签名的替换和升级。这意味着,只要旧签名证书在手,我们就可以通过它在新的 APK 文件中,更改签名。
需要注意的是:对于覆盖安装的情况,签名校验只支持升级,而不支持降级
V4 签名:
在 Android 11 之前,建议不要使用 APK 密钥轮替,在 Android 11 之后增加了 V4 签名,V4 签名将签名存储在单独的
关于签名更多内容,会在后续文章内介绍。
总结:
- V1 签名是对压缩包中单个文件签名验证
- V2 签名是对整个 APK 签名验证
- zipalign 可以在 V1 签名后执行
- zipalign 不能在 V2 签名后执行,只能在 V2 签名之前执行
- V2 签名更安全(不能修改压缩包)
- V2 签名验证时间更短(不需要解压验证), 因而安装速度加快
- apksigner 工具默认同时使用 V1 和 V2 签名, 以兼容 Android 7.0 以下版本
- Android 7 以下使用 V1 签名
- Android 7 开始增加了 V2 签名
- Android 9 增加了 V3 签名
- Android 11 之后增加了 V4 签名
- V3 签名 和 V4 签名 目前只能在 Google Play 上使用
参考文献
- 本文作者:hi-dhl
- 本文标题:Android 10 源码分析:APK 是如何生成的
- 本文链接:https://hi-dhl.com/2020/06/07/android10/package-manager/01-APK-generate/
- 版权声明:本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 hi-dhl