Android Build

Apk总构建流程

简述

Aapt 会将主工程、依赖库中的资源(res、assets)和androidManifest都合并,产出R.java、资源及资源索引resources.arsc;

之后javac编译包括R.java文件、主工程的java文件、aidl产生的java文件,产出class文件;如果需要插桩的话就插桩

之后使用Proguard/R8混淆工具对.class文件脱糖、压缩、混淆等,产出新的class文件;

之后使用Dx/D8编译工具将新的class文件再转换成dex文件,

之后打包成apk,然后签名、zipalign优化。

工具:aapt/aapt2、javac、Proguard/R8、Dx/D8、ApkBuilder、zipalign

img

典型的APK中内容

  • AndroidManifest.xml 程序全局配置文件
  • classes.dex Dalvik字节码
  • resources.arsc 资源索引表
  • META-INF该目录下存放的是签名信息
  • res 该目录存放资源文件
  • assets该目录可以存放一些配置或资源文件

流程大致总结为:

1、资源编译:主工程和三方库中的Mainifest、资源(res和assets)的合并,而后由aapt编译构建后产生R.java、resources.arsc(资源索引表)。如果是AAPT2的话会将资源编译成二进制文件.flat

2、**.aidl编译生成.java文件**

3、java源码编译:AIDL产生的.java文件、主工程中的java文件、上述的R.java文件 一起经过javac工具编译后,产生.class文件

3又1/2、 Tramform插桩 & ASM修改字节码

4、Proguard/新为R8:脱糖、压缩、优化、混淆

5、转化为dex:调用dx.bat(新为D8)将所有的class文件(上一个Transform的结果,包括3、java源码编译和第三方库中的.class)转化为 classes.dex文件(如果是multiDex的话就是classesN.dex),dx会将class转换为Dalvik字节码

6、打包生成apk:通过sdklib.jar的ApkBuilder类进行打包,生成apk

7、签名:签名过程主要利用apksign.jar或者jarsinger.jar两个工具

8、zipalign优化:通过 zipalign工具进行内存对齐工作。

0003

aapt2编译简要过程

资源文件编译(AAPT2)

resource(/res)目录 :

res/ 目录下的所有资源文件AndroidManifest.xml 都会被编译成 .flat 的二进制文件(体积更小,解析速度更快),同时会被映射到R.java文件,resource.arsc文件,访问的时候直接使用资源ID即 R.id.filename (另外,依赖库中的resource也会被引入而assets不会)

//如果是AAPT则是直接合并AndroidMenifest,直接使用res。而不会将这两者编译成二进制文件。

image-20210402161114621

assets( /assets)目录:

会被原封不动打包进apk中,没有R文件映射,访问的时候需要AssetManager类。

java文件编译

javac 是将 .java 文件编译成 .class文件的工具,是JRE提供的工具。

Tramform插桩 & ASM修改字节码

在javac执行完成,生成.class文件之后,会经过desugar脱糖、shrinker压缩、optimizer优化、等transform,而我们可以通过自定义Transform,自定义Transform会插到整个Transform链条的最前面,并且除了主工程java文件编译后的class文件,也能拉去到第三方依赖包jar/aar,及asset文件 然后我们在自定义Transform中拿到的字节码(主工程和三方库)可以用ASM进行修改,最后结果产物又会传递给Transform链后面的脱糖、压缩、优化等Transform继续处理。

混淆(Proguard->R8)

简述:

R8之后:.class文件会在一个步骤中执行:脱糖、摇树、混淆、Dex处理

  • 脱糖(Desugar):去除语法糖,还原原有代码,kt很多语法糖比如拓展函数之类;

  • 摇树(Code shrinking):tree shaking,从AndroidMainifest中的所有入口(Activity、Serivce等)入手,构建运行所需要的所有类,形成一张图,不在图上的类都可能被移除;

  • 资源缩减(Resource shrinking):只有在打开了minifyEnabled才有效,配合摇树,删除所有不被引用的资源,包括依赖的库资源。

  • 混淆(Obfuscation):缩短应用的所有非保留代码(@Keep)中的类名、方法名、字段名

  • Dex处理:.class转.dex

  • 优化(Optimization):

    • 如果您的代码从未采用过给定 if/else 语句的 else {} 分支,R8 可能会移除 else {} 分支的代码。
    • 如果您的代码只在一个位置调用某个方法,R8 可能会移除该方法并将其内嵌在这一个调用点。
    • 如果 R8 确定某个类只有一个唯一子类且该类本身未实例化(例如,一个仅由一个具体实现类使用的抽象基类),它就可以将这两个类组合在一起并从应用中移除一个类。

