前言
之前写过一篇aapt2适配之资源id固定,该文章介绍了如何使用aapt2固定资源id,其实这篇文章是对该文章的一点补充,主要介绍如何在固定id的同时,将该资源进行导出,打上public标记,供其他资源进行引用。整个问题的解决方案断断续续差不多思考了一个来月,现将解决方法简单介绍一下。
从aapt2资源id固定说起
首先来回顾一下aapt2如何将资源id符号表导出,使用–emit-ids参数指定导出文件即可
1
2
3
4
5
|
android {
aaptOptions {
additionalParameters
"--emit-ids",
"${project.file('public.txt')}"
}
}
|
以及符号表导出后,如何使用导出的符号表进行资源id的固定,使用–stable-ids参数,指定导入文件即可
1
2
3
4
5
|
android {
aaptOptions {
additionalParameters
"--stable-ids",
"${project.file('public.txt')}"
}
}
|
执行./gradlew assembleDebug编译后,看下产出的resources_debug.ap_文件,看下arsc中对应的资源是否打上了PUBLIC导出标记
1
|
aapt2
dump .
/app/build
/intermediates/res
/resources-debug.ap_
|
执行命令后输出如下内容
1
2
3
4
5
6
7
8
9
|
Package
name=io.github.lizhangqu.aapt2
id=7f
type layout
id=1
entryCount=1
spec
resource 0x7f010090 io.github.lizhangqu.aapt2:layout/activity_main
() (file) res/layout/activity_main.xml
type style
id=2
entryCount=1
spec
resource 0x7f020080 io.github.lizhangqu.aapt2:style/AppTheme
() (style)
|
可以看出,arsc中对应的资源并没有打上PUBLIC标记。问题就出在这,我们明明指定了导入资源符号表,为什么没有PUBLIC标记呢?
从aapt public.xml说起
回顾一下aapt是如何添加PUBLIC标记的,其实这事和android gradle plugin有关,在android gradle plugin 1.3以下的版本,我们可以直接往src/main/res/values目录下添加public.xml文件,该文件会自动参与编译,但是不幸的是在1.3以上版本,所有public类型的资源全都会被android gradle plugin过滤掉,因此正常的途径我们是无法让public.xml参与编译的。
所以在aapt的时候,我们在在mergeResources任务最后,将public.xml文件拷贝到build目录下merge完毕的res目录下,即build/intermediates/res/merged/目录,然后gradle在执行processResources任务时会将我们拷贝进去的文件与其他资源文件一同参与编译,于是就可以为那些资源打上PUBLIC标记,并进行id的固定。
那么在aapt2中,由于aapt2提供了一种固定资源id的方式,最开始,个人以为这种方式和aapt的public.xml作用是一样的,在经过严格的测试后发现其实并不是想象的那么简单,它只是固定资源id的一种方式,但是并不包括添加资源PUBLIC标记,因此aapt2的public.txt不等于aapt的public.xml,在aapt2中如果要添加PUBLIC标记,其实还是得另寻其他途径。
通过查看aapt2的源码发现,资源是否属于public类型,在资源文件compile为flat文件的时候就已经决定了,也就是说必须主动声明public类型的资源,这个资源才会被打上PUBLIC标记,所以问题就变成了和aapt一样,只要在mergeResource任务的最后,将public.xml拷贝到mergeResource任务的输出文件夹即可。但是有一个棘手的问题,就是aapt2的mergeResource任务的输出文件不是原始资源文件,而是经过编译后的flat文件,所以最终的问题变成了如何将public.xml文件编译为flat文件。
由于最开始没有想到这种方式,一直纠结于怎么修改aapt2的源码,来达到添加PUBLIC标记的效果,后来不经意间想到这种方式,发现之前想复杂了。这个问题其实非常简单,只需要获取aapt2可执行文件,自己调用一下compile命令,传递相关参数执行下资源编译步骤,然后将编译后的文件拷贝到mergeResource任务的输出文件夹,参考这篇文章aapt2资源compile过程可以获得aapt2资源编译的命令行参数及使用方式。那么gradle代码怎么实现呢?也很简单
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
project.afterEvaluate {
def android = project.getExtensions().findByName('android')
android.getApplicationVariants().all {
def variant ->
def mergeResourceTask = project.tasks.findByName(
"merge${variant.getName().capitalize()}Resources")
if (mergeResourceTask) {
mergeResourceTask.doLast {
def variantData = variant.getMetaClass().getProperty(variant, 'variantData')
def mBuildToolInfo = variantData.getScope().getGlobalScope().getAndroidBuilder().getTargetInfo().getBuildTools()
//buildTools下的所有可执行文件都在这个map里
Map<BuildToolInfo.PathId, String> mPaths = mBuildToolInfo.getMetaClass().getProperty(mBuildToolInfo,
"mPaths") as Map<BuildToolInfo.PathId, String>
project.exec(new Action<ExecSpec>() {
@Override
void execute(ExecSpec execSpec) {
//拼接aapt2 compile参数
execSpec.executable
"${mPaths.get(BuildToolInfo.PathId.AAPT2)}"
execSpec.args(
"compile")
execSpec.args(
"--legacy")
execSpec.args(
"-o")
execSpec.args(
"${mergeResourceTask.outputDir}")
execSpec.args(
"${project.file('src/main/res/values/public.xml')}")
}
})
}
}
}
}
|
然后将public.xml放在app/src/main/res/values/目录下,编译一下,然后用aapt2 dump命令查看一下产出的arsc文件
1
2
3
4
5
6
7
8
9
|
Package
name=io.github.lizhangqu.aapt2
id=7f
type layout
id=1
entryCount=1
spec
resource 0x7f010090 io.github.lizhangqu.aapt2:layout/activity_main PUBLIC
() (file) res/layout/activity_main.xml
type style
id=2
entryCount=1
spec
resource 0x7f020080 io.github.lizhangqu.aapt2:style/AppTheme PUBLIC
() (style)
|
可以看到,资源名后面多了一个PUBLIC字符,也就是说该资源被打上PUBLIC标记了,对于其他项目中的资源,可以在aapt2链接的时候直接使用-I参数引用该arsc,就和android.jar的引用是一样的,使用@包名:资源类型/资源名进行引用,如
1
|
@io
.github
.lizhangqu
.aapt2:style/AppTheme
|
到这里为止,其实已经解决了PUBLIC标记的问题,但是还不够完美
自动转换public.txt文件为public.xml文件
上面虽然解决了PUBLIC标记的问题,但是还得自己维护app/src/main/res/values/public.xml文件,由于aapt2自己会导出一份public.txt文件,因此可以利用这份文件,在编译过程中将其自动转为public.xml文件,就不需要我们自己维护public.xml文件。
这个问题其实说简单也很简单,只是有一些细节需要处理
- public.txt中存在styleable类型资源,public.xml中不存在,因此转换过程中如果遇到styleable类型,需要忽略
- vector矢量图资源如果存在内部资源,也需要忽略,在aapt2中,它的名字是以$开头,然后是主资源名,紧跟着__数字递增索引,这些资源外部是无法引用到的,只需要固定id,不需要添加PUBLIC标记,并且$符号在public.xml中是非法的,因此忽略它即可
- 由于aapt2有资源id的固定方式,因此转换过程中可直接丢掉id,简单声明即可
- aapt2编译的public.xml文件的上级目录必须是values文件夹,否则编译过程会报非法路径
所以整个过程就变成了读取public.txt的每一行,正则匹配资源类型,资源名,资源id,如果是内部资源和styleable类型资源,直接无视,否则写入public.xml文件,写入只需要写入资源类型和资源名即可,资源id按需选择,所以最终的代码抽象为如下函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
|
/**
* 转换publicTxt为publicXml
*/
@SuppressWarnings(
"GrMethodMayBeStatic")
void convertPublicTxtToPublicXml(
File publicTxtFile,
File publicXmlFile,
boolean withId) {
if (publicTxtFile ==
null || publicXmlFile ==
null || !publicTxtFile.exists() || !publicTxtFile.isFile()) {
throw
new GradleException(
"publicTxtFile ${publicTxtFile} is not exist or not a file")
}
GFileUtils.deleteQuietly(publicXmlFile)
GFileUtils.mkdirs(publicXmlFile.getParentFile())
GFileUtils.touch(publicXmlFile)
project.logger.info
"convert publicTxtFile ${publicTxtFile} to publicXmlFile ${publicXmlFile}"
publicXmlFile.
append(
"<!-- AUTO-GENERATED FILE. DO NOT MODIFY -->")
publicXmlFile.
append(
"\n")
publicXmlFile.
append(
"<resources>")
publicXmlFile.
append(
"\n")
Pattern linePattern = Pattern.
compile(
".*?:(.*?)/(.*?)\\s+=\\s+(.*?)")
publicTxtFile.
eachLine {
def line ->
Matcher matcher = linePattern.matcher(line)
if (matcher.matches() && matcher.groupCount() ==
3) {
String resType = matcher.
group(
1)
String resName = matcher.
group(
2)
if (resName.startsWith(
'$')) {
project.logger.info
"ignore to public res ${resName} because it's a nested resource"
}
else
if (resType.equalsIgnoreCase(
"styleable")) {
project.logger.info
"ignore to public res ${resName} because it's a styleable resource"
}
else {
if (withId) {
publicXmlFile.
append(
"\t<public type=\"${resType}\" name=\"${resName}\" id=\"${matcher.group(3)}\" />\n")
}
else {
publicXmlFile.
append(
"\t<public type=\"${resType}\" name=\"${resName}\" />\n")
}
}
}
}
publicXmlFile.
append(
"</resources>")
}
|
最开始的那段代码就可以优化为如下代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
project.afterEvaluate {
def android =
project.getExtensions().findByName(
'android')
android.getApplicationVariants().all {
def variant ->
def mergeResourceTask =
project.tasks.findByName(
"merge${variant.getName().capitalize()}Resources")
if (mergeResourceTask) {
mergeResourceTask.
doLast {
//目标转换文件,注意public.xml上级目录必须带values目录,否则aapt2执行时会报非法文件路径
File publicXmlFile =
new
File(
project.buildDir,
"intermediates/res/public/${variant.getDirName()}/values/public.xml")
//转换public.txt文件为publicXml文件
convertPublicTxtToPublicXml(
project.
file(
'public.txt'), publicXmlFile,
false)
def variantData = variant.getMetaClass().getProperty(variant,
'variantData')
def mBuildToolInfo = variantData.getScope().getGlobalScope().getAndroidBuilder().getTargetInfo().getBuildTools()
Map<BuildToolInfo.PathId, String> mPaths = mBuildToolInfo.getMetaClass().getProperty(mBuildToolInfo,
"mPaths") as Map<BuildToolInfo.PathId, String>
project.exec(
new Action<ExecSpec>() {
@Override
void execute(ExecSpec execSpec) {
execSpec.executable
"${mPaths.get(BuildToolInfo.PathId.AAPT2)}"
execSpec.args(
"compile")
execSpec.args(
"--legacy")
execSpec.args(
"-o")
execSpec.args(
"${mergeResourceTask.outputDir}")
execSpec.args(
"${publicXmlFile}")
}
})
}
}
}
}
|
总结
有时候,解决问题一种思路行不通可以换一种方式,说不定就会柳暗花明呢!