Android 顏色色值與 alpha 分離解決方案

1、背景

目前 Android 並不支持 xml 文件中顏色與透明度分開定義,若是想用帶透明度的顏色值,只能在 colors.xml 文件中定義一個新色值。好比,有一個顏色名字叫 N900,定義以下:android

<color name="N900">#1F2329</color>複製代碼

當我須要一個 50% 透明度 N900 的顏色時,只能本身定義再一個色值:緩存

<color name="N900_alpha_50">#7F1F2329</color>複製代碼

因而,colors.xml 內就出現不少不規則顏色,就像下面這樣子:bash

而且還會繼續增長這些不規則顏色。當下次換顏色時,這些帶透明度的顏色每個都須要更換,維護起來十分麻煩。而且,這些顏色目前所在的module已經打成aar,每次若是須要增長新的顏色,都須要從新打包aar上傳,十分影響開發效率。app

爲了解決上述問題,開發了ResKitPlugin 插件,在編譯時期動態替換顏色,支持顏色與透明度分開定義。性能

2、技術原理

基本思路是在aapt最終打包前,替換資源編譯後生成的文件,使 aapt 最後打包時使用的資源二進制文件內的相應的顏色值已經帶上了透明度。先上一張圖:學習

下面詳細介紹:測試

(一).相關背景知識介紹

  1. 目前咱們的資源編譯使用的都是aapt2, aapt2 編譯資源分兩步:gradle

  • 編譯:將資源文件編譯爲.flat 文件優化

  • 連接:將.flat 文件連接爲最終的二進制資源文件.ap_ui

  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)編譯生成:

<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" />複製代碼

對於硬編碼的顏色,會直接進行以下轉換:

<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. 編譯過程當中自動收集生成皮膚包

最後

漫漫開發之路,咱們只是其中的一小部分……只有不斷的學習、進階,纔是咱們的出路!纔跟得上時代的進步!我花一個月的時間收錄整理了一套知識體系,若是有想法深刻的系統化的去學習的,能夠點擊傳送門:傳送門會把我收錄整理的資料都送給你們,幫助你們更快的進階。
圖片描述

相關文章
相關標籤/搜索