欢迎大家加入QQ群一起讨论: 489873144(android格调小窝)
我的github地址:https://github.com/jeasonlzy
概念
假设我们正在编译的是Package-1,这时候我们可以设置另外一个Package-2,用来告诉aapt,如果Package-2定义有和Package-1一样的资源,那么就用定义在Package-2的资源来替换掉定义在Package-1的资源。通过这种Overlay机制,我们就可以对资源进行定制,而又不失一般性。
aapt overlay
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.
...
举个例子
aapt package \
-M AndroidManifest.xml \
-m -J gen \
-S ./src/main/res \
-S ./src/main/res2
假如我们有如上的aapt命令输入,那么当 src/main/res 与 src/main/res2 有相同资源的时候,就会使用前者的,这里对资源替换的粒度是resource而不是文件,比如两个文件夹的values/string.xml都有对同一个string id的描述,最后就会使用前者的字符串。
然后我们再来看看 –auto-add-overlay 有什么用,
假如我们在 src/main/res2 定义了资源string a,但是在 src/main/res 却没有这个string,那就会报错,因为基础包里是没有那个资源的,这时候就需要加上 –auto-add-overlay ,于是就会自动把新的资源都添加进去。
aaptOptions
在工程src目录下新建两个目录,src2和src3,结构如下
├── res
│ └── values
│ ├── strings.xml
├── res2
│ └── values
│ └── strings.xml
└── res3
└── values
└── strings.xml
res2和res3分别定义了一个string,id=test ,value分别为test1,test2,test3
然后在代码中使用getString(R.string.test)
这时as会报错,因为res2和res3并没有标示为资源文件夹。
在module的build.gradle里:
android {
...
aaptOptions {
additionalParameters '--auto-add-overlay',
'-S', './src/main/res3',
'-S', './src/main/res2'
noCompress 'foo', 'bar'
ignoreAssetsPattern '!.svn:!.git:!.ds_store:!*.scc:.*:<dir>_*:!CVS:!thumbs.db:!picasa.ini:!*~'
}
...
}
重新编译运行,成功,打印结果为test3,如果我们对上面的-S参数进行交换,打印结果就变成了test2,这就证明了资源重叠包的优先级是由左至右的
资源合并
在过去的编译系统中,资源合并是通过传给aapt一个作为重叠包的资源文件夹列表来做的,再加上–auto-add-overlay来确保在重叠包里的新资源会被自动添加(默认行为只会重载既有资源)。
基础Gradle的编译系统的一个目标就是提供更大的灵活性,而另一个经常并问到的功能要求则是能拥有多个资源文件夹。aapt无法去处理这个,所以新的编译系统引进了一种新的超越aapt的合并机制,生成一个单独的,合并的,资源文件夹并提供给aapt。这个合并机制拥有增量的优点,既因为Gradle的输入/输出变更检测,又因为其实现方式(可以只使用唯一一个变更文件来做重新merge)。
合并的资源来自3种来源:
- 主资源,和main sourceSet相关联,大多位于src/main/res
- Variant重叠包,来自Build Type和Flavor(s).
- Library项目依赖,通过它们的aar bundle提供资源。
以下大部分来自于文档:Add App Resources
When all resources from each source set or library are unique, they’re all added into the final APK. A resource is considered unique if it’s file name is unique within both its resource type directory and the resource qualifier (if defined).
If there exist two or more matching versions of the same resource, then only one version is included in the final APK. The build tools select which version to keep based on the following priority order (highest priority on the left):
优先级
build variant > build type > product flavor > main source set > library 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个维度http://blog.csdn.net/luoshengyang/article/details/8738877,现在变成了19是因为多了Round screen这个维度,用于描述Android Wear,添加于API 23.
https://android.googlesource.com/platform/frameworks/native/+/jb-dev/libs/utils/README
处理多个资源文件夹
每个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 25
buildToolsVersion "25.0.1"
defaultConfig {
minSdkVersion 14
targetSdkVersion 25
versionCode 1
versionName "1.0"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
sourceSets {
main.res.srcDirs = ['src/main/res', 'src/main/res2']
}
aaptOptions {
additionalParameters '--auto-add-overlay',
'-S', './src/main/res3',
'-S', './src/main/res2'
noCompress 'foo', 'bar'
ignoreAssetsPattern '!.svn:!.git:!.ds_store:!*.scc:.*:<dir>_*:!CVS:!thumbs.db:!picasa.ini:!*~'
}
}
运行后发现界面显示了 test2,符合预期,因为res2已经和res合并了,所以先找到了
build/intermediates/res/merged/debug/values/values.xml
下的string,没有用res3的。
可以执行 gradle assembleDebug -d > a.txt
命令将log输出到文件中,搜索overlay,发现以下log
合并后的资源文件在第一个 -S 已经能找到资源id,后续的 -S将不起作用
overlay的好处就是,不同的buildType、渠道下的包,使用不同的资源,做一些定制,而不用侵入代码本身的逻辑。
我们了解了Android aapt overlay的机制,以及gradle下的资源合并,并分别编写运行了demo验证资源生效的结果。