聊聊 APK —— aapt 编译资源

in 开发 with 0 comment

上两期我们讲了 APK 里面 Dex 的东西,明白了 Dex 只是 classes 的某种打包形式,我们暂时不拘泥于细节,关于代码的部分就告一段落。我们知道除了代码,一个应用里,资源占用了相当大的一部分。

背景

资源本身是很简单的,我们可以理解为一个文件,但是,Android 天生为兼容各种各样不同的设备做了相当多的工作,比如屏幕大小、国际化、键盘、像素密度等等。我们能为各种各样特定的场景下使用特定的资源做兼容————而不用改动一行代码,这是 Google 对于 Android 设计的初衷。那么,假设我们为各种各样不同的场景适配了不同的资源,如何能快速的应用上这些资源呢?如果这些场景都需要我们用 if else 一个一个去判断,然后分别使用一个资源的特定某一个图片,那就太浪费我们的力气了。为了解决这个问题,Android 为我们提供了 R 这个类,指定了一个资源的索引(id),然后我们只需要告诉系统 ———— 在这个业务场景下,使用这个资源就好了,至于具体是指定资源里面的某一个具体文件的话,就由系统根据开发者的配置决定吧。

在这种场景下,假设我们给定的 id 是 x 值,那么当下业务需要使用这个资源的时候,手机的状态就是 y 值,有了(x,y),在一个表里面就能迅速的定位到资源文件的具体路径了。在这里我们描述的这个表大家可能很眼熟,就是 resources.arsc,它是怎么来的呢?答案是从 aapt 编译出来的。

使用 aapt 编译资源

以上我们介绍了 Android 加载资源的策略,接下来我们就要介绍资源编译了,为什么资源也需要编译?其实二进制的资源(比如图片)是不需要编译的,只不过这个“编译”的行为,是为了生成 resources.arsc 以及对 xml 文件进行二进制化等操作,resources.arsc 是上面说的表,xml 的二进制化是为了系统读取上性能更好。AssetManager 在我们调用 R 相关的 id 的时候,就会在这个表里面找到对应的文件,读取出来。其实 R 文件的存在是没有必要的,前提是你知道 id。

当下我们的 build-tools 最新版本是 28.0.3,因为 aapt 已经 deprecated,取而代之的是 aapt2,我们就以 aapt2 为例吧。实话说两者差距还是蛮大的,aapt2 是对 aapt 的改良,但是在我看来,aapt2 并没有非常完善,没有到达完全替代 aapt 的程度。可能 Google 也是这么想的,所以其实 28.0.3 还是带了 aapt 的二进制文件 ———— 只是不让你在 gradle 中用而已(你会发现 android.enableAapt2=false 不起作用)。

学一个东西最重要的是学会如何看文档,所以先贴上文档

https://developer.android.com/studio/command-line/aapt2

这里是对 aapt2 所有命令的解释,Gradle 在编译资源的过程中,就是调用的这个命令,传的参数也在这个文档里都介绍了,只不过对开发者隐藏起了调用细节,今天我们不使用 Gradle,就来揭开 aapt 神秘的面纱了。

首先创建一个项目 ———— 当然可以手动来不经过 Android Studio,我们可以没有代码,只有资源。

那么 aapt2 主要分两步,一步叫 compile,一步叫 link。人家估计就抄 GCC 的。

构造项目

我构造了一个很简单的项目,写了两个 xml,分别是 AndroidManifest.xml 和 activity_main.xml。首先我们要把 activity_main.xml 编译出来(AndroidManifest.xml 后续留做 link 用)。

WX20190717-231127@2x.png

Compile

调用命令

 ~/Code/MyFirstAndroidApplication/ mkdir compiled
 ~/Code/MyFirstAndroidApplication/ aapt2 compile src/main/res/layout/activity_main.xml -o compiled/

在 compiled 文件夹中,我们就看见了我们要的 layout_activity_main.xml.flat 这个文件,对 flat 有兴趣的同学可以自行搜索,这里就不再赘述,我们就理解成一个中间产物即可。它是 aapt2 特有的,aapt 没有,aapt2 用它能进行增量编译。
如果我们有很多的文件的话,需要依次调用 compile 才行,其实这里也可以使用 --dir 参数,只不过这个参数就没有增量编译的效果了。

Link

那么简单的一个资源文件我们就编译完了。接下来要 link。 link 的工作量比 compile 要多一点,我们可以看下 link 相关的参数,最后组装起来。它的 input 是那些 flat,然后还要指定 AndroidManifest,然后指定输出的文件,以及输出的 R.java。
注意,此处的输入是多个 flat 的文件 和 AndroidManifest.xml,外部资源,输出是只包含资源的 apk(如果你曾经研究过的话,你会发现它的后缀名是 ap_)和 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:text,我们平常使用的@android:xxx都是放在这个 jar 里面,其实我们也可以提供自己的资源供别人链接,后续再做介绍。

