Android 颜色色值与 alpha 分离解决方案

1、背景

目前 Android 并不支持 xml 文件中颜色与透明度分开定义,若是想用带透明度的颜色值,只能在 colors.xml 文件中定义一个新色值。好比,有一个颜色名字叫 N900,定义以下:android

<color name="N900">#1F2329</color>
复制代码

当我须要一个 50% 透明度 N900 的颜色时,只能本身定义再一个色值:git

<color name="N900_alpha_50">#7F1F2329</color>
复制代码

因而,colors.xml 内就出现不少不规则颜色,就像下面这样子:github

不规则颜色

而且还会继续增长这些不规则颜色。当下次换颜色时,这些带透明度的颜色每个都须要更换,维护起来十分麻烦。而且,这些颜色目前所在的module已经打成aar,每次若是须要增长新的颜色,都须要从新打包aar上传,十分影响开发效率。缓存

为了解决上述问题,开发了ResKitPlugin 插件,在编译时期动态替换颜色,支持颜色与透明度分开定义。app

2、技术原理

基本思路是在aapt最终打包前,替换资源编译后生成的文件,使 aapt 最后打包时使用的资源二进制文件内的相应的颜色值已经带上了透明度。先上一张图:性能

流程图

下面详细介绍:测试

(一).相关背景知识介绍

  1. 目前咱们的资源编译使用的都是aapt2, aapt2 编译资源分两步:
  • 编译:将资源文件编译为.flat 文件
  • 连接:将.flat 文件连接为最终的二进制资源文件.ap_
  1. gradle 在编译apk 时,是执行一系列Task,而且有些Task 是有严格前后顺序的。
  2. 咱们须要关心的是其中两个和资源编译相关的Task :
  • mergeDebugResources : 这个Task 是负责收集全部的资源文件并使用aapt2编译成.flat文件,放在build/intermediates/res/merged/{*flavor*}/{buildType}目录下。
  • processDebugResources:这个Task是负责 使用aapt2 将.flat文件 连接为最终的二进制资源文件
  • mergeDebugResources 与 processDebugResource有严格的前后顺序,先执行mergeDebugResources,后执行 processDebugResource
  1. gradle 的API支持 “改变Task的执行顺序" 的操做
  2. MergeResources 类的 computeResourceSetList 方法能够获取编译要用的所有 res 路径
  3. mergeDebugResources 执行后,全部res/values目录下的内容,都会合并到一个文件内,放在build/intermediates/incremental/merge${variantName}Resources/merged.dir/values/values.xml内,这里面包括了定义的颜色资源。

(二).实现步骤

  1. 首先,定义一个本身的Task,叫作handleAlphaColorTask,负责修改.flat文件
  2. 经过gradle 的 API , 将handleAlphaColorTask 插入 mergeDebugResources 与 processDebugResources之间,这一步执行后,gradle 的编译Task 的 调用顺序以下:

流程图

  1. 在handleAlphaColorTask内处理mergeDebugResources生成的文件,使其内部的颜色属性带上了透明度,例子以下:

最开始,drawable_a.xml.flat 文件是由(代码-1)编译生成:gradle

<solid android:color="@color/N900" android:alpha="0.5" />
复制代码

经过咱们的处理,drawable_a.xml.flat 文件 变成了由(代码-2)编译生成:优化

<solid android:color="@color/reskit_tmp_color_N900_alpha_0_5" />
复制代码

对于硬编码的颜色,会直接进行以下转换:ui

<solid android:color="#1F2329" android:alpha="0.5" />
              |
             \|/
  <solid android:color="#7F1F2329"/>
复制代码

经过这样的处理,咱们的颜色在运行时就拥有了透明度。下面介绍具体处理的步骤。

(三). 颜色转换的具体实现方式

经过computeResourceSetList去获取到全部参与编译的资源文件,而后修改源码,编译生成新的.flat文件,并替换原来的.flat文件。分为如下几步:

  1. 经过反射调用MergeResources 的 computeResourceSetList 方法,获取参与编译的所有 res 文件夹路径,包括aar内的。
  2. build/intermediates/incremental/merge${variantName}Resources/merged.dir/values/values.xml内挑出全部的颜色定义并生成colors.xml,为后续根据id找颜色值提供基础。
  3. 遍历全部 res 文件夹下的xml 文件。
  4. 经过xml 解析,识别 颜色属性和与之配对的透明度属性,并经过计算生成最终的颜色值。若是颜色属性是引用属性,则去colors.xml 根据引用id 找到对应的色值,而后计算出最终颜色。计算出最终颜色后,须要替换颜色属性,进行替换时有如下两个策略
    • 原颜色属性是硬编码颜色时,如 android:color ="#1F2329",则直接修改值便可。
    • 原颜色属性是引用颜色时,如android:color="@color/lkui_N900",会生成一个新key,而后将其替换为新key,并把这个新key与颜色的对应关系存在一个Map里,待新key 所有生成后,统一将新key 与颜色的对应关系写入build/intermediates/incremental/merge${variantName}Resources/merged.dir/values/values.xml文件,参与后续编译。
  5. 遍历期间,将须要修改的文件,则保存下来,放到一个Map里,这么作的目的是当出现同名资源时,提供筛选资源的数据。Map的定义以下:
Map<String, Map<String, String>>

资源文件的父文的名字 + “/” + 资源文件的名字  :  [  原始文件全路径  : 处理了alpha 后的新文件的全路径  ]

