整片文章是围绕 tinker
的 TinkerResourceIdTask
里的知识点进行扩展的。
aapt
和aapt2
的差异(运行环境和运行结果);- 资源
id
的固定; - 进行
PUBLIC
的标记;
aapt
运行环境为 gradle:2.2.0
和 gradle-wrapper:3.4.1
aapt2
运行环境为 gradle:3.3.2
和 gradle-wrapper:5.6.2
android-aapt-sample 项目是我自己的实验样例。有 aapt
和 aapt2
两个分支,分别对应其实现。
从 Android Studio 3.0
开始,google
默认开启了 aapt2
作为资源编译的编译器,aapt2
的出现,为资源的增量编译提供了支持。当然使用过程中也会遇到一些问题,我们可以通过在 gradle.properties 中配置 android.enableAapt2=false 来关闭 aapt2
。
Android
天生为兼容各种各样不同的设备做了相当多的工作,比如屏幕大小、国际化、键盘、像素密度等等,我们能为各种各样特定的场景下使用特定的资源做兼容而不用改动一行代码,假设我们为各种各样不同的场景适配了不同的资源,如何能快速的应用上这些资源呢?Android
为我们提供了 R
这个类,指定了一个资源的索引(id
),然后我们只需要告诉系统在不同的业务场景下,使用对应的资源就好了,至于具体是指定资源里面的哪一个具体文件,由系统根据开发者的配置决定。
在这种场景下,假设我们给定的 id
是 x
值,那么当下业务需要使用这个资源的时候,手机的状态就是 y
值,有了(x,y
),在一个表里面就能迅速的定位到资源文件的具体路径了。这个表就是 resources.arsc
,它是从 aapt
编译出来的。
其实二进制的资源(比如图片)是不需要编译的,只不过这个“编译”的行为,是为了生成 resources.arsc
以及对 xml
文件进行二进制化等操作,resources.arsc
是上面说的表,xml
的二进制化是为了系统读取上性能更好。AssetManager
在我们调用 R
相关的 id
的时候,就会在这个表里面找到对应的文件,读取出来。
Gradle
在编译资源的过程中,就是调用的这些aapt2命令,传的参数也在这个文档里都介绍了,只不过对开发者隐藏起了调用细节。
aapt2
主要分两步,一步叫 compile
,一步叫 link
。
创建一个空工程:只写了两个 xml
,分别是 AndroidManifest.xml
和 activity_main.xml
。
mkdir compiled
aapt2 compile src/main/res/layout/activity_main.xml -o compiled/
在 compiled
文件夹中,生成了 layout_activity_main.xml.flat
这个文件,它是 aapt2
特有的,aapt
没有(aapt
拷贝的是源文件),aapt2
用它能进行增量编译。如果我们有很多的文件的话,需要依次调用 compile
才行,其实这里也可以使用 –dir
参数,只不过这个参数就没有增量编译的效果了。也就是说,当传递整个目录时,即使只有一个资源发生了变化,AAPT2
也会重新编译目录中的所有文件。
link
的工作量比 compile
要多一点,此处的输入是多个 flat
的文件 和 AndroidManifest.xml
,外部资源,输出是只包含资源的 apk
和 R.java
。命令如下:
aapt2 link -o out.apk \
-I $ANDROID_HOME/platforms/android-28/android.jar \
compiled/layout_activity_main.xml.flat \
--java src/main/java \
--manifest src/main/AndroidManifest.xml
- 第二行
-I
是import
外部资源,此处主要是android
命名空间下定义的一些属性,我们平常使用的@android:xxx
都是放在这个jar
里面,其实我们也可以提供自己的资源供别人链接; - 第三行是输入的
flat
文件,如果有多个,直接在后面拼接即可; - 第四行是
R.java
生成的目录; - 第五行是指定
AndroidManifest.xml
;
Link
完成后会生成out.apk
和R.java
,out.apk
中包含了一个resources.arsc
文件。只带资源文件的可以用后缀名.ap_
。
除了是用 Android Studio
去查看 resources.arsc
,还可以直接使用 aapt2 dump apk
信息的方式来查看资源相关的 ID
和状态:
aapt2 dump out.apk
输出的结果如下:
Binary APK
Package name=com.geminiwen.hello id=7f
type layout id=01 entryCount=1
resource 0x7f010000 layout/activity_main
() (file) res/layout/activity_main.xml type=XML
可以看到 layout/activity_main
对应的 ID
是 0x7f010000
。
android.jar
只是一个编译用的桩,真正执行的时候,Android OS
提供了一个运行时的库(framework.jar
)。android.jar
很像一个 apk
,只不过它存在的是 class
文件,然后存在一个 AndroidManifest.xml
和 resources.arsc
。这就意味着我们也可以对它用aapt2 dump
,执行如下命令:
aapt2 dump $ANDROID_HOME/platforms/android-28/android.jar > test.out
得到很多类似如下的输出:
resource 0x010a0000 anim/fade_in PUBLIC
() (file) res/anim/fade_in.xml type=XML
resource 0x010a0001 anim/fade_out PUBLIC
() (file) res/anim/fade_out.xml type=XML
resource 0x010a0002 anim/slide_in_left PUBLIC
() (file) res/anim/slide_in_left.xml type=XML
resource 0x010a0003 anim/slide_out_right PUBLIC
() (file) res/anim/slide_out_right.xml type=XML
它多了一些PUBLIC
的字段,一个 apk
文件里面的资源,如果被加上这个标记的话,就能被其他 apk
所引用,引用方式是@包名:类型/名字
,例如:@android:color/red
。
如果我们想要提供我们的资源,那么首先为我们的资源打上 PUBLIC
的标记,然后在 xml
中引用你的包名,比如:@com.gemini.app:color/red
就能引用到你定义的 color/red
了,如果你不指定包名,默认是自己。
至于 AAPT2
如何生成 PUBLIC
,感兴趣的可以接着阅读本文。
ids.xml
:为应用的相关资源提供唯一的资源id
。id
是为了获得xml
中的对象需要的参数,也就是 Object = findViewById(R.id.id_name);
中的id_name
。
这些值可以在代码中用android.R.id
引用到。
若在ids.xml
中定义了ID,则在layout
中可如下定义@id/price_edit
,否则@+id/price_edit
。
优点
- 命名方便,我们可以把一些特定的控件先命好名,在使用的时候直接引用
id
即可,省去了一个命名环节。 - 优化编译效率:
- 添加
id
后会在R.java
中生成; - 使用
ids.xml
统一管理,一次性编译即可多次使用. 但使用"@+id/btn_next"
的形式,每次文件保存(Ctrl+s
)后R.java
都会重新检测,如果存在该id
则不生成,如果不存在就需要添加该id
。故编译效率降低。
- 添加
ids.xml
文件内容:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<item name="forecast_list" type="id"/>
<!-- <item name="app_name" type="string" />-->
</resources>
也许有人很好奇上面有一行被注释的代码,打开注释会发现编译会报一下错误:
Execution failed for task ':app:mergeDebugResources'.
> [string/app_name] /Users/tanzx/AndroidStudioProjects/AaptDemo/app/src/main/res/values/strings.xml [string/app_name] /Users/tanzx/AndroidStudioProjects/AaptDemo/app/src/main/res/values/ids.xml: Error: Duplicate resources
因为app_name
对于的资源已经在value
中被声明了。
官方相关的说明官网:选择要设为公开的资源。
原文翻译:库中的所有资源在默认情况下均处于公开状态。如需将所有资源隐式设为私有,您必须至少将一个特定属性定义为公开。资源包括您项目的
res/
目录中的所有文件,例如图像。为了防止库的用户访问仅供内部使用的资源,您应该通过声明一个或多个公开资源的方式来使用这种自动私有标识机制。或者,您也可以通过添加空的<public />
标记将所有资源设为私有,此标记不会将任何资源设为公开,而是会将一切(所有资源)都设为私有。通过将属性隐式设为私有,您不仅可以防止库的用户从内部库资源获得代码补全建议,还可以重命名或移除私有资源,而不会破坏库的客户端。系统会从代码补全中过滤掉私有资源,并且 Lint 会在您尝试引用私有资源时发出警告。
在构建库时,Android Gradle 插件会获取公开资源定义,并将其提取到
public.txt
文件中,然后系统会将此文件打包到 AAR 文件中。
实测结果也仅仅是不回代码自动不全,编译器报红。如果进行lint
检查,编译都没有警告~!
现在大部分的解释为:文件RES/value/public.xml用于将固定资源 ID
分配给 Android
资源。
stackoverfloew:What is the use of the res/values/public.xml file on Android?
public.xml
文件内容:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<public name="forecast_list" id="0x7f040001" type="id" />
<public name="app_name" id="0x7f070002" type="string" />
<public name="string3" id="0x7f070003" type="string" />
</resources>
资源id的固定在热修复和插件化中极其重要。在热修复中,构建patch
时,需要保持patch
包的资源id
和基准包的资源id
一致;在插件化中,如果插件需要引用宿主的资源,则需要将宿主的资源id
进行固定,因此,资源id
的固定在这两种场景下是尤为重要的。
在Android Gradle Plugin 3.0.0
中,默认开启了aapt2
,原先aapt的资源固定方式public.xml
也将失效,必须寻找一种新的资源固定的方式,而不是简单的禁用掉aapt
2,因此本文来探讨一下aapt和aapt2
分别如何进行资源id
的固定。
项目环境配置(PS:吐槽一下aapt已经被aapt2代替了,aapt相关资料几乎没有,环境搭建太费劲了~!)
com.android.tools.build:gradle:2.2.0
distributionUrl=https\://services.gradle.org/distributions/gradle-3.4.1-all.zip
compileSdkVersion 24
buildToolsVersion '24.0.0'
先在value
文件下按照上面的ids.xml
和public.xml
的内容以及文件名,生成对应的文件。
直接编译结果
通过直接编译之后的R文件
的内容,可以看到我们想要的设置的资源id
并没有按照我们预期的生成。
将
public.xml
文件拷贝到build/intermediates/res/merged
对应的目录
afterEvaluate {
for (variant in android.applicationVariants) {
def scope = variant.getVariantData().getScope()
String mergeTaskName = scope.getMergeResourcesTask().name
def mergeTask = tasks.getByName(mergeTaskName)
mergeTask.doLast {
copy {
int i=0
from(android.sourceSets.main.res.srcDirs) {
include 'values/public.xml'
rename 'public.xml', (i++ == 0? "public.xml": "public_${i}.xml")
}
into(mergeTask.outputDir)
}
}
}
}
这次我们可以直接看到资源id
按照我们需要生成了。
这是为什么呢?
-
android gradle
插件1.3
以下版本可以直接将public.xml
放在源码res
目录参与编译; -
android gradle
插件1.3+
版本在执行mergeResource
任务时忽略了public.xml
,所以merge
完成后的build
目录下的res
目录下没有public.xml
相关的内容。所以需要在编译时通过脚本将public.xml
插入到merge
完成后的build
目录下的res
目录下。之所以这样做可行,是因为aapt
本身是支持public.xml
的,只是gradle
插件在对资源做预处(merge)
时对public.xml
做了过滤。
在aapt2
编译(将资源文件编译为二进制格式)后,发现merge
的资源都已经经过了预编译,产生了flat
文件,这时候将public.xml
文件拷贝至该目录就会产生编译错误。
但在aapt2
的链接阶段中,我们查看相关的链接选项:
选项 | 说明 |
---|---|
--emit-ids path |
在给定的路径下生成一个文件,该文件包含资源类型的名称及其 ID 映射的列表。它适合与 --stable-ids 搭配使用。 |
--stable-ids outputfilename.ext |
使用通过 --emit-ids 生成的文件,该文件包含资源类型的名称以及为其分配的 ID 的列表。此选项可以让已分配的 ID 保持稳定,即使您在链接时删除了资源或添加了新资源也是如此。 |
发现--emit-ids
和--stable-ids
命令搭配可以实现id
的固定。
android {
aaptOptions {
File publicTxtFile = project.rootProject.file('public.txt')
//public文件存在,则应用,不存在则生成
if (publicTxtFile.exists()) {
project.logger.error "${publicTxtFile} exists, apply it."
//aapt2添加--stable-ids参数应用
aaptOptions.additionalParameters("--stable-ids", "${publicTxtFile}")
} else {
project.logger.error "${publicTxtFile} not exists, generate it."
//aapt2添加--emit-ids参数生成
aaptOptions.additionalParameters("--emit-ids", "${publicTxtFile}")
}
}
}
- 第一次编译,先通过
--emit-ids
在项目的根目录生成public.txt
; - 再将
public.txt
里面对于的id
改为自己想要固定的id
; - 再次编译,通过
--stable-ids
和根目录下的public.txt
进行资源id
的固定;
--emit-ids
编译结果
修改
public.txt
文件内容再次编译
我们一般正常打包生成的中间产物是build/intermediates/symbols/debug/R.txt
,需要将其转化为public.txt
。
R.txt
格式(int
type
name
id
)或者(int[]
styleable
name
{id,id,xxxx}
)
public.txt
格式(applicationId:type/name = id
)
所以在转化过程中需要过滤掉R.txt
文件中的styleable
类型。
android {
aaptOptions {
File rFile = project.rootProject.file('R.txt')
List<String> sortedLines = new ArrayList<>()
// 一行一行读取
rFile.eachLine {line ->
//rLines.add(line)
String[] test = line.split(" ")
String type = test[1]
String name = test[2]
String idValue = test[3]
if ("styleable" != type) {
sortedLines.add("${applicationId}:${type}/${name} = ${idValue}")
}
}
Collections.sort(sortedLines)
File publicTxtFile = project.rootProject.file('public.txt')
if (!publicTxtFile.exists()) {
publicTxtFile.createNewFile()
sortedLines?.each {
publicTxtFile.append("${it}\n")
}
}
}
}
在AAPT概述
这部分我们讲过如果一个 apk
文件里面的资源,如果被加上PUBLIC
标记的话,就能被其他 apk
所引用,引用方式是@包名:类型/名字
,例如:@android:color/red
。
阅读上面《aapt
进行id
的固定》到《aapt2
进行id
的固定》这两部分,我们知道aapt
和aapt2
进行id
固定的方法是不相同的。
其实如果我们用aapt2 dump build/intermediates/res/resources-debug.ap_
命令查看生成资源的相关信息。
aapt
通过public.xml
进行id
固定的资源信息有PUBLIC
标记:
二使用上面aapt2
进行id
固定的方式是没有下图中的PUBLIC
标记的。
原因还是aapt
和aapt2
的差异造成的,aapt2
的public.txt
不等于aapt
的public.xml
,在aapt2
中如果要添加PUBLIC
标记,其实还是得另寻其他途径。
回顾
aapt
进行资源id
固定和PUBLIC
标价,是将public.xml
复制到${mergeResourceTask.outputDir}
;aapt2
相比于aapt
,做了增量编译的优化。AAPT2
会解析该文件并生成一个扩展名为.flat
的中间二进制文件。
思考
能否使用aapt2
自己将public.xml
编译为public.arsc.flat
,并像 aapt
操作一样将其复制到 ${mergeResourceTask.outputDir}
;
android {
//将public.txt转化为public.xml,并对public.xml进行aapt2的编译将结果复制到${ergeResourceTask.outputDir}
//下面大部分代码是copy自tinker的源码
applicationVariants.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文件,最后一个参数true标识固定资源id
convertPublicTxtToPublicXml(project.rootProject.file('public.txt'), publicXmlFile, false)
def variantData = variant.getMetaClass().getProperty(variant, 'variantData')
def variantScope = variantData.getScope()
def globalScope = variantScope.getGlobalScope()
def androidBuilder = globalScope.getAndroidBuilder()
def targetInfo = androidBuilder.getTargetInfo()
def mBuildToolInfo = targetInfo.getBuildTools()
Map<BuildToolInfo.PathId, String> mPaths = mBuildToolInfo.getMetaClass().getProperty(mBuildToolInfo, "mPaths") as Map<BuildToolInfo.PathId, String>
//通过aapt2 compile命令自己生成public.arsc.flat并输出到${mergeResourceTask.outputDir}
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}")
}
})
}
}
}
}
将public.txt
文件转化为public.xml
文件.
public.txt
中存在styleable
类型资源,public.xml
中不存在,因此转换过程中如果遇到styleable
类型,需要忽略;vector
矢量图资源如果存在内部资源,也需要忽略,在aapt2
中,它的名字是以$
开头,然后是主资源名,紧跟着__数字递增索引,这些资源外部是无法引用到的,只需要固定id
,不需要添加PUBLIC
标记,并且$
符号在public.xml
中是非法的,因此忽略它即可;- 由于
aapt2
有资源id
的固定方式,因此转换过程中可直接丢掉id
,简单声明即可(PS:这里通过withId
参数控制是否需要固定id
);aapt2
编译的public.xml
文件的上级目录必须是values
文件夹,否则编译过程会报非法路径;
/**
* 转换publicTxt为publicXml
* copy tinker:com.tencent.tinker.build.gradle.task.TinkerResourceIdTask#convertPublicTxtToPublicXml
*/
@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>")
}
以上思考和动手实践的过程,我们不仅解决了aapt2
进行PUBLIC
标记的问题,还找到了一种新的aapt2
进行id
固定的方法。
可能遇到的报错:
no signature of method com.android.build.gradle.internal.variant.applicationvariantdata.getscope() is applicable for argument types: () values: []
解决方法为修改gradle
版本为 gradle:3.3.2
和 gradle-wrapper:5.6.2
,毕竟 tinker
也不支持最新版的 gradle
.