- 前言
在 Android 包体积治理的过程中,对于资源文件的治理一直是很重要的一部分,在开发业务的过程中,必要资源的压缩是资源治理的前提条件,但在后续开发过程中,如何进行长期的治理、预警、自动化是值得深入探讨的问题。本篇文章从资源文件治理中的重复资源剔除角度出发,抛砖引玉。
比心 Android 项目采用的是组件化的架构,多仓模式,各业务 module 不存在依赖关系,所以没办法直接访问,还有一些历史原因,部分资源文件也不在 UI 库中,业务开发过程中,会出现一个问题“A业务的同学直接 copy B 业务模块的资源文件,修改文件名后直接使用”,最终导致 apk 中存在多个相同文件,仅仅文件名不同而已,最终导致打出来的 apk 存在重复资源。
- 问题分析
对于这个问题,我们首先会想到一个解决方案,既然是去除重复资源,那我们在开始编译之前,通过脚本,拉取各业务仓库代码,跑各个仓库的资源目录,计算 md5,找出重复资源,将重复资源下沉到通用 UI 库,重新打包 UI 库,各业务仓库重新依赖 UI 库,修改各业务仓库代码中资源引用,重新提交代码,编译,打包。
这个方案会存在几个问题:
(1) 编译打包时间被拉长
(2) 业务仓库属于不同部门,各业务有自己的分支管理规则,侵入性极强
(3) 不够通用,其他 APP 不能快速接入
上述方案在现有的比心包体治理中,目前做到了统计出重复资源,推动各业务手动去修改,进度非常慢。针对这一情况,需要重新思考方案的基本原则,制定新的技术方案。
- 目标与挑战
3.1 业务无感知
Android 编译过程是可以自定义任务的,我们只要明确自定义任务在任务拓扑中的位置,在任务删除重复资源,重定向资源即可。
3.2 通用
在 yupaopao gradle 插件中添加任务,其他 APP 只要升级该插件并添加任务即可。
3.3 明确代价,有所取舍
人力的投入、编译时间的增加、适配难度都要控制到最小。
- 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 的构建过程也结束了。
- task 分析
描述完 APPPLugin 的构建过程,回到我们的重复资源删除这个问题上,我们需要找到切入口,也就是哪个任务是处理资源问题,对处理过后的产物进行解析,剔除,重定向资源文件,不难发现,是 createApkProcessResTask 这个方法里创建的 processXXXResourses 任务。
5.1 processXXXResourses
最终会生成一个.ap 资源文件和 R.java 文件,同时 res 中的资源文件对应的 id 和文件路径会存在 resource.arsc 文件中,我们需要在这个产物生成后,对齐处理。
- resource.arsc 简析
6.1 arsc 扩展文件是编译器编译 Android 应用程序代码生成 apk 文件后生成的二进制文件。 arsc 文件是一个 Android 资源表,它以表格格式包含应用程序的资源列表,这包含资源名称、属性、和 ID 信息。从整体结构上看,其结构位:资源索引头部 + 字符串资源池 + N个 package 数据块。贴一张网上图:
- 比心 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_
- 删除中间产物
下面贴一下删除重复文件和资源重定向的核心代码:
- 收益与后续规划
8.1 成果
目前这个工具在比心 App 已经在使用,共删除重复文件 936 个,获得收益是包体降低了 3M 并且生成了无感知的工具链。
8.2 后续规划
-
对不重复的资源,比如 PNG 采取一定的策略进行压缩,并输出压缩日志,让业务可回归。
-
对资源文件引入进行管控,优先使用 webp 格式的文件。
-
资源路径混淆,例如将 res/drawable/bixin_logo 变为 r/d/a
9. 总结
安装包瘦身的探索还有很长的路走,本文也只是列举了一些常用的瘦身方案,对于庞大的项目除了优化外,还有做好项目之间的治理,持续对APP进行体积优化,提升用户体验。