简述:

引入R8前,.class文件得先由Proguard压缩、摇树、混淆、预校验后生成新的.class文件,再经过D8编译成.dex

引入R8后,.class文件会在一个步骤中压缩、摇树、混淆、预校验、D8编译后直接生成.dex

Proguard

如果打开了混淆(minifyEnabled = true)

那么在上述编译过程中, 工程项目和R.java 文件编成 .class 文件后,R8(旧Progurad 新R8)即会对 .class文件进行 脱糖、压缩、混淆、优化。 之后形成 新的 .class文件后再交给 D8 编译器 编译成.dex文件 ,之后继续走下一步构建流程。

ProGuard 与 R8 都提供了压缩(shrinker)、优化(optimizer)、混淆(obfuscator)、预校验(preverifier)四大功能:

  1. 压缩(也称为摇树优化,tree shaking):从 应用及依赖项 中移除 未使用 的类、方法和字段,有助于规避 64 方法数的瓶颈

  2. 优化:通过代码分析移除更多未使用的代码,甚至重写代码

  3. 混淆:使用无意义的简短名称 重命名 类/方法/字段,增加逆向难度

  4. 预校验:对于面向 Java 6 或者 Java 7 JVM 的 class 文件,编译时可以把 预校验信息 添加到类文件中(StackMap 和 StackMapTable属性),从而加快类加载效率。预校验对于 Java 7 JVM 来说是必须的,但是对于 Android 平台 无效

minifyEnabled只对代码进行压缩、优化、混淆

shrinkResources

Ps:shrink 美 [ʃrɪŋk]

如需启用资源缩减功能,请将 build.gradle 文件中的 shrinkResources 属性(若为代码缩减,则还包括 minifyEnabled)设为 true

1
2
3
4
5
6
7
8
9
10
11
12
android {
...
buildTypes {
release {
shrinkResources true
minifyEnabled true
proguardFiles
getDefaultProguardFile('proguard-android.txt'),
'proguard-rules.pro'
}
}
}

资源缩减只有在与代码缩减配合使用时才能发挥作用。在代码缩减器移除所有不使用的代码后,资源缩减器便可确定应用仍要使用的资源,当您添加包含资源的代码库时尤其如此。您必须移除不使用的库代码,使库资源变为未引用资源,因而可由资源缩减器移除。

.class文件编译(DX->D8)

DX和D8就是将 .class 文件转成 .dex文件的工具

资源文件编译后产出的R.java 和 工程中的其他.java 文件一起,先通过 javac命令编译成**.class**文件,然后所有产出的 .class 文件再通过 dx指令(现默认D8编译器) 编译成 .dex文件 ,

class转换为Dalvik字节码,生成常量池,消除冗余数据等,目的是其中各个类能够共享数据,在一定程度上降低了冗余,同时也是文件结构更加经凑,实验表明,dex文件是传统jar文件大小的50%左右。class文件结构和dex文件结构比对。

image-20210402142211305

image-20210402142104030

打包成Apk

打包生成APK文件。旧的apkbuilder脚本已经废弃,现在都已经通过sdklib.jar的ApkBuilder类进行打包了。输入为我们之前生成的包含resources.arcs的.ap_文件,上一步生成的dex文件,以及其他资源如jni、jar包内的资源。

大致步骤为
以包含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());

签名

对一个APK文件签名之后,APK文件根目录下会增加META-INF目录,该目录下增加三个文件:

  • MANIFEST.MF
  • [CERT].RSA
  • [CERT]

Android系统就是根据这三个文件的内容对APK文件进行签名检验的。

zipalign优化

调用buildtoolszipalign,对签名后的apk文件进行对齐处理,使apk中所有资源文件距离文件起始偏移为4字节的整数倍,从而在通过内存映射访问apk文件时会更快。同时也减少了在设备上运行时的内存消耗。

附录:

版本迭代,R8、D8引入

安装

