作为一个Android 开发者,对于大部分人来说APK可能是既熟悉又陌生,熟悉的是所有的源文件最终的成品都是APK,陌生的是很多人可能不知道背后发生的一些流程,简单来说就是如何从项目中的源文件经由构建系统编译打包最终输出APK。
Google 基于IntelliJIdea 开发了Android Studio (当然如果不使用 Android Studio,可以通过命令行执行指令来构建和运行您的应用。 ),为了降低开发者的开发难度和开发成本,提供了Android 插件,同时采用了Gradle 构建系统来编译应用资源和源文件,并将它们打包成可快速测试、部署、签署和分发的 APK,得益于Gradle 强大的构建机制,除了默认的配置之外,还支持根据需求去自定义构建配置信息,其中每个构建配置均可自行定义一组代码和资源(即源集SourceSet),同时对所有应用版本公有部分的重复利用。要想充分了解Gradle构建系统以下的一些名词你需要掌握:
Android Studio 按照逻辑关系将每个模块的源代码文件和资源(包含assets、JNI、清单文件),Android Studio 默认就为我们创建了main 源集,main源集包含的源代码文件和资源,可供其他源集共享使用。源集还和构建变体有关系,在您配置新的构建变体时,除了可以使用main源集还可以自己创建对应构建变体的源集,创建源集的操作见Gradle脚本语法详解
src/main/——main源集包括所有构建变体所公有的代码和资源。
src/buildType/——创建此格式源集可加入特定构建类型专用的代码和资源。
src/productFlavor/——创建此格式源集可加入特定产品风格专用的代码和资源,如果配置构建以组合多个产品风格,则可为风格维度间产品风格的各个组合创建源集目录: src/productFlavor1ProductFlavor2/
src/productFlavorBuildType/——创建此格式源集可加入特定构建变体专用的代码和资源
注意:当您在 Android Studio 中使用 File > New 菜单选项新建文件或目录时,可以针对特定源集进行创建, 可供您选择的源集取决于您的构建配置,如果所需目录尚不存在,Android Studio 会自动创建,而且以上的buildType、productFlavor、productFlavor1ProductFlavor2、productFlavorBuildType皆不是意味着目录名称,只是表示目录名称的格式。
如果不同源集均包含同一文件的不同版本,Gradle 在进行编译时将按以下优先顺序决定使用哪一个文件(左侧源集替换右侧源集的文件和设置):构建变体 > 构建类型 > 产品风格 > 主源集 > 库依赖项;而对于清单文件则是采取合并的方式处理,在合并多个清单时,Gradle 也使用同一优先顺序。
构建类型定义 Gradle 在构建和打包您的应用时使用的某些属性(Android Studio 默认在build.gradle脚本中buildTypes节点下会创建子节点debug调试和发布release构建类型),针对开发生命周期的不同阶段进行配置,例如调试构建类型支持调试选项,使用调试密钥签署 APK;而发布构建类型则可压缩、混淆 APK 以及使用发布密钥签署要分发的 APK,必须至少定义一个构建类型才能构建应用。
产品风格代表可以向用户发布的不同版本的应用(例如免费和付费版本的应用) 也可以将产品风格自定义为使用不同的代码和资源,同时对所有应用版本共有的部分加以共享和重复利用。 不过产品风格是不是必选项,但是如果需要就必须手动创建。
构建变体是构建类型与产品风格的交叉产物,是 Gradle 在构建应用时使用的配置。 您可以利用构建变体在开发时构建调试版本的产品风格,或者构建要分发的已签署发布版产品风格。 您不是直接配置构建变体,而是配置组成变体的构建类型和产品风格。 创建附加构建类型或产品风格也会创建附加构建变体。
构建系统支持在构建配置中指定签署设置,并可在构建过程中自动签署您的 APK。 构建系统通过使用已知凭据的默认密钥和证书签署调试版本,以避免在构建时提示密码。 除非您为此构建明确定义签署配置,否则,构建系统不会签署发布版本。
构建系统支持为每个构建变体指定不同的 ProGuard 规则文件,在构建时运行 ProGuard 对类进行压缩和混淆处理。
在 2018 年的 Google I/O 大会上,Google 向 Android 引入了新 App 动态化框架AAB(即 Android App Bundle)。Android App Bundle 是一种包含编译后代码和资源文件的新的上传格式(.aab),它推迟了 APK 的生成和签名,由 Google play 来完成,是Google Play 推出新 app 交付模式,叫做动态交付 (Dynamic Delivery),它会根据每个用户的设备信息,使用开发者上传的 app bundle 来生成对应的 apk 文件,
注意: 使用aab文件会将apk的大小限制增加到500MB,这个限制不是指aab文件的大小,而是下载apk时的大小,每个 APP bundle 对应一个独立的 app 或 applicationID。因此,如果你使用了 product flavors 来创建不同版本的 app,并且每个版本 app 都对应一个不同的 applicationID,那你就需要为每个 app 构建一个 app bundle
其中蓝色方框区域就是configuration apks支持的配置项,也可以在 app bundle 中添加 dynamic feature modules,这些模块可以包含新的功能和资源。开发者可以决定用户第一次安装时需不需要下载这些资源构建一个Android App Bundle,只需要几次点击,但是添加dynamic feature模块,可能需要重构整个项目,因为Android App Bundle != APK,而App Bundle 纯粹是为了上传设计的文件,用户无法直接安装和使用它。它仅是一个 zip 文件,Google Play 从中生成优化的 APK 并将其提供给设备进行安装,从 APK 切换到 App Bundle 是一个无缝过程。构建 app bundles 和支持动态交付的主要条件和步骤:
Split APKs 是 Android 5.0 开始提供多 apk 构建机制,是 Dynamic Delivery 功能的最基本组件。Split APKs 将原来一个 APK 中多个模块共享同一份资源的模型,分离成多个 APK 使用各自的资源,并且可以继承 Base APK 中的资源,多个 APK 有相同的 data,cache 目录,多个 dex 文件,相同的进程,在 Settings.apk 中只显示一个 APK,并且使用相同的包名。Split APK 可以将一个庞大的 APK,按屏幕密度,ABI 等形式拆分成多个独立的 APK,在应用程序更新时,不必下载整个 APK,只需单独下载某个模块即可安装更新。
对了,以上第7、8需要Google Play的支持,所以选择性了解。
Android的包文件APK本质上是包含代码和资源的一个压缩包,解压之后通常会有以下主要的目录:
文件内容 | 说明 |
---|---|
classess.dex | Java 源码被编译后生成的Dalvik 虚拟机字节码文件,即Dalvik虚拟机的可执行文件 |
lib | 其子目录 armeabi 存放的是 so 文件,若需要使用 JNI调用 C/C++,则so 文件放到该目录下。 |
AndroidManifest.xml | 程序的全局配置文件。此文件在每个应用中都必须被定义和包含,它描述了应用的名称、开放权限、引用的库文件、版本号等重要信息。 |
resources.arsc | 经过编译的二进制资源文件,Id与真实文件路径的映射表。 |
res | 存放资源文件的,包括程序使用的布局文件、图片和字符串常量等资源。 |
META-INF | META-INF 目录下保存的则是应用中的签名信息,签名信息可以一定程度上验证原始apk 的完整性。 |
assets | 存放的原生资源文件,android不为/assets下的文件生成ID,也不会进行编译。如果使用/assets下的文件,需要通过指定文件的路径和文件名 |
打包的简要流程可以概括为:aapt打包资源——>处理aidl——>javac 编译java源文件——>dx 把classes文件转为.dex——>apkbuilder脚本打包apk——>签名——>对齐。
详细的流程如下图所示:
aapt(The Android Asset Packaing Tool)位于android-sdk/build-tools目录下,负责把项目中的AndroidManifest.xml和res资源文件(除了assets、图片文件和raw目录之外)编译成二进制xml文件,最后生成相应的R.java和resources.arsc文件,其中R.java主要存放的是资源id,而resources.arsc则保存放了资源id与文件路径之间的映射关系。
aidl(Android Interface Definition Language)位于android-sdk/build-tools目录下,aidl工具解析接口定义文件然后生成相应的Java代码接口供程序调用,如果在项目没有使用到aidl文件,则会跳过这一步。
通过javac编译R.java、Java接口文件、Java源文件等项目中所有的Java代码编译成.class文件
javac -source 1.7 \ -target 1.7 \ -cp /xxx/AndroidSDK/sdk/platforms/android-25/android.jar \ ./src/com/crazymo/demo/MainActivity.java ./src/com/crazymo/demo/R.java \ -d ./gen/classes
dx/d8工具位于android-sdk/platform-tools 目录下,把所有的第三方的libraries和.class文件都会被转换成.dex文件供Android系统Dalvik虚拟机执行的,dx/d8工具的主要工作是将Java字节码转成成Dalvik字节码、压缩常量池、消除冗余信息等。
d8 是在 AS3.1 中被设置为了默认的 dex 编译器,以前使用的是dx,d8编译速度更快,输出的 .dex 文件更小,运行时效率"更高",不过如果你使用的是 AS 3.0.+ 版本,可以直接在项目的 gradle.properties 文件中,增加 enableD8 的开关,android.enableD8 = true就可以使用d8。
dx --dex \ --verbose \ --output ./gen/dex/classes.dex ./gen/classes/
apkbuilder是位于 android-sdk/tools目录下一个脚本文件(实际调用的是android-sdk/tools/lib/sdklib.jar文件中的com.android.sdklib.build.ApkbuilderMain类),所有没有编译的资源(如images等)、编译过的资源和.dex文件都会被apkbuilder工具打包到最终的.apk文件中。
在AndroidSDKBuildTools V17以上apkbuilder又被Google 给删除掉了,可以参考SDK自带的Ant打包脚本自己写一个。
aapt package -f \ -J ./gen \ -M ./AndroidManifest.xml \ -S ./res/ \ -I /xxx/AndroidSDK/sdk/platforms/android-25/android.jar \ -D ./output/demo_unsigned.apk
apkbuilder是位于 android-sdk/build-tools目录下一个脚本文件,一旦APK文件生成,它必须被签名才能被安装在设备上。在开发过程中,主要用到的就是两种签名的keystore:一种是用于调试的debug.keystore,在Android Studio中直接run以后跑在手机上的就是使用的debug.keystore;另一种就是用于发布正式版本的keystore。
apksigner sign --ks ur-release-key.jks demo.apk
zipalign位于android-sdk/build-tools目录下,主要对APK进行对齐处理,对齐的主要过程是将APK包中所有的资源文件距离文件起始偏移为4字节整数倍,这样通过内存映射访问apk文件时的速度会更快,对齐的作用就是减少运行时内存的使用,如果要上传到Google Play必须进行对齐操作。
zipalign -v -p 4 demo_unsigned.apk demo_signed.apk
Android应用程序资源的组织方式有18个维度,每一个维度都代表一个配置信息,从而可以使得应用程序能够根据设备的当前配置信息来找到最匹配的资源来展现在UI上,从而提高用户体验。为了支持Android资源管理框架快速定位最匹配资源,Android资源打包工具aapt在编译和打包资源的过程中,会执行以下两个额外的操作:赋予每一个非assets资源一个ID值并定义在R.java文件和生成一个resources.arsc文件
xml编写的Android资源文件都会编译成二进制格式的xml文件,资源的打包都是由AAPT工具来完成的,资源打包主要有以下流程:
XML资源文件之所以要从文本格式编译成二进制格式,原因主要有:
二进制格式的XML文件占用空间更小。因为所有XML元素的标签、属性名称、属性值和内容所涉及到的字符串都会被统一收集到一个字符串资源池中去,并且会去重。有了这个字符串资源池,原来使用字符串的地方就会被替换成一个索引到字符串资源池的整数值,从而可以减少文件的大小。
二进制格式的XML文件解析速度更快。因为二进制格式的XML元素里面不再包含有字符串值,因此就避免了进行字符串解析,从而提高速度。
虽然将XML资源文件从文本格式编译成二进制格式解决了空间占用以及解析效率的问题,但是对于Android资源管理框架来说,这只是完成了其中的一部分工作,Android资源管理框架的另外一个重要任务就是要根据资源ID来快速找到对应的资源,于是资源映射表应运而生。
上述9种类型的资源文件(除了raw类型资源以及Bitmap文件的drawable类型资源之外),这些二进制格式的XML文件分别有一个字符串资源池,用来保存文件中引用到的每一个字符串,包括XML元素标签、属性名称、属性值,以及其它的一切文本值所使用到的字符串。这样原来在文本格式的XML文件中的每一个放置字符串的地方在二进制格式的XML文件中都被替换成一个索引到字符串资源池的整数值,这写整数值统一保存在 R.java类中,R.java会和其他源文件一起编译到APK中去。
每个Android项目里都有一个R.java文件,用于存储资源的Id,如图所示:
每个资源项后的整数就是资源ID,资源ID是一个4字节的无符整数,其中最高字节是命名空间Package ID(用于标示资源的来源),Android系统自己定义了两个Package ID:系统资源命名空间0x01 和 应用资源命名空间:0x7f;
次字节是资源类型 Type ID(用于区分anim、color、string);最低两个字节是Entry ID,表示资源在其所属资源类型中所出现的次序。
Android加载资源的时候,会通过这个索引表根据资源ID进行资源的查找,为不同语言、不同地区、不同设备提供相对应的最佳资源(查找和通过Resources和 AssetManger来完成的)
其实resources.arsc是一个编译后的二进制文件内存中的数据结构如下:
注:整个文件都是有一系列chuck(块)构成的,chuck是整个文件的划分单位,每个模块都是一个chuck,chuck最前面是一个ResChunk_header的结构体,用来描述整个chunk的信息,更多关于索引表格式的细节,可以查阅源码文件/frameworks/base/xx/include/androidfw/ResourceTypes.h
数据结构用ResTable_header来描述,用来描述整个文件的信息,包括文件头大小,文件大小,资源包Package的数量等信息。
存放所有的字符串,所以资源复用这些字符串,字符串里存放的是资源文件的路径名和资源值等信息。全局字符串池分为资源类型(type)字符串池和
资源包会有多个,比如系统资源包、应用资源包,主要由以下主要部分组成:
从这里可以看到resources.arsc索引表存在很多常量池,常量池的使用目的也很明显,就是提供资源的复用率,减少resources.arsc索引表的体积,提高索引效率。