APK 的前世今生:从 Android 源码到 apk 的编译打
最近想要研究热修复的原理,并自己实现一套简单的热修复框架(音视频的坑刚挖好就又挖另一个坑了2333),已经在看书入门 c++,方便后续查看业内一些知名热修复框架的源码,另外在涉及编译期/运行时修改代码之前,先了解一下 apk 是如何从项目源码被打包生成的,可能对后续的从思路上或实际操作都会所裨益。
文中涉及到的工具所在目录:Android/sdk/build-tools
。下面开始分解并逐步实现对源码的打包。
编译流程
1. 生成仅包含资源文件的 apk 包和 R.java 文件
根据资源文件和 AndroidManifest.xml 由工具 AAPT 生成 R.java 文件。Android Gradle Plugin 3.0.0 以后默认使用 AAPT2,详见 AAPT2 官方文档。来看一下 AAPT2 的使用:
预编译
编译所有 Android 支持的资源文件。可以通过编译语句将单个资源文件编译成 .flat
后缀的过渡二进制文件
AAPT 可以编译单个文件,例如编译 strings.xml,会生成 values_strings.arsc.flat 文件:
aapt2 compile app/src/main/res/values/strings.xml -o test/
但一个项目不可能只有一个资源文件,通常都是编译 整个 res 资源目录,会生成 zip 压缩包,包含了所有资源文件编译后的 flat 格式文件:
aapt2 compile --dir app/src/main/res/ -o package/res.zip
参数含义:
- --dir:指定输入目录
- -o: 指定输出目录(如果输入源是文件夹,则需要指定编译后的(zip)文件名)
链接(link)
将预编译生成的过渡二进制文件合并并打包成单独的 APK 包,R 文件和 ProGuard 规则文件也是在这个时期生成的,生成的 APK 包不包含 DEX 字节码并且是未签名的(后续可使用 D8 编译工具将 Java 字节码编译成 DEX 字节码,使用 apksigner 对 APK 签名)
aapt2 link package/res.zip \
-I ~/Library/Android/sdk/platforms/android-27/android.jar \
--java package/ \
--manifest app/src/main/AndroidManifest.xml \
-o package/res.apk
参数含义:
- -I:必要参数,指定 android.jar 目录,因为 xml 中可能使用到了例如 android:id 等自带的 android 命名空间
- o:指定输出 apk 路径
- --java:指定生成的 R 文件的路径
- --manifest:必要参数,Manifest 文件中包含了 app 的包名和 application id
执行上述命令后报错:style/Theme.AppCompat.Light.DarkActionBar not found.
以及 layout_constraintBottom_toBottomOf (新建的工程默认依赖了 constraint-layout库)等各种 not found。
报这些错是因为 link 时没有引入第三方库,在 Google 文档里没有找到相关的命令,所以先移除这些依赖,跑通整个流程后再回头看怎么解决。
// AppTheme 暂时先移除对 Theme.Appcompat 的依赖
<style name="AppTheme" parent="android:Theme.Holo.Light.DarkActionBar">
</style>
再执行一遍命令,可以看到指定的输出目录已经生成了 apk 包和 R 文件:
仅包含资源文件的apk与R文件2. 处理aidl,生成对应的java文件
因为 demo 没有涉及到 aidl,暂且跳过。
3. 编译 .java 文件为 .class 文件
编译项目 src 目录下所有 .java 文件还有之前生成的 R.java 、aidl 生成的 java 文件为相应的的 class 文件
javac -encoding utf-8 \
-target 1.8 \
-bootclasspath ~/Library/Android/sdk/platforms/android-27/android.jar \
app/src/main/java/com/yazhidev/demo/*.java package/com/yazhidev/demo/R.java \
-d package/
参数含义:
- -encoding: 指定编码方式为 uts-8
- -target:指定 Java 版本号
- -bootclasspath:引入 Android.jar 包内的类
- -d:指定编译生成的字节码文件存放的路径
4. class 文件编译为 dex 文件
dex 文件是 Android 虚拟机所能识别、解析并运行的文件。Java 源文件被编译为 class 文件后,需要通过 dex 编译器将多个 class 文件整合为一个 dex 文件,从 Android Studio 3.1 开始,已经使用 D8 替代原先的 DX 作为默认的 dex 编译器。D8 的使用很简单:
d8 package/com/yazhidev/demo/*.class \
--classpath ~/Library/Android/sdk/platforms/android-27/android.jar \
--output ./
参数含义:
- --classpath:指定编译需要引用到的类
- --output:指定编译后生成的 .dex 文件的存放路径
5. 将 dex 文件添加进 apk 包
原本这步是通过 apkbuilder 脚本来做的,现在改成用 aapt 命令来做。
aapt add package/res.apk classes.dex
需要注意的是 dex 文件前不能加路径,否则会将路径带入 apk 包中。
6. 优化对齐 apk 文件
apksigner 文档中提到,如果使用 apksigner 对 apk 签名,则需要在签名之前使用 zipalign 优化对齐。
zipalign 的使用很简单:
zipalign 4 package/res.apk package/app-unsigned-aligned.apk
4 代表 32 位对齐,zipalign 可以确保所有未压缩的数据的开头均相对于文件开头部分执行特定的字节对齐,这样可减少应用消耗的 RAM 量。
7. 签名
签名需要私钥,可以通过 Android Studio 生成,也可通过 JDK bin 目录下的 keytool 工具生成。keytool 以及 apksigner 的使用可参考:从命令行构建和签署您的应用
我自己这么就直接使用现有的 key 签名,签名的命令很简单:
//apk 签名
apksigner sign --ks key.jks --out package/app-release.apk package/app-unsigned-aligned.apk
参数含义:
- --ks:指定私钥文件
- --out:指定签名后的 apk 输出路径
//检查 apk 的签名
apksigner verify app.apk
至此,就完成了 Android 源码打包成 apk 的整个流程,当然以上只是最简单的情况,对于第三方库、多 module 等情景下的打包流程都没有涉及。将 apk 安装到手机上,可以正常打开:
丑出天际的首页一打开首页我的内心的拒绝的,这首页可以说是丑出天际。但起码从 0 到 1 了(化身阿 Q 疯狂自我安慰),一番操作算是对 apk 的打包流程有了个笼统的认识,后面要了解一下 app 启动流程,为热修复的学习打基础。(也许后面会回来填坑?)