android 命令打apk_XML

上两期我们讲了 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 用)。

android 命令打apk_xml_02

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。那么我们的命令如下

1 aapt2 link -o out.apk \

2 -I $ANDROID_HOME/platforms/android-28/android.jar \

3 compiled/layout_activity_main.xml.flat \

4 --java src/main/java \

5 --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 一探究竟

android 命令打apk_xml_03

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

然后再打开生成的 R.java

android 命令打apk_android_04

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

查看编译后的资源

除了是用 Android Studio 去查看 resources.arsc,我们还可以直接使用 aapt2 dump 出我们的 apk 信息的方式来查看资源相关的 ID 和状态,比如执行这个命令

aapt2 dump out.apk

输出的结果如图

1 Binary APK

2 Package name=com.geminiwen.hello id=7f

3 type layout id=01 entryCount=1

4 resource 0x7f010000 layout/activity_main

5 () (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 命令打apk_android_05

你如果仔细看一看这里面的内容的话,会发现和上面 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 命令打apk_XML_06

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

 

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