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

在安卓编译的过程中,会先处理工程里面的资源文件。资源文件经历了合并、编译的过程得到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图片,但是库里面的图片需要压缩的话,需要每个库自己配置。

相关推荐
故事与他64543 分钟前
Thinkphp(TP)框架漏洞攻略
android·服务器·网络·中间件·tomcat
每次的天空3 小时前
项目总结:GetX + Kotlin 协程实现跨端音乐播放实时同步
android·开发语言·kotlin
m0_748233175 小时前
SQL之delete、truncate和drop区别
android·数据库·sql
CYRUS_STUDIO6 小时前
OLLVM 增加 C&C++ 字符串加密功能
android·c++·安全
帅次8 小时前
Flutter 输入组件 Radio 详解
android·flutter·ios·kotlin·android studio
&有梦想的咸鱼&9 小时前
Android Compose 框架的状态与 ViewModel 的协同(collectAsState)深入剖析(二十一)
android
开开心心就好9 小时前
高效PDF翻译解决方案:多引擎支持+格式零丢失
android·java·网络协议·tcp/ip·macos·智能手机·pdf
路上阡陌10 小时前
docker 安装部署 canal
android·adb·docker
&有梦想的咸鱼&10 小时前
入剖析 Android Compose 框架的关键帧动画(keyframes、Animatable)(二十三)
android
thinkMoreAndDoMore11 小时前
android音频概念解析
android·音视频