举例:
drawable/aab.xml : [/Users/guoxiao/ResPluginDemo/app/src/main/res/drawable/aab.xml  :  /Users/guoxiao/ResPluginDemo/app/build/coloralpha/res/drawable/aab.xml]
复制代码
  1. 遍历完成后,咱们就获得了一个Map和在指定目录下合并了alpha属性的资源 源码文件,接下来,须要处理重名资源
  2. 咱们须要知道在intermediates/res/merged/{*flavor*}/{buildType}目录下,对于重名文件来讲,系统究竟使用了哪一个文件去参与编译的。这里使用的方法是:
    • 在第5步得到的map里,能够知道有几个重名文件,全路径是什么,对应的新的修改后的文件是什么。
    • 编译每个重名的原文件,生成.flat文件,而后和intermediates/res/merged/{flavor}/{buildType}目录下的同名文件作md5比较,比较结果相同的,说明找到了系统编译使用的文件
    • 找到了系统编译是用的文件,咱们就知道了最后咱们应该编译哪一个新的修改了属性的文件去替换原.flat文件
  3. 重名文件处理完成后,就能够编译新的修改后的文件,产生新的.flat 文件
  4. 用新的.flat 文件 替换老的.flat 文件。
  5. 若是本次编译没有修改资源文件,即intermediates/res/merged/{flavor}/{buildType}目录下的文件的md5和上次一致,则直接使用上次的缓存的.flat文件进行替换

三.对编译性能的影响

咱们缓存了上次颜色处理获得的.flat文件,对于本次颜色处理:

  1. 未命中缓存时:
    • 在CI 平台上测试:替换了78个.flat文件,用时13991ms,其中,替换资源文件用时10135ms(未指定文件夹过滤,此时是最坏状况,全量遍历), 编译用时352ms。一个文件的编译时间4.5ms左右。
    • 本地测试:替换了78个.flat文件,指定文件夹过滤 ,耗时6192ms,替换颜色3995m,编译255ms;未指定文件夹过滤,耗时9179ms左右, 替换颜色 7392 ms, 编译 184ms。
  2. 命中缓存时,耗时500ms左右

四.探索的过程

目前的实现并非最初的方案,测试时,替换7个文件,耗时从30s ,到20s, 最终优化到如今的5s左右。下面介绍这期间经历的几个方案:

  1. 最初方案是在processDebugResources 后插入颜色处理Task:
    1. 获取到系统链接后的资源文件包.ap_
    2. 经过ApkTool反编译.ap_文件,获得源码
    3. 修改源码
    4. 从新编译新修改的源码,得到新的.flat文件
    5. 将新的.flat文件与系统编译产生的.flat文件一块儿参与连接,生成最终的二进制文件
    6. 用新得到的由新的源文件编译连接而产生的二进制文件提替换.ap_文件内的文件
    7. 处理完成

这种方式,一次连接耗时7s左右,并且为了连接,还须要作一些压缩与解压的文件操做,压缩所有.flat文件须要7秒多,反编译.ap_又须要7秒多,最终一次颜色处理下来,耗时30s左右。

  1. 第二种方案,在mergeDebugResources 和 processDebugResources之间插入颜色处理Task:
    1. 获取mergeDebugResources 后生成的.flat文件,连接这些.flat文件,生成tmp.ap_包
    2. 反编译tmp.ap_,获得源码
    3. 修改源码
    4. 从新编译修改了源码的文件,获得.flat文件
    5. 替换mergeDebugResources 生成的同名.flat文件
    6. 处理完成

这种方式,能够去除对系统生成的.ap_文件的修改,耗时20s左右。

​ 前两种方案耗时,主要是进行连接和反编译,从而获得源码。因而思考,有没有可能不经过连接和反编译的方式来获得源码,最终有个方案3。

  1. 第三种方案,依然是 在mergeDebugResources 和 processDebugResources之间插入颜色处理Task,区别于第二种方案,是经过反射MergeResource的 computeResourceSetList 获得全部参与编译的资源文件(/res):
    1. 经过反射调用computeResourceSetList 得到全部的资源目录
    2. 遍历资源目录,若是须要处理颜色,则拷贝一份新文件,而后处理并保存到指定目录build/coloralpha/res
    3. 处理完成后,编译build/coloralpha/res生成新的.flat文件
    4. 用新的.flat文件替换系统编译生成的同名.flat文件
    5. 处理完成

最终耗时5s 左右。

处理完成后,系统的processDebugResources就会使用咱们处理过的.flat文件。

五.特别注意的坑

mergeResource Task 若使用了 gradle 的构建缓存(运行该Task 会输出 FROM_CACHE) ,会缺失这个Task的中间产物,即merged.dir文件夹为空。

针对这种状况,咱们每次在MergeResource执行前判断是否有merged.dir,若没有,不让它走 FROM_CACHE。

具体作法是:临时生成一个资源文件,致使缓存失效,这样就会触发mergeResources走一遍,而后 在mergeResource以后删除咱们临时生成的资源文件。

六.一些想法

因为咱们能够拿到参与编译的全部资源文件,也能够修改替换系统编译产生的文件。这两个能力,提供了巨大的想象空间,如:

  1. 能够作全局资源查重,包括aar内的资源
  2. 能够作资源压缩,如压缩图片
  3. 能够支持更多的相似color,alpha这样的组合属性的自定义
  4. 能够根据编译环境修改string.xml 内容
  5. 编译过程当中自动收集生成皮肤包

文章做者:国霄(EE Lark Android 团队)

邀请优秀的人一块儿作有挑战的事儿!字节跳动效率工程团队研发职位招聘,想成为技术大牛的伙伴快点进来看!职位介绍

相关文章
相关标签/搜索