官方对APK构建过程的介绍
典型 Android 应用模块的构建流程,按照以下常规步骤执行:
- 编译器将您的源代码转换成 DEX 文件(Dalvik 可执行文件,其中包括在 Android 设备上运行的字节码),并将其他所有内容转换成编译后的资源。
- 打包器将 DEX 文件和编译后的资源组合成 APK 或 AAB(具体取决于所选的 build 目标)。 必须先为 APK 或 AAB 签名,然后才能将应用安装到 Android 设备或分发到 Google Play 等商店。
- 打包器使用调试或发布密钥库为 APK 或 AAB 签名:
- 如果您构建的是调试版应用(即专门用来测试和分析的应用),则打包器会使用调试密钥库为应用签名。Android Studio 会自动使用调试密钥库配置新项目。
- 如果您构建的是打算对外发布的发布版应用,则打包器会使用发布密钥库(您需要进行配置)为应用签名。
- 在 Android Studio 中为应用签名
- 在生成最终 APK 之前,打包器会使用
zipalign
工具对应用进行优化,以减少其在设备上运行时所占用的内存。构建流程结束时,您将获得应用的调试版或发布版 APK/AAB,以用于部署、测试或向外部用户发布。
精通APK构建
APK的一般生成步骤
- 打包资源文件 aapt/aapt2
- 生成R文件
- 生成编译后的资源文件
- aidl生成java文件 aidl
- Java代码生成class文件 javac
- class文件生成dex文件 dx/d8/r8
- 打包(未签名)apk apkbuilder
- 签名apk jarsigner/apksigner
- 对齐apk zipalign
注:官方对上述部分命令行工具的使用有介绍:[ 官方指导:命令行工具 ]
1. 打包资源文件 aapt/aapt2
- aapt/aapt2:ANDROID_SDK/build-tools/
见[ 官方指导:命令行工具-aapt2 ]
aapt package -f -m -J ./gen -S res -M AndroidManifest.xml -I D:\android.jar
-f 如果编译生成的文件已经存在,强制覆盖。
-m 使生成的包的目录存放在-J参数指定的目录
-J 指定生成的R.java 的输出目录路径
-S 指定res文件夹的路径
-I 指定某个版本平台的android.jar文件的路径
-A 指定assert文件夹的路径
aapt 入口为 frameworks/base/tools/aapt/Main.cpp ,其中对 assets文件夹路径、res文件夹路径、AndroidManifest文件等会采取不同的策略。
对assets目录下的资源不进行编译,会被原封不动的打入apk中,也就是说assets不会被压缩。
AndroidManifest.xml会被aapt编译成二进制。
res下的资源,大多会被编译成针对Android平台优化过的二进制文件。对drawable下的png默认会进行压缩处理(raw目录下除外)
资源文件(res/下的文件) ---AAPT---|---> R.java(资源索引表)
|---> resource.arsc 资源文件
|---> res文件(二进制&非二进制如res/raw和pic保持原样)
// 每个资源ID占4字节
public final class R {
public static final class anim {
public static int fade_in = 0x7f010001;
public static int fade_out = 0x7f010002;
}
}
// 第一位字节 0x7f 表示 packageID ,用来限定资源的来源。系统资源包是 0x01,SharedLibrary类型资源包是 0x00, 普通App包则是 0x7f;
// 次一位字节 01 表示 TypeID,用来表示资源类型,如 drawable、layouts、anims、color、menu 等;
// 后2字节 0001/0002 表示 EntryID,指的是每一个资源在对应的 TypeID 中出现的顺序
resources.arsc 是一个App的资源索引表,可以理解为一个map映射表,map的key是 R.java 中的资源ID,而 value 就是其对应的资源所在路径,通过 R.java 文件和 resources.arsc 就可以在代码中找到对应的资源引用。
res/raw和assets的相同点:
- 两者目录下的文件在打包后会原封不动的保存在apk包中,不会被编译成二进制。
res/raw和assets的不同点:
- res/raw中的文件会被映射到R.java文件中,访问的时候直接使用资源ID即R.id.filename;assets文件夹下的文件不会被映射到R.java中,访问的时候需要AssetManager类,通过文件名访问。
- res/raw不可以有目录结构,而assets则可以有目录结构,也就是assets目录下可以再建立文件夹
2. aidl生成java文件 aidl
- aidl:ANDROID_SDK/build-tools/
3. Java代码生成class文件 javac
- javac:jdk/
javac -target 1.7 -bootclasspath D:\android-sdk-windows\platforms\android-8\android.jar -d bin src\demo\project\*.java gen\demo\project\R.java
-target <版本> 生成特定 VM 版本的类文件
-bootclasspath <路径> 覆盖引导类文件的位置
-d <目录> 指定存放生成的类文件的位置
-sourcepath <路径> 指定查找输入源文件的位置
Javac 入口为 com.sun.tools.javac.main.JavaCompiler 类,主要逻辑集中在 compile() 和 compile2() 方法中
4. class文件生成dex文件 dx/d8/r8
- dx:ANDROID_SDK/build-tools/
- 最老、仅支持编译dex(dexing)
- d8:ANDROID_SDK/build-tools/(高版本sdk才提供)
- 较新,用于替代dx,支持编译dex(dexing)与脱糖(desugaring)
- r8:/Applications/Android Studio.app/Contents/plugins/android/lib/r8.jar
- 最新,相当于
ProGuard + d8
,支持缩减(shrinking)、编译dex(dexing)与脱糖(desugaring) - sdk未提供r8.jar,网上也没有下载,需自行下载源码编译jar包,但有点麻烦,可在Android Studio/plugin/目录中找到
- 最新,相当于
注:关于dex生成的发展史,可以参考我的另一篇文章:[通过命令行进行R8混淆]
dx用法
dx --dex --output=D:\ProjectDemo\bin\classes.dex D:\ProjectDemo\bin
--output=<要生成的classes.dex路径> <要处理的class文件的路径>
d8用法
见[ 官方指导:命令行工具-d8 ]
# debug mode
$ java -jar build/libs/d8.jar --output out input.jar
# release mode
$ java -jar build/libs/d8.jar --release --output out input.jar
# example: java -jar build/libs/d8.jar --release --output . --pg-conf ./proguard-project.txt ./input.jar
r8用法
$ java -jar build/libs/r8.jar --release --output out --pg-conf proguard.cfg input.jar
ANDROID_SDK中提供的有些jar包由于没有指定入口类main方法,不能直接通过 -jar 执行,可通过 -cp 显式的指定入口,举例:
java -cp r8.jar com.android.tools.r8.R8 --help
5. 打包(未签名)apk apkbuilder
- apkbuilder:
- 较老SDK:ANDROID_SDK/tools/
- 较新SDK:ANDROID_SDK/tools/lib/sdklib.jar
apkbuilder只是一个脚本,实际上调用的是 ANDROID_SDK/tools/ 下的jar包中的 com.android.build.ApkBuilderMain 类。
这个jar包在低版本SDK中是哪个我没注意看过。而高版本SDK中已经不提供 apkbuilder 了,但 ANDROID_SDK/tools/lib/sdklib.jar 中有ApkBuilderMain类,可以使用(未指定Main方法入口,调用时需显式的指定:java -cp sdklib.jar com.android.build.ApkBuilderMain --help)。
apkbuilder D:\ProjectDemo\bin\projectdemo.apk -v -u -z D:\ProjectDemo\bin\resources.ap_ -f D:\ProjectDemo\bin\classes.dex -rf D:\ProjectDemo\src
-v Verbose 显示过程信息
-u 创建一个无签名的包
-z 指定apk资源路径
-f 指定dex文件路径
-rf 指定源码路径
6. 签名apk jarsigner/apksigner
- jarsigner:v1签名,jdk/bin/,参考 [jarsigner指导文档]
- apksigner:v2签名,ANDROID_SDK/build-tools/
jarsigner用法
jarsigner -digestalg SHA1 -sigalg MD5withRSA -keystore mykeystore.key -storepass STORE_PASS MY_APP.apk KEY_ALIAS
-verbose 签名/验证时输出详细信息
-keystore 密钥库位置
-storepass 用于密钥库完整性的口令
-keypass 专用密钥的口令(如果不同)
-signedjar 已签名的 JAR 文件的名称 (第一个apk是签名之后的文件, 第二个apk是需要签名的文件)
apksigner用法
见[ 官方指导:命令行工具-apksigner ]
- apksigner:ANDROID_SDK/build-tools/
apksigner sign --ks **.keystore --ks-key-alias [别名] --ks-pass pass:[别名密码] --key-pass pass:[证书密码] --out [签名后文件存放路径] [未签名的文件路径]
// 私钥和证书必须提供,有两种方式:
--ks:指定密钥库文件(即.keystore/.jks,私钥和证书被存放其中)
--key 和 --cert:分别指定私钥和证书,私钥必须使用 PKCS #8 格式(.pk8),证书必须使用 X.509 格式(x509.pem)。
7. 对齐apk zipalign
见[ 官方指导:命令行工具-zipalign ]
- zipalign:ANDROID_SDK/build-tools/
- 若使用v1签名,zipalign 必须在签名后进行
- 若使用v2签名,zipalign 必须在签名前进行
// 对齐
zipalign -p -f -v 4 infile.apk outfile.apk
// 确认existing.apk的对齐方式
zipalign -c -v 4 existing.apk
-c 仅检查对齐情况(不会修改文件)。
-f 覆盖现有输出文件。
-h 显示工具帮助。
-p 使未压缩的 .so 文件对齐页面。
-v 详细输出。
-z 使用 Zopfli 重新压缩。
zipalign是一个zip归档文件对齐工具,它将zip(apk也是zip文件)中的所有未压缩文件相对于文件开头对齐(偏移为4字节的整数倍),这样就可以通过内存映射(mmap)访问这些文件,而无需在 RAM 中复制这些数据,既高效又减少了应用的内存使用。
CPU在处理内存数据时,并非一次提取一个memory cell,通常是提取一组相邻内存单元。在32-bit machine,CPU一次从内存中读取4个连续的memory cell(4-byte) 。4 byte chunk(4字节流) 为一个读取周期。比如,读取一个int型 数据时,需要一个读取周期(int 占4 byte),读取Double型,则需要2个读取周期。
注:上图是以c代码为例,c语言中 1char = 1byte,而在java中 1char=2byte。
附加:smali工具
baksmali:将dex编译为smali
java -jar baksmali-1.3.2.jar -o classout/ classes.dex
smali:将smali反编译成dex
java -jar smali-1.3.2.jar classout/ -o classes.dex