編譯時替換資源 - Android重疊包與資源合并一見
來自: http://blog.zhaiyifan.cn/2016/02/18/android-resource-overlay/
前言
在 Android逆向分析(2) APK的打包與安裝 一文中對資源編譯過程的介紹中,筆者提到了overlay(重疊包)這個概念,一位每天都被自己帥醒的好友看了那篇東西后,來問我這個重疊包究竟是個什么東西,筆者想了想,確實這個概念有很多同學們都不甚了解,搜索了一下網上了介紹,也幾乎沒有看到任何對這個的講解,只有 老羅的博客 提到過
–mOverlay:表示當前正在編譯的資源的重疊包。重疊包是什么概念呢?假設我們正在編譯的是Package-1,這時候我們可以設置另外一個Package-2,用來告訴aapt,如果Package-2定義有和Package-1一樣的資源,那么就用定義在Package-2的資源來替換掉定義在Package-1的資源。通過這種Overlay機制,我們就可以對資源進行定制,而又不失一般性。
那我們應該怎么怎么去使用重疊包呢?它又能用在什么地方,帶來什么便利呢?
本文的測試源碼已上傳: ResourceOverlayDemo 。
aapt overlay
我們看看aapt的命令help里是怎么描述的,省略版:
Usage: aapt l[ist] [-v] [-a] file.{zip,jar,apk} List contents of Zip-compatible archive. aapt d[ump] [--values] [--include-meta-data] WHAT file.{apk} [asset [asset ...]] ... aapt p[ackage] [-d][-f][-m][-u][-v][-x][-z][-M AndroidManifest.xml] \ ... [--utf16] [--auto-add-overlay] \ ... [-S resource-sources [-S resource-sources ...]] \ [-F apk-file] [-J R-file-dir] \ ... Package the android resources. It will read assets and resources that are supplied with the -M -A -S or raw-files-dir arguments. The -J -P -F and -R options control which files are output. ... Modifiers: ... # 特別說明下,這就是前一篇我們說到的include的base set啦,比如android.jar -I add an existing package to base include set ... # overlay通過-S指定,可以指定多個目錄, -S directory in which to find resources. Multiple directories will be scanned and the first match found (left to right) will take precedence. ... # 自動添加overlays包里的資源 --auto-add-overlay Automatically add resources that are only in overlays. ...
</div>
舉個例子
aapt package \ -M AndroidManifest.xml \ -m -J gen \ -S src/com/example/res \ -S src/com/example/ui/res
</div>
假如我們有如上的aapt命令輸入,那么當 src/com/example/res 與 src/com/example/ui/res 有相同資源的時候,就會使用前者的,這里對資源替換的粒度是resource而不是文件,比如兩個文件夾的values/string.xml都有對同一個string id的描述,最后就會使用前者的字符串。
然后我們再來看看 --auto-add-overlay 有什么用,
假如我們在 src/com/example/ui/res 定義了資源string a,但是在 src/com/example/res 卻沒有這個string,那就會報錯,因為基礎包里是沒有那個資源的,這時候就需要加上 --auto-add-overlay ,于是就會自動把新的資源都添加進去。
overlay大致就是這么一回事啦。
Gradle實踐
aaptOptions
google的官方文檔簡直說了和沒說一樣。還是自己來吧,用AS的模板新建一個Testapp工程,隨便建兩個res文件夾,各放兩個strings.xml,結構為:
├── res │ ├── drawable │ ├── layout │ │ ├── activity_main.xml │ │ └── content_main.xml │ ├── menu │ │ └── menu_main.xml │ ├── mipmap-xxxhdpi │ │ └── ic_launcher.png │ └── values │ ├── colors.xml │ ├── dimens.xml │ ├── strings.xml │ └── styles.xml ├── res2 │ └── values │ └── strings.xml └── res3 └── values └── strings.xml
res2和res3分別定義了一個string hehe ,value分別為 hehe res2 和 hehe res3 。
content_main.xml的TextView使用了 hehe (原來就是那個Hello World)。當然這里as會報錯,因為res2和res3并沒有標示為資源文件夾。
在module的build.gradle里:
android { ... aaptOptions { additionalParameters '-S', '/Users/yifan/dev/github/Testapp/app/src/main/res3', '-S', '/Users/yifan/dev/github/Testapp/app/src/main/res2', '--auto-add-overlay' noCompress 'foo', 'bar' ignoreAssetsPattern '!.svn:!.git:!.ds_store:!*.scc:.*:<dir>_*:!CVS:!thumbs.db:!picasa.ini:!*~' } ... }
</div>
然后我們試圖編譯:
All input files are considered out-of-date for incremental task ':app:processDebugResources'. Starting process 'command '/Users/yifan/dev/sdk/adt-bundle-mac-sdk/build-tools/23.0.2/aapt''. Working directory: /Users/yifan/dev/github/Testapp/app Command: /Users/yifan/dev/sdk/adt-bundle-mac-sdk/build-tools/23.0.2/aapt package -f --no-crunch -I /Users/yifan/dev/sdk/adt-bundle-mac-sdk/platforms/android-23/android.jar -M /Users/yifan/dev/github/Testapp/app/build/intermediates/manifests/full/debug/AndroidManifest.xml -S /Users/yifan/dev/github/Testapp/app/build/intermediates/res/merged/debug -A /Users/yifan/dev/github/Testapp/app/build/intermediates/assets/debug -m -J /Users/yifan/dev/github/Testapp/app/build/generated/source/r/debug -F /Users/yifan/dev/github/Testapp/app/build/intermediates/res/resources-debug.ap_ --debug-mode --custom-package cn.zhaiyifan.testapp -0 apk -S /Users/yifan/dev/github/Testapp/app/src/main/res2 --output-text-symbols /Users/yifan/dev/github/Testapp/app/build/intermediates/symbols/debug Successfully started process 'command '/Users/yifan/dev/sdk/adt-bundle-mac-sdk/build-tools/23.0.2/aapt'' /Users/yifan/dev/github/Testapp/app/build/intermediates/res/merged/debug/values-af/values-af.xml:3 : AAPT: Resource at abc_action_bar_home_description appears in overlay but not in the base package; use <add-resource> to add. ... ...各種類似報錯 /usr/local/google/buildbot/src/googleplex-android/mnc-supportlib-release/frameworks/support/v7/appcompat/res/color/switch_thumb_material_light.xml:19 : AAPT: No resource found that matches the given name (at 'color' with value '@color/switch_thumb_normal_material_light'). :app:processDebugResources FAILED :app:processDebugResources (Thread[main,5,main]) completed. Took 10.493 secs.
</div>
看到 <add-resource> 這個,大概知道啥問題了…于是在 additionalParameters 最后又加上了 --auto-add-overlay ,成功編譯運行。
在屏幕中央,顯示了hehe res3,交換-S順序后則變成了hehe res2,符合我們第一節中說到的,選擇首個匹配原則。
不僅是string,anim,layout等等資源都可以使用重疊包來進行動態指定。
資源合并
和aapt的overlay有關,但使用場景略有不同,也介紹一下。
Google在Android Tools Project Site專門為此開了一個頁面: Resource Merging(資源合并) 。
在過去的編譯系統中,資源合并是通過傳給aapt一個作為重疊包的資源文件夾列表來做的,再加上–auto-add-overlay來確保在重疊包里的新資源會被自動添加(默認行為只會重載既有資源)。
基礎Gradle的編譯系統的一個目標就是提供更大的靈活性,而另一個經常并問到的功能要求則是能擁有多個資源文件夾。aapt無法去處理這個,所以新的編譯系統引進了一種新的超越aapt的合并機制,生成一個單獨的,合并的,資源文件夾并提供給aapt。這個合并機制擁有增量的優點,既因為Gradle的輸入/輸出變更檢測,又因為其實現方式(可以只使用唯一一個變更文件來做重新merge)。
合并的資源來自3種來源:
- 主資源,和main sourceSet相關聯,大多位于src/main/res
- Variant重疊包,來自Build Type和Flavor(s).
- Library項目依賴,通過它們的aar bundle提供資源。
優先級
優先級為:BuildType -> Flavor -> main -> Dependencies.
這意味著如果一個資源同時在Build Type和main存在,會使用Build Type里的。
需要注意的是合并的scope,同樣(類型,名字)的資源但標示符不同的,是分開處理的。
即如果src/main/res有:
- res/layout/foo.xml
- res/layout-land/foo.xml
而src/debug/res有: - res/layout/foo.xml
則合并后的資源文件夾會包含默認的來自src/debug/res的foo.xml,但橫屏版本則會選擇src/main/res下的。
PS: android的資源有19個維度,見 Grouping Resource Types 的Table 2,這19個維度會唯一指定1個資源(qualifier標示符)。在老羅的資源介紹博客中曾經提到過18個維度,現在變成了19是因為多了Round screen這個維度,用于描述Android Wear,添加于API 23.
處理多個資源文件夾
每個sourceSet可以定義多個資源文件夾,舉個例子:
android.sourceSets { main.res.srcDirs = ['src/main/res', 'src/main/res2'] }
這種情況下,兩個資源文件夾具有相同優先級,即如果一個資源在兩個文件夾都聲明了,合并會報錯。
Library依賴的優先級順序
根據傳遞的依賴,Library項目的實際集被工程視為一個圖,而不是平鋪的列表,然后合并機制只會處理一個平優先級列表。
如果我們考慮如下例子的依賴關系:
項目 -> A, B (意味著A的優先級高于B)
A -> C, D
B -> C
則最后的優先級list為A, D, B, C,同時保證了A和B可以重載C。
小測試
繼續在之前我們建立的工程的基礎上做個小測試吧。在sourceSet加上res2文件夾,最后build.gradle的android域如下:
android { compileSdkVersion 23 buildToolsVersion "23.0.2" defaultConfig { applicationId "cn.zhaiyifan.testapp" minSdkVersion 14 targetSdkVersion 23 versionCode 1 versionName "1.0" } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } aaptOptions { additionalParameters '-S', '/Users/yifan/dev/github/Testapp/app/src/main/res3', '-S', '/Users/yifan/dev/github/Testapp/app/src/main/res2', '--auto-add-overlay' noCompress 'foo', 'bar' ignoreAssetsPattern '!.svn:!.git:!.ds_store:!*.scc:.*:<dir>_*:!CVS:!thumbs.db:!picasa.ini:!*~' } android.sourceSets { main.res.srcDirs = ['src/main/res', 'src/main/res2'] } }
</div>
運行后發現界面顯示了 hehe res2,符合預期,因為res2已經和res合并了,所以先找到了build/intermediates/res/merged/debug下的string,沒有用res3的。
使用場景
不同的buildType、渠道下的包,使用不同的資源,做一些定制,而不用侵入代碼本身的邏輯。
總結
我們了解了Android aapt overlay的機制,以及gradle下的資源合并,并分別編寫運行了demo驗證資源生效的結果。