安卓构建之资源构建流程分析

在安卓编译的过程中,会先处理工程里面的资源文件。资源文件经历了合并、编译的过程得到asrc文件。在最近的一次构建里发现打出来的apk包拖到 Android Studio 里查看的时候,res里面的文件名字全都变成了单个的符号:

一开始以为是isMinifyEnabled=true的原因,后来发现debug包如果isDebuggable=false,哪怕 isminifyEnable=false,也会出现这个现象。后来通过 gradle 添加

plain 复制代码
android.enableResourceOptimizations=false

才解决。

到这里不由得追问几AI个问题:

  1. 资源编译,资源优化,资源混淆到底分别干了啥,哪些会影响包大小?每一步的产物在哪?
  2. 为什么资源不混淆,资源名也会变短,而且为什么和 isDebuggale 有关系?
  3. 如果想缩小包里图片的资源大小,我们要怎么做?安卓打包默认会做吗?

可惜,无所不能的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:

总结

到此安卓资源编译处理流程有了一个比较清晰的概念。这里我把资源处理过程总结为图片:

顺便我们来解答一下文章开头的几个问题:

  1. 资源编译,资源优化,资源混淆到底分别干了啥,哪些会影响包大小?每一步的产物在哪?

按照资源合并->资源编译->资源链接->资源优化的步骤,资源编译的时候如果开启了isCrunchPngs的配置,会压缩png图片,减小包大小。当开启enableResourceOptimizations(默认开启)的时候,也会有一些结构上的优化,能小幅度优化包大小。

  1. 为什么资源不混淆,资源名也会变短,而且为什么和 isDebuggable 有关系?

这个是aapt2的optimized过程,和R8混淆资源无关,并且确实是在isDebuggable为true的时候开启。

  1. 如果想缩小包里图片的资源大小,我们要怎么做?安卓打包默认会做吗?

    可以自己写一个task,在资源link之后处理.ap_里面的文件,这种做法无需关心库里的资源,也可以处理png之外的文件,例如jpg和webp。安卓打包默认不会压缩图片资源,只有配置了isCrunchPngs为true,aapt2才会压缩png图片,但是库里面的图片需要压缩的话,需要每个库自己配置。

相关推荐
xiangpanf8 小时前
Laravel 10.x重磅升级:五大核心特性解析
android
robotx11 小时前
安卓线程相关
android
消失的旧时光-194312 小时前
Android 面试高频:JSON 文件、大数据存储与断电安全(从原理到工程实践)
android·面试·json
dalancon13 小时前
VSYNC 信号流程分析 (Android 14)
android
dalancon13 小时前
VSYNC 信号完整流程2
android
dalancon13 小时前
SurfaceFlinger 上帧后 releaseBuffer 完整流程分析
android
用户693717500138414 小时前
不卷AI速度,我卷自己的从容——北京程序员手记
android·前端·人工智能
程序员Android14 小时前
Android 刷新一帧流程trace拆解
android
墨狂之逸才15 小时前
解决 Android/Gradle 编译报错:Comparison method violates its general contract!
android
阿明的小蝴蝶16 小时前
记一次Gradle环境的编译问题与解决
android·前端·gradle