安装方式
系统程序安装,开机时安装,没有安装界面。
第一步,将apk文件解压复制到程序目录下(/data/app/);第二步,为应用创建数据目录(/data/data/package name/)、提取dex文件到指定目录(/data/dalvik-cache/)、修改系统包管理信息。
由开机时启动的PackageManagerService服务完成,会在启动时扫描/system/app, vender/app, /data/app, /data/app-private并安装。
PackageInstallerActivity
当Android系统请求安装apk程序时,会启动这个Activity,并通过Intent读取传来的apk信息。下面是apk安装的具体过程。

  1. 解析过程会首先读取AndroidManifest.xml获取程序包名以构建Package对象,然后再处理manifest的其他标签包括四大组件,并把信息全都存到Package对象里面。
  2. 首先检测该程序是否已安装,是则弹框提示是否替换程序,否则直接调用startInstallConfirm(),做UI初始化和事件绑定,于是当我们点击安装的时候则会触发onClick下的OK按钮事件:
  3. 无论是替换还是新安装,都会调用scanPackageLI(),然后跑去scanPackageDirtyLI,它会判断是否为系统程序,解析apk程序包,检查依赖库,验证签名,检查sharedUser签名、权限冲突、ContentProvider冲突,更新native库目录文件(检测abi),进行dexopt,杀掉现有进程(仅对覆盖安装的场景)等等,最后调用createDataDirsLI()进行实际安装:
    4.执行完毕后,通过socket回传结果,而PackageInstaller根据返回结果做对应处理并显示给用户,至此为止,整个apk安装过程结束。

包名签名相关
android系统使用包名(package name)来判定应用程序的同一性,但是由于包名可以由开发者自由设置,为了保护应用程序不被其他开发者开发的同包名应用覆盖,用于发布的Android应用程序需要加上开发者签名。在应用程序被升级的时候,Android系统将会验证被升级的应用程序包与升级后的应用程序包是否使用了同样的开发者签名,如果一致,该应用程序可以被升级;如果不一致,那么将被视为非同一开发者开发的应用程序,用户需要先卸载已经安装的应用然后再安装新应用,在卸载的过程中,应用在android系统中所保存的设置信息(SavedPreferences)将被删除,以保护应用本地保存的资料不被盗取。综上,应用是否可以覆盖的方式是对于包名的判断,然后是对于该APK签名的判断。

AOP原理

AOP原理

在确定好技术选型以后我们来看下ASM的相关原理。其实通过上图我们已经能够大概了解其大致的原理。AS Gradle的编译会将我们的java class文件、jar包以及resource资源文件打包最为最原始的数据输出给第一个Transform,第一个transform处理完的产物再输出给第二个transform,以此类推形成完整的链路。而ASM就是作用于图中的第一个红色TransformA。它会拿到一开始的原始数据以后会进行一定的分析。并且按照JVM字节码的格式针对类、变量、方法等类型调用相关的回调方法。在相应的回调方法中我们可以对相关的字节码指令进行操作。比如新增、删除等等。中间的图片就是它具体的运行时序图。最后两者结合编译

手动构建 APK(aapt2)

最后我们通过命令行来手动打包一个可执行的 APK,能让我们对 APK 构建的理解更加深入。首先需要准备下 代码、资源文件、AndroidManifest 这些构建 APK 的必要文件。

图片

① 通过 aapt2 compile 将 res 资源编译成 .flat 的二进制文件:

1
aapt2 compile -o build/res.zip --dir res

② 通过 aapt2 link 将 .flat 和 AndroidManifest 进行连接,转化成不包含 dex 的 apk 和 R.java:

1
aapt2 link build/res.zip -I $ANDROID_HOME/platforms/android-30/android.jar --java build --manifest AndroidManifest.xml -o build/app-debug.apk

③ 通过 javac 将 Java 文件编译成 .class 文件:

1
javac -d build -cp $ANDROID_HOME/platforms/android-30/android.jar com/**/**/**/*.java

④ 通过 d8 将 .class 文件转化成 dex 文件:

1
d8 --output build/ --lib $ANDROID_HOME/platforms/android-30/android.jar build/com/tencent/hockeyli/androidbuild/*.class

⑤ 合并 dex ⽂件和资源⽂件:

1
zip -j build/app-debug.apk build/classes.dex

⑥ 对 apk 通过 apksigner 进行签名:

1
apksigner sign -ks ~/.android/debug.keystore build/appdebug.apk

参考

10分钟了解Android项目构建流程 - 掘金 (juejin.cn)

APK打包安装过程 - SegmentFault 思否

Author

white crow

Posted on

2021-04-01

Updated on

2024-03-25

Licensed under