Android 包体积优化之重复资源删除

avatar
@比心
  1. 前言

在 Android 包体积治理的过程中,对于资源文件的治理一直是很重要的一部分,在开发业务的过程中,必要资源的压缩是资源治理的前提条件,但在后续开发过程中,如何进行长期的治理、预警、自动化是值得深入探讨的问题。本篇文章从资源文件治理中的重复资源剔除角度出发,抛砖引玉。

比心 Android 项目采用的是组件化的架构,多仓模式,各业务 module 不存在依赖关系,所以没办法直接访问,还有一些历史原因,部分资源文件也不在 UI 库中,业务开发过程中,会出现一个问题“A业务的同学直接 copy B 业务模块的资源文件,修改文件名后直接使用”,最终导致 apk 中存在多个相同文件,仅仅文件名不同而已,最终导致打出来的 apk 存在重复资源。

  1. 问题分析

对于这个问题,我们首先会想到一个解决方案,既然是去除重复资源,那我们在开始编译之前,通过脚本,拉取各业务仓库代码,跑各个仓库的资源目录,计算 md5,找出重复资源,将重复资源下沉到通用 UI 库,重新打包 UI 库,各业务仓库重新依赖 UI 库,修改各业务仓库代码中资源引用,重新提交代码,编译,打包。

这个方案会存在几个问题:

(1) 编译打包时间被拉长

(2) 业务仓库属于不同部门,各业务有自己的分支管理规则,侵入性极强

(3) 不够通用,其他 APP 不能快速接入

上述方案在现有的比心包体治理中,目前做到了统计出重复资源,推动各业务手动去修改,进度非常慢。针对这一情况,需要重新思考方案的基本原则,制定新的技术方案。

  1. 目标与挑战

3.1 业务无感知

Android 编译过程是可以自定义任务的,我们只要明确自定义任务在任务拓扑中的位置,在任务删除重复资源,重定向资源即可。

3.2 通用

在 yupaopao gradle 插件中添加任务,其他 APP 只要升级该插件并添加任务即可。

3.3 明确代价,有所取舍

人力的投入、编译时间的增加、适配难度都要控制到最小。

  1. AppPlugin 构建流程

在分析前,我们先编译一下项目,我们会看到以下输出:

图片

从这里我们可以看出,使用 gradle 构建,其实是一个个 Task 执行,这些 task 是如何被执行的,需要我们开始看 gradle 源码,那么我们从哪开始看起?

在每个 Android 项目 app 目录下的 build.gradle 中会有如下的插件引用: 图片

根据自定义插件的规则,需要定义一个 com.android.application.properties 文件,里面声明插件入口类:

图片

这里定义了入口是 AppPlugin,AppPlugin 继承自 BasePlugin:

图片

AppPlugin 没做太多事, 主要是 BasePlugin 做的:

图片

继续跟进 basePluginApply 这个方法,该方法主要做三件事,1.配置工程(configureProject),2. 配置 extension(configureExtension) 3.配置 Task(createTasks),代码如下:

图片

4.1 配置工程

configureProject这个方法主要干了三件事:

1.创建 SdkHandler 对象

图片

2.创建 AndroidBuilder 对象,这是主构建器类

图片

3.添加 gradle 生命周期监听,在 project 执行结束后清除 dex 缓存

图片

4.2 配置 Extension

4.2.1 创建了 BuildType,ProductFlavor,SigningConfig 三种类型的 container,这里的 BaseExtension 其实就是我们 build.gradle 下的 android{} DSL 闭包

图片

4.2.2 依次创建 variantFactory ,taskManager ,variantManager 。taskManager 是创建具体任务的管理类,variantManager 是创建变体的工厂类,主要是生成构建变体的对象

图片

4.2.3 为 container 配置 whenObjectAdded 回调,并且每个回调都会放置在 variantManager 中管理

图片

4.2.4 创建默认的变体,分别为 debug 和 release

图片

4.3 创建 task

在 BasePlugin 的 createTasks 方法里开始构建需要的 Task,主要是创建通用的 tasks 和 Android Tasks,因为 Android Tasks 需要依赖配置项的配置才能生成任务,所以 beforeEvaluate 跟我们编译没太大关系,重点关注 createAndroidTasks

图片

4.3.1 createTasksBeforeEvaluate

这个方法里给容器注册了一堆 Task,包括:uninstallAllTask,deviceCheckTask,connectedCheckTask,assembleAndroidTestTask 等。

4.3.2 createAndroidTasks

createAndroidTasks 这个方法主要是生成 flavor 相关数据,并根据 flavor 创建与之对应的 Task 实例并注册到 Task 当中

图片

如果变体的作用域(variantScopes)是空的,则会根据 flavor 和 dimension 创建对应的组合,存放在  flavorComboList,最后调用 createVariantDataForProductFlavors 方法, 为每个组合创建 VariantData

图片

再看一下为每个 VariantData 创建 Task 的过程,即 createTasksForVariantData 方法

图片

可以看到这个方法里创建了一些列 task,当这些个 task 执行完,整个 gradle plugin 的构建过程也结束了。

  1.  task 分析

描述完 APPPLugin 的构建过程,回到我们的重复资源删除这个问题上,我们需要找到切入口,也就是哪个任务是处理资源问题,对处理过后的产物进行解析,剔除,重定向资源文件,不难发现,是 createApkProcessResTask 这个方法里创建的 processXXXResourses 任务。

5.1 processXXXResourses 图片

最终会生成一个.ap 资源文件和 R.java 文件,同时 res 中的资源文件对应的 id 和文件路径会存在 resource.arsc 文件中,我们需要在这个产物生成后,对齐处理。

图片

  1. resource.arsc 简析

6.1 arsc 扩展文件是编译器编译 Android 应用程序代码生成 apk 文件后生成的二进制文件。  arsc 文件是一个 Android 资源表,它以表格格式包含应用程序的资源列表,这包含资源名称、属性、和 ID 信息。从整体结构上看,其结构位:资源索引头部 + 字符串资源池 + N个 package 数据块。贴一张网上图:

图片

  1. 比心 Android 去重实践

7.1 创建开关模型

图片

7.2 创建 ApkMonitorPlugin

图片

7.3 创建 properties

图片

7.4 创建 RemoveRepeatTask ,处理 .ap 和 arsc

7.4.1 在 processResource 任务结束后处理 .ap_ 文件

图片

7.4.2 处理 ap 文件过程:

  • 将 *.ap_ 文件解压到当前目录下的同名目录中
  • 获取到解压出来的 resources.arsc 文件
  • 将 resources.arsc 文件中的 chunks 拿到,并筛选出资源文件部分的 chunks,并做去重。
  • 删除旧的 resources.arsc,生成新的 resources.arsc
  • 打包中间产物生成新的 .ap_
  • 删除中间产物

下面贴一下删除重复文件和资源重定向的核心代码:

图片

  1. 收益与后续规划

8.1 成果

目前这个工具在比心 App 已经在使用,共删除重复文件 936 个,获得收益是包体降低了 3M 并且生成了无感知的工具链。

8.2 后续规划

  1. 对不重复的资源,比如 PNG 采取一定的策略进行压缩,并输出压缩日志,让业务可回归。

  2. 对资源文件引入进行管控,优先使用 webp 格式的文件。

  3. 资源路径混淆,例如将 res/drawable/bixin_logo 变为 r/d/a

9. 总结

安装包瘦身的探索还有很长的路走,本文也只是列举了一些常用的瘦身方案,对于庞大的项目除了优化外,还有做好项目之间的治理,持续对APP进行体积优化,提升用户体验。


wxg.JPG