第三行是输入的 flat 文件,如果有多个,直接在后面拼接即可,比如

compiled/res/drawable_Image.flat compiled/layout_activity_main.xml.flat

这样。

第四行是 R.java 生成的目录,第五行是指定 AndroidManifest.xml

我们执行下这个命令,完了目录下就会出现一个out.apk,源码文件夹里面会多了一个 R.java,我们把 out.apk,拖进 Android Studio 一探究竟

APK 内容

哇,这就是一个没有classes.dex的标准 APK 呀,我们注意一下红框里面的十六进制数字,resources.arsc 从第三列开始,就是我们的配置了,看见一个 default,意思是当没有命中配置的时候,就是用里面的值,我们这里指定的是 xml 的路径。

然后再打开生成的 R.java

R.java

这里 R.java 就是 resources.arsc 里面的索引值,AssetManager 就是这么定位资源的。这下明白了么?

查看编译后的资源

除了是用 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,下面对应了两个资源,默认使用res/layout/activity_main.xml。我们顺便来看下一个用 Android Studio 新建出来的 apk 吧,为了简单,我暂时去除了 support library,因为会引入非常多的资源,我们使用aapt2 dump,得到如下:

Binary APK
Package name=com.gemini.app.properties id=7f
  type color id=01 entryCount=3
    resource 0x7f010000 color/colorAccent
      () #ffd81b60
    resource 0x7f010001 color/colorPrimary
      () #ff008577
    resource 0x7f010002 color/colorPrimaryDark
      () #ff00574b
  type drawable id=02 entryCount=3
    resource 0x7f020000 drawable/$ic_launcher_foreground__0
      (v24) (file) res/drawable-v24/$ic_launcher_foreground__0.xml type=XML
    resource 0x7f020001 drawable/ic_launcher_background
      () (file) res/drawable/ic_launcher_background.xml type=XML
    resource 0x7f020002 drawable/ic_launcher_foreground
      (v24) (file) res/drawable-v24/ic_launcher_foreground.xml type=XML
  type layout id=03 entryCount=1
    resource 0x7f030000 layout/activity_main
      () (file) res/layout/activity_main.xml type=XML
  type mipmap id=04 entryCount=2
    resource 0x7f040000 mipmap/ic_launcher
      (mdpi) (file) res/mipmap-mdpi-v4/ic_launcher.png type=PNG
      (hdpi) (file) res/mipmap-hdpi-v4/ic_launcher.png type=PNG
      (xhdpi) (file) res/mipmap-xhdpi-v4/ic_launcher.png type=PNG
      (xxhdpi) (file) res/mipmap-xxhdpi-v4/ic_launcher.png type=PNG
      (xxxhdpi) (file) res/mipmap-xxxhdpi-v4/ic_launcher.png type=PNG
      (anydpi-v26) (file) res/mipmap-anydpi-v26/ic_launcher.xml type=XML
    resource 0x7f040001 mipmap/ic_launcher_round
      (mdpi) (file) res/mipmap-mdpi-v4/ic_launcher_round.png type=PNG
      (hdpi) (file) res/mipmap-hdpi-v4/ic_launcher_round.png type=PNG
      (xhdpi) (file) res/mipmap-xhdpi-v4/ic_launcher_round.png type=PNG
      (xxhdpi) (file) res/mipmap-xxhdpi-v4/ic_launcher_round.png type=PNG
      (xxxhdpi) (file) res/mipmap-xxxhdpi-v4/ic_launcher_round.png type=PNG
      (anydpi-v26) (file) res/mipmap-anydpi-v26/ic_launcher_round.xml type=XML
  type string id=05 entryCount=1
    resource 0x7f050000 string/app_name
      () "Gemini"

这里就不用一一解释它的意思了,我相信大家看了能看明白.

资源共享

上面说了 aapt 编译和链接资源的事情,我们还有一个事情没有讲,就是 android.jar 里面共享资源是怎么做的。首先我要再明确一点,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

在 test.out 中得到很长的结果:

android.jar

你如果仔细看一看这里面的内容的话,会发现和上面 APK 的不同:

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熟悉不熟悉?那么这个包名是哪里来的呢?我们把android.jar改名成android.apk,然后拖到 Android Studio 中,如下图:

android.jar

再对比下我们上面放出的图,结论就非常清楚了,比如我们想要提供我们的资源,那么首先为我们的资源打上 PUBLIC 的标记,然后在 xml 中引用你的包名,比如:@com.gemini.app:color/red 就能引用到你定义的 color/red 了,如果你不指定包名,默认是自己。

至于 AAPT2 如何生成 PUBLIC,我们以后可以再讲,需要一定的篇幅。资源共享的应用在插件化的框架中是最多的,平常我们不一定用的到。我们最主要是了解到 aapt 怎么工作,以及产物在 APK 中是怎么样的方式存在即可。

如果还有不明白的地方,非常欢迎互相交流。