赞
踩
作为一个Android
开发,每天都会有相当一部分的时间花在编译打包上,如果项目比较大的话编译一次可能就要十几分钟。
那么在编译打包的过程中AGP
到底做了什么?为什么编译那么耗时,又该怎么优化?要解决这些问题,首先就需要我们对编译打包的流程有个总体的了解
本文主要包括以下内容
Task
首先看下Android
官网给出的编译打包总体流程
典型 Android
应用的构建流程如图所示,主要分为以下几步:
DEX
文件(Dalvik
可执行文件,其中包括在 Android
设备上运行的字节码),并将其他所有内容转换成编译后的资源。DEX
文件和编译后的资源组合成 APK
或 AAB
(具体取决于所选的 build
目标)。APK
或 AAB
签名。APK
之前,打包器会使用 zipalign
工具对应用进行优化,以减少其在设备上运行时所占用的内存关于Android
编译打包还有一张更加复杂的图
这个看起来是相当复杂的,但其实我们也可以把这些步骤做一个分类,跟总体流程的四个步骤做一个对应
apk
资源包含:
res
目录下的所有文件assets
目录下的文件AndroidManifest.xml
apk
的资源编译是编译过程中的一项主要工作,AGP3.0.0
之后默认通过AAPT2
来编译资源。
AAPT2
(Android Asset Packaging Tool2
)是一种构建工具,Android Studio
和 Android Gradle
插件使用它来编译和打包应用的资源。AAPT2
会解析资源、为资源编制索引,并将资源编译为针对 Android
平台进行过优化的二进制格式。
AAPT2
做了什么优化?为什么AGP3.0.0
之后默认通过AAPT2
来编译资源呢?它又做了什么优化呢?
AAPT2
支持通过启用增量编译实现更快的资源编译。这是通过将资源处理拆分为两个步骤来实现的:
把所有的Android
资源文件进行解析,生成扩展名为.flat
的二进制文件。比如是png
图片,那么就会被压缩处理,采用.png.flat
的扩展名。可以在build/intermediates/merged_res/
文件下查看生成的中间产物
首先,这一步会生成辅助文件,比如R.java
与resources.arsc
,R
文件大家应该都比较熟悉,就是一个资源索引文件,我们平时引用也都是通过R.
的方式引用资源id
。而resources.arsc
则是资源索引表,供在程序运行时根据id索引到具体的资源
最后,会将R
文件,ressources.arsc
文件和之前的二进制文件进行打包,打包到一个软件包中。
这种拆分方式有助于提高增量编译的性能。例如,如果某个文件中有更改,您只需要重新编译该文件。
AIDL
文件编译对于AIDL
,大家应该都很熟悉,它是一种用于进程间通信的接口文件。
其实它是Google
为了帮助我们进行进程间通信的简便写法,最后还是需要被解析编译为java
文件,而做这个工作的就是aidl
工具,存在于sdk/build-tools
目录。
这个阶段的主要的工作就是将项目中的aidl
文件编译为java
文件。
Java
与Kotlin
文件编译Java Compiler
编译项目中所有的Java
代码,包括R.java
、.aidl
文件生成的.java
文件、Java
源文件,生成.class
文件。在对应的build
目录下可以找到相关的代码Kotlin Compiler
编译项目中的所有Kotlin
代码,生成.class文件
注解处理器(APT
,KAPT
)生成代码也是在这个阶段生成的。当注解的生命周期被设置为CLASS
的时候,就代表该注解会在编译class
文件的时候生效,并且生成java
源文件和Class
字节码文件。
Class
文件打包成DEX
这一步就是将.class
文件打包成dex
文件。
有人可能会奇怪了,.class
文件不就是JVM
可以识别的二进制文件吗,为什么还要进行一次转化呢?
这就涉及到另一个问题:JVM
和 Dalvik(ART
的区别。
其中一个重要的区别就是Dalvik(ART)
有自己的二进制文件,也就是.dex
文件,所以需要将class
文件进行再一次转换。
你可以把dex
文件理解为一个class
文件包,里面装着很多的class
文件,让这些类能够共享数据,类似这种关系:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vlNHXxfu-1656308643096)(https://upload-images.jianshu.io/upload_images/27762813-f22c4c7ea5d97395.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)]
D8
编译器与R8
工具在 AGP 3.X
以后,Google
分别引入 D8
编译器和 R8
工具作为默认的 DEX
编译器和混淆压缩工具。
AGP3.0.1
之后,D8
编译器取代了Dx
,用于将class
文件打包成DEX
,D8
编译器编译更快、时间更短;DEX
编译时占用内容更小;生成的dex
文件大小更小;同时拥有相同或者是更好的运行时性能;AGP3.4.0
之后,默认开启R8
,R8
是 ProGuard
的替代工具,用于代码的压缩(shrinking
)和混淆(obfuscation
)在 AGP3.4.0
版本中,R8
把 desugaring
、shrinking
、obfuscating
、optimizing
和 dexing
都合并到一步进行执行。在 AGP3.4.0
以前的版本编译流程如下:
在AGP3.4.0
之后的编译流程如下:
APK
包在资源文件与代码文件都编译完成后,接下来就是生成apk
包了,将manifest
文件、resources
文件、dex
文件、assets
文件等等打包成一个压缩包,也就是apk
文件。
在老版本使用的工具是apkbuilder
,但是在最新的版本我发现没有这个工具了,sdk
目录下也找不到了。
在AGP3.6.0
之后,使用zipflinger
作为默认打包工具来构建APK
,以提高构建速度
zipalign
(对齐处理)zipalign
是一种归档对齐工具,可对 Android
应用 (APK
) 文件提供重要的优化
zipalign
会对apk
中未压缩的数据进行4字节对齐,对齐的主要过程是将APK
包中所有的资源文件距离文件起始偏移为4字节整数倍,对齐后就可以使用mmap
函数读取文件,可以像读取内存一样对普通文件进行操作。如果没有4字节对齐,就必须显式的读取,这样比较缓慢并且会耗费额外的内存。
有的同学可能会有疑问,这个对齐处理不是应该放在签名之后吗?其实这里就涉及到了签名工具的不同带来的对齐处理的顺序不同:
apksigner
,只能在为 APK
文件签名之前执行 zipalign
。jarsigner
,只能在为 APK
文件签名之后执行 zipalign
。APK
进行签名在生成APK文件之后,必须对该apk
文件进行签名,否则无法被安装。
之前大家比较熟知的签名工具是JDK
提供的jarsigner
,而apksigner
是Google
专门为Android
提供的签名和签证工具。
其区别就在于jarsigner
只能进行v1
签名,而apksigner
可以进行v2
、v3
、v4
签名。下面我们简单介绍下V1
签名和V2
签名的区别:
V1
签名v1
签名方式主要是利用META-INFO
文件夹中以MF
、SF
和 RSA
的三个文件,流程如下所示:
首先,将apk
中除了META-INFO
文件夹中的所有文件进行进行摘要写到 META-INFO/MANIFEST.MF
;然后计算MANIFEST.MF
文件的摘要写到CERT.SF
;最后计算CERT.SF
的摘要,使用私钥计算签名,将签名和开发者证书写到CERT.RSA
。
所以META-INFO
文件夹中这三个文件就能保证apk
不会被修改。但是V1
签名方案主要有两个问题
Apk
中所有的文件进行校验,这会拖累老设备的安装时间。META-INFO
文件夹不会被签名,存在一定安全隐患V2
签名Android7.0
之后,Google
推出了V2
签名,解决V1
签名速度慢以及签名不完整的问题。
apk本质上是一个压缩包,而压缩包文件格式一般分为三块:
文件数据区,中央目录,中央目录结束节。
而V2
要做的就是,在文件中插入一个APK
签名分块,位于中央目录部分之前,如下图:
这样处理之后,文件签名完成就无法修改了,这也是为什么ZipAlign
对齐只能在ApkSigner
签名之前执行的原因。
Task
上面介绍了Apk
编译打包过程的主要步骤,这些步骤也都是通过AGP
插件实现的,那么这些主要步骤又对应AGP
中的哪些Task
呢
当我们在Android Studio
中点击Run
时,便可以在控制台看到一系列的Task
执行
Executing tasks: [:app:assembleDebug] in project > Task :app:preBuild UP-TO-DATE > Task :app:preDebugBuild UP-TO-DATE > Task :app:mergeDebugNativeDebugMetadata NO-SOURCE > Task :app:compileDebugAidl NO-SOURCE > Task :app:compileDebugRenderscript NO-SOURCE > Task :app:dataBindingMergeDependencyArtifactsDebug UP-TO-DATE > Task :app:dataBindingMergeGenClassesDebug UP-TO-DATE > Task :app:generateDebugResValues UP-TO-DATE > Task :app:generateDebugResources UP-TO-DATE > Task :app:mergeDebugResources UP-TO-DATE > Task :app:packageDebugResources UP-TO-DATE > Task :app:parseDebugLocalResources UP-TO-DATE > Task :app:dataBindingGenBaseClassesDebug UP-TO-DATE > Task :app:generateDebugBuildConfig UP-TO-DATE > Task :app:checkDebugAarMetadata UP-TO-DATE > Task :app:mapDebugSourceSetPaths UP-TO-DATE > Task :app:createDebugCompatibleScreenManifests UP-TO-DATE > Task :app:extractDeepLinksDebug UP-TO-DATE > Task :app:processDebugMainManifest UP-TO-DATE > Task :app:processDebugManifest UP-TO-DATE > Task :app:processDebugManifestForPackage UP-TO-DATE > Task :app:processDebugResources UP-TO-DATE > Task :app:javaPreCompileDebug UP-TO-DATE > Task :app:mergeDebugShaders UP-TO-DATE > Task :app:compileDebugShaders NO-SOURCE > Task :app:generateDebugAssets UP-TO-DATE > Task :app:mergeDebugAssets UP-TO-DATE > Task :app:compressDebugAssets UP-TO-DATE > Task :app:processDebugJavaRes NO-SOURCE > Task :app:checkDebugDuplicateClasses UP-TO-DATE > Task :app:desugarDebugFileDependencies UP-TO-DATE > Task :app:mergeExtDexDebug UP-TO-DATE > Task :app:mergeLibDexDebug UP-TO-DATE > Task :app:mergeDebugJniLibFolders UP-TO-DATE > Task :app:mergeDebugNativeLibs NO-SOURCE > Task :app:stripDebugDebugSymbols NO-SOURCE > Task :app:validateSigningDebug UP-TO-DATE > Task :app:writeDebugAppMetadata UP-TO-DATE > Task :app:writeDebugSigningConfigVersions UP-TO-DATE > Task :app:compileDebugKotlin > Task :app:compileDebugJavaWithJavac > Task :app:mergeDebugJavaResource UP-TO-DATE > Task :app:dexBuilderDebug UP-TO-DATE > Task :app:mergeProjectDexDebug > Task :app:packageDebug > Task :app:createDebugApkListingFileRedirect UP-TO-DATE > Task :app:assembleDebug BUILD SUCCESSFUL in 2s 35 actionable tasks: 4 executed, 31 up-to-date
上面就是点击运行过程中运行的所有Task
,我们精简一下,列出上面主要步骤中提到的Task
//aidl 转换aidl文件为java文件 > Task :app:compileDebugAidl //生成BuildConfig文件 > Task :app:generateDebugBuildConfig //获取gradle中配置的资源文件 > Task :app:generateDebugResValues // merge资源文件,AAPT2 编译阶段 > Task :app:mergeDebugResources // merge assets文件 > Task :app:mergeDebugAssets > Task :app:compressDebugAssets // merge所有的manifest文件 > Task :app:processDebugManifest //生成R文件 AAPT2 链接阶段 > Task :app:processDebugResources //编译kotlin文件 > Task :app:compileDebugKotlin //javac 编译java文件 > Task :app:compileDebugJavaWithJavac //转换class文件为dex文件 > Task :app:dexBuilderDebug //打包成apk并签名 > Task :app:packageDebug
上面这些Task
就对应于上面说的编译过程中的主要步骤,比如mergeDebugResources
就对应于AAPT2
的编译阶段,在Task
结束后,会在build/intermediates/merged_res/
文件夹中生成Flat
文件
而processDebugResources
则对应于AAPT2
的链接阶段,会生成R.java
与resources.arsc
,并合并所有已编译的文件并将它们打包到一个软件包中
关于其他Task
内容也都比较多,感兴趣的同学可以自行查看相关源码,这里就不缀述了
本文主要详细介绍了Android APK
打包编译的总体流程,主要步骤,以及AGP
中相关的Task
。这些知识点在平常的开发中或许没有多大用处,但是如果你要做包体积优化,或者编译优化相关的一些工作的话,这些应该是需要了解的前置知识,希望对你有所帮助~
作者:程序员江同学
链接:https://juejin.cn/post/7113713363900694565
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。