Android 资源链接 -- AAPT2与R.java

近来重新打包 APK 时发现,会有 R.type.id 找不到的报错,花两天时间研究了下 R 文件与 aapt 工具并解决了问题,这里记录一下。

为了解答这些问题,我通过搜索引擎查看了 StackOverflow(问答),Android Developers(官方文档),GitHub(源码,有 android Git repositories - Git at Google 的镜像)。

几点疑惑

之前虽然也在用打包工具,但是这次遇到的问题确实没研究过,在研究过程中引申出以下这些问题:

  • R 文件究竟是什么?
  • 这个文件的内容是怎么生成的,又是怎么被用到的呢?
  • aapt1 aapt2 有什么区别?为什么改用 aapt2 编译时出错了?
  • apktool 是怎么使用 aapt2 的?可以参考它使用的参数来解决问题吗?
  • apktool d 会生成的 public.xml 究竟有什么用处?
  • AAR 格式有什么好处?可以防止资源冲突吗?
  • gradle android task 是怎么编译资源的?有源码可以参考吗?
  • apktool 中的 Framework Files 是什么?有什么用途?

通过 R 文件访问资源

When your application is compiled, aapt generates the R class, which contains resource IDs for all the resources in your res/ directory. For each type of resource, there is an R subclass (for example, R.drawable for all drawable resources), and for each resource of that type, there is a static integer (for example, R.drawable.icon). This integer is the resource ID that you can use to retrieve your resource.

R.classaapt 工具生成,包含应用中 res 目录下所有资源的ID。其中,对应每个资源类型,都有 R 的子类对应创建。(如 R.drawable 对应所有 drawable 资源)

资源与资源 ID 映射

At build time, the aapt tool collects all of the resources you have defined (though separate files or explicit definitions in files) and assigns resource IDs to them.
A resource ID is a 32 bit number of the form: PPTTNNNN. PP is the package the resource is for; TT is the type of the resource; NNNN is the name of the resource in that type. For applications resources, PP is always 0x7f.

Once the resources are compiled and identifiers assigned, aapt generates the R.java file for your source code and a binary file called “resources.arsc” that contains all of the resource names, identifiers, and values (for resources that come from separate file, their value is the path to that file in the .apk), in a format that can easily mapped and parsed on the device at runtime.
You can get a summary of the resources.arsc file in an apk with the command “aapt dump resources “.

一般情况下,R 文件中的 ID 都是随机生成的。运行时通过 ID 在 resources.arsc 文件中映射为实际的资源文件。

资源 ID 生成

从哪里可以看出 ID 的组合规则呢?下面是单个资源的结构声明:

struct ResourceIdResource.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* A binary identifier representing a resource. Internally it
* is a 32bit integer split as follows:
*
* 0xPPTTEEEE
*
* PP: 8 bit package identifier. 0x01 is reserved for system
* and 0x7f is reserved for the running app.
* TT: 8 bit type identifier. 0x00 is invalid.
* EEEE: 16 bit entry identifier.
*/
struct ResourceId {
// ......
};

可以从这个结构体的注释中看出,资源 ID 的组成规则为 PPTTEEEE,其中:

  • PP:8位的包ID
  • TT:8位的类型ID
  • EEEE:16位的记录ID

生成规则是选定范围,随机为每个资源指定唯一的ID,ID尽可能分布在范围之内。相关源码:platform_frameworks_base/IdAssigner.cpp

aapt2

AAPT2 | Android Developers

aapt compile

这个命令可以编译所有类型的 Android 资源,例如 drawables 和 XML 文件。

每次只编译单个文件,可以有效利用增量编译的特性,避免重复进行全量编译。(可以使用 --dir 参数指定文件夹)

aapt2 compile path-to-input-files [options] -o output-directory/

链接阶段,aapt2 会将编译阶段生成的所有中间文件合并,并且打包到 APK 中。(这个 APK 不包含 DEX 代码也没有签名,所以不能用于发布。)

aapt2 link path-to-input-files [options] -o outputdirectory/outputfilename.apk --manifest AndroidManifest.xml

生成 R 文件

这一阶段可以生成 R 文件,相关的命令参数如下:

  • --java directory 指定生成 R.java 的目录
  • --extra-packages package_name 使用不同的包名生成同样的 R.java 文件

引用 Android 资源

如果使用到 Android 命名空间下的资源,需要通过 -I path 这个参数来传入对应平台的 android.jar 或者 framework-res.apk

Apktool

在解决问题的过程中,尝试过查看 iBotPeaches/Apktool: A tool for reverse engineering Android apk files 的源码,想参考下它使用的 aapt link 参数列表。

aapt2Package()AndrolibResources.java
1
2
3
4
5
6
if (include != null) {
for (File file : include) {
cmd.add("-I");
cmd.add(file.getPath());
}
}

向上查找这里的 include 参数,发现传入的是 Framework Files,下文解释。

Framework Files

As you probably know, Android apps utilize code and resources that are found on the Android OS itself. These are known as framework resources and Apktool relies on these to properly decode and build apks.

框架文件包含 Android OS 自带的一些资源,apktool 这个工具也预置框架文件,在不同系统上的目录如下:

  • unix - $HOME/.local/share/apktool
  • windows - %UserProfile%\AppData\Local\apktool
  • mac - $HOME/Library/apktool

使用自己编写的工具打包时,可以在对应目录下找到框架文件,然后 aapt link 时使用 -I path 传入,这样就可以解析 android 命名空间下的资源引用了。

后记

经过前文的一些梳理,我们有了对 Android 资源链接 这一问题基本的认识。这一小节把没有明确解答的几个疑惑列举一下:

  • aapt1 aapt2 有什么区别?为什么改用 aapt2 编译时出错了?

aapt2 区分了 compile 和 link 两个阶段,可以利用增量编译的特性,加快整个流程。

之前的用法是 compile 所有资源,解压生成的中间文件,再使用 link 进行链接,但是会报资源找不到的错误。

后来的解决方案是:不解压,直接把中间文件传递给 link,正常打包成功了。

  • apktool 是怎么使用 aapt2 的?可以参考它使用的参数来解决问题吗?

研究 apktool 的代码,主要有两点收获。其一是发现 gradle 脚本的好处,各种预编译,单元测试以及打包。当然以前了解的 Makefile 也有这种功能,只是这次近距离接触,感触更深。其二是发现了预置的框架文件,解决了后面找不到 android 资源的问题。

  • apktool d 会生成的 public.xml 究竟有什么用处?

看了许多文档,作用主要是导出了所有公有资源,这些资源的id是固定的,不应该重新生成。

但具体哪里读取 public.xml,在 apktool 和 aapt2 的源码中没有找到直接引用。推测是 gradle 构建时会读取,然后通过 aapt 的 --stable-ids 参数进行传递。

还有个作用是在 AAR 中,别的应用如果引用不在 public.xml 中的资源,会被提示“引用了一个私有的资源”。

  • AAR 格式有什么好处?可以防止资源冲突吗?

AAR 是 Android Studio 支持的一种格式,可以把代码和资源都打包进去,既可以共享代码,也可以共享资源文件。

不能防止资源冲突,AAR 的所有资源在编译时都会打包在一起,官方也建议,每个库在自己的资源ID前加上前缀,避免冲突。

  • gradle android task 是怎么编译资源的?有源码可以参考吗?

暂时没有找到对应 android 编译任务的开源代码。