在安卓编译的过程中,会先处理工程里面的资源文件。资源文件经历了合并、编译的过程得到asrc文件。在最近的一次构建里发现打出来的apk包拖到 Android Studio 里查看的时候,res里面的文件名字全都变成了单个的符号:
一开始以为是isMinifyEnabled=true的原因,后来发现debug包如果isDebuggable=false,哪怕 isminifyEnable=false,也会出现这个现象。后来通过 gradle 添加
plain
android.enableResourceOptimizations=false
才解决。
到这里不由得追问几AI个问题:
- 资源编译,资源优化,资源混淆到底分别干了啥,哪些会影响包大小?每一步的产物在哪?
- 为什么资源不混淆,资源名也会变短,而且为什么和 isDebuggale 有关系?
- 如果想缩小包里图片的资源大小,我们要怎么做?安卓打包默认会做吗?
可惜,无所不能的AI在根本没有人投喂数据的安卓编译问题面前手足无措。那我们就只有靠自己弄明白这个问题了。
agp源码调试
想要弄清楚这些问题,没有一份agp源码调试看看是搞不明白的,毕竟安卓编译的逻辑太多了。
我们在idea里面新建一个java/kotlin工程,gradle依赖我们同版本的agp:
plain
implementation("com.android.tools.build:gradle:8.8.0")
这样就可以看到agp的源码。然后在运行配置里面新建一个远程jvm调试,在我们的安卓工程开启编译的时候debug这个远程调试即可。
而我们的安卓工程需要在构建的时候添加调试的参数选项:
plain
./gradlew assembleDebug
--no-daemon
-Dorg.gradle.debug=true
你点击了debug远程调试,这个构建命令才会继续往下执行。
安卓构建中的绝大部分task都在 com.android.build.gradle.internal.TaskManager
里面有注册体现,需要断点调试你关注的task就去里面寻找。
资源构建相关
源码里很容易搜到resources对应的任务创建:
- createMergeResourcesTask
- createProcessResTask
分别对应资源合并和资源处理的流程。
合并
资源合并见 MergeResources.CreationAction
MergeResources里定义了一个 MergedResourceWriter 执行资源合并。
它的接口MergeConsumer定义里面已经包含了关键的合并步骤, 开始、结束、添加被合并的元素。
目标目录路径为 app/build/intermediates/merged_res/debug/mergeDebugResources
需要编译的资源会存下来:
在merge结束的时候,会把 mComplleResourceRequests 都取出来依次提交编译:
这里compileOutputFor里面定义的产物名为 values_values.arsc.flat
这些其实就是记录配置一下,最后在 WorkerExecutorResourceCompilationService 的close方法里,会开始调用aapt2开始编译。
编译
编译的内容在WorkerExecutorResourceCompilationService里面又进行了处理。我把流程写代码注释里面:
plain
// WorkerExecutorResourceCompilationService
override fun close() {
if (requests.isEmpty()) {
return
}
val maxWorkersCount = aapt2Input.maxWorkerCount.get()
val jvmRequests = requests.filter {
canCompileResourceInJvm(it.inputFile, it.isPngCrunching) //筛选可以编译的文件
//内部实现逻辑如果资源是xml,返回true
//如果资源是png,并且gradle buildType里配置的 isCrunchPngs 是false,返回true
//实际上这里筛选的是不需要进行png压缩的资源
}
requests.removeAll(jvmRequests) // 移除这些过滤出来的资源
var ord = 0
val jvmBuckets =
jvmRequests.groupByTo(HashMap(maxWorkersCount)) { (ord++) % maxWorkersCount }
//把直接编译的资源分桶,这个应该是为了并行编译用
jvmBuckets.values.forEach { bucket ->
val workQueue = workerExecutor.noIsolation()
// 每个资源通过ResourceCompilerRunnable执行
workQueue.submit(ResourceCompilerRunnable::class.java) {
//ignore...
}
if (await) {
workQueue.await()
}
}
//处理剩下需要压缩的资源,按文件大小排序
requests.sortWith(compareBy({ getExtension(it.inputFile) }, { it.inputFile.length() }))
val buckets = minOf(requests.size, aapt2Input.maxAapt2Daemons.get())
for (bucket in 0 until buckets) {
//分桶执行,分桶对我们理解流程不重要,忽略
val bucketRequests = requests.filterIndexed { i, _ ->
i.rem(buckets) == bucket
}
val workQueue = workerExecutor.noIsolation()
//通过Aapt2CompileRunnable执行
workQueue.submit(Aapt2CompileRunnable::class.java) {
// ignroe...
}
if (await) {
workQueue.await()
}
}
requests.clear()
}
那我们需要关注的就是ResourceCompilerRunnable和Aapt2CompileRunnable
ResourceCompilerRunnable
执行compileResource里的compileFunction
具体根据不同资源文件类型区分为 compileTable(res/values下的xml)、compileXml(xml)、compileFile(other)和compilePng(png)。
- compileTable: res/values下面的xml文件编译成类似
values_strings.arsc.flat
这种 - compileZml/compilePng: 编译成
drawable_ic_launcher_foreground.xml.flat
这种
这些compile方法内部调用的是com.android.tools.build:aapt2-proto里的逻辑,生成编译后的文件。
Aapt2CompileRunnable
执行Aapt2Daemon的doCompile方法。
requestCompile使用的是aapt2的compile命令与命令行参数:
这个对应aapt2文档里关于编译的命令:
plain
aapt2 compile path-to-input-files [options] -o output-directory/
这里能注意到,只有isCrunchPngs为false的时候,才会加上--no-crunch选项(实际上通过前面的分析,如果isCrunchPngs为false的时候,也不会走到这里),那么如果isCrunchPngs为true的时候,会执行aapt2压缩图片的功能。
通过aapt2源码查看:android.googlesource.com/platform/fr...
结合ai的分析🐶,能确认:aapt2开启压缩图片之后,会使用libpng.so这个库进行图片压缩。png_compression_level_int表示的是压缩等级,默认为9,最高压缩级别。
资源处理
createProcessResTask里面创建注册以下task:
plain
taskFactory.register(
LinkApplicationAndroidResourcesTask.CreationAction(
creationConfig,
useAaptToGenerateLegacyMultidexMainDexProguardRules,
mergeType,
baseName,
isLibrary = false
)
)
if (!creationConfig.debuggable &&
!creationConfig.componentType.isForTesting &&
projectOptions[BooleanOption.ENABLE_RESOURCE_OPTIMIZATIONS]) {
taskFactory.register(OptimizeResourcesTask.CreateAction(creationConfig))
}
第一个注册的是编译后的资源链接任务,第二个是当debuggable为false,且android.enableResourceOptimizations为false的时候,注册优化资源任务。
链接
link的输出文件为:
DOT_RE为.ap_文件
最后会在processResources里执行 AaptDaemon的link方法:
这里执行的对应aapt2的link命令:
plain
aapt2 link path-to-input-files [options] -o
outputdirectory/outputfilename.apk --manifest AndroidManifest.xml
link会把资源表、资源文件等打包到 linked-resources-binary-format文件:
这个_ap既然是归档起来的文件,我们把这个文件unzip试试:
里面包含的是资源文件、arsc文件和manifest文件。
优化
OptimizeResourcesTask内部实现和前面几个task类似,实际通过命令后调用了aapt2的optimize:
plain
aapt2 optimize options file[,file[..]]
按照aapt2的文档,aapt2会对资源和asrc进行一些优化,使用更紧凑的二进制搜索树替换通常的平面表格表示法,这个优化会把apk大小缩小1-3%。
输出路径为 build/intermediates/optimized_processed_res:
总结
到此安卓资源编译处理流程有了一个比较清晰的概念。这里我把资源处理过程总结为图片:
顺便我们来解答一下文章开头的几个问题:
- 资源编译,资源优化,资源混淆到底分别干了啥,哪些会影响包大小?每一步的产物在哪?
按照资源合并->资源编译->资源链接->资源优化的步骤,资源编译的时候如果开启了isCrunchPngs的配置,会压缩png图片,减小包大小。当开启enableResourceOptimizations(默认开启)的时候,也会有一些结构上的优化,能小幅度优化包大小。
- 为什么资源不混淆,资源名也会变短,而且为什么和 isDebuggable 有关系?
这个是aapt2的optimized过程,和R8混淆资源无关,并且确实是在isDebuggable为true的时候开启。
-
如果想缩小包里图片的资源大小,我们要怎么做?安卓打包默认会做吗?
可以自己写一个task,在资源link之后处理.ap_里面的文件,这种做法无需关心库里的资源,也可以处理png之外的文件,例如jpg和webp。安卓打包默认不会压缩图片资源,只有配置了isCrunchPngs为true,aapt2才会压缩png图片,但是库里面的图片需要压缩的话,需要每个库自己配置。