聊聊 APK —— AAR 的合并进 APK

in with 0 comment

我们知道,Android 对于多人协作的方式,是使用 AAR 作为 Android 的库来给 App 引入参与编译的。Android 之于 Java 应用,在编译上最大的不同有两个,一个是 res 下面的资源,一个是 class 文件需要转成 dex 文件才能被加载和运行。

那么,在 AAR 的使用过程中,我们的 Gradle 到底对 AAR 做了什么动作呢?其实这部分内容,在以下的文章提过一些,不过我们今天想再仔细讲讲资源。

Gradle Builds Everything —— 处理依赖(aar)

我们来看看 AAR 中存在的东西

viewpager2

如果你解压看 classes.jar,可以看到里面就是 class 文件。

classes.jar

然后 res 下面是资源文件,这些资源文件都还没经过压缩

res

然后是一个 R.txt

R.txt

那么,这里只有一个 R.txt,如果我们仔细检查 classes.jar,发现里面也没有带 R.class 或者 R.java,这是为什么呢?

R 与资源

如果我们去看一个 apk 的 R 文件,它里面看 smali 字节码是这样的:

R 的字节码

其实里面存的都是一个 int 值,大部分还是 0x7f 开头,我们把他们叫做 id,或者资源的索引
既然在这看见了索引,索引所指向的条目在哪里呢?答案是resources.arsc这个文件:

resources.arsc

注意到 ID 这一栏,是不是特别熟悉?我们把右边的列结合起来看。我想你一下子就明白这个意思了

ID 是索引,根据 R 指明的 ID,结合手机当下的场景,比如 v20,v21 等使用特定的资源,如果没有找到相关的资源,就使用 default 下的资源。

这样我们就实现了机器自动找资源的功能,R 最伟大的功能就明了了 —— 从 resources.arsc 这个文件中,根据特定的场景,找到正确的资源并加载。开发无需再在代码里判断机器当时的场景来做特定的处理了。这的确大大简化了国际化等功能的开发。

那么新的问题来了,为什么 aar 里面不带 R 这个文件呢?

R 文件的生成

首先我们来看 ID 这个值,显然这个值不能重复,假设 aar 里面带了 R,意味着这个 R 里面的值需要保证唯一,那么如果互联网上有这么多的 aar,里面的 R 都需要维护「全局唯一性」未必要求也太高了点,一个低成本的做法是:

把 AAR 在合并进 apk 的过程中,对所有的资源 ID 进行重生成,使得这个 R 在这个 app 内全局唯一。

这件事比起全世界 R 唯一就简单很多,那么说完了 R 的原理和 ID 生成逻辑,我们就要说说这件事是怎么做到的了。

R 的 regeneration

我们清楚的知道一点,一个 aar 里面的代码,当然可以使用自己的一些资源文件,即有自己的 R;那么同时,使用这个 AAR 的 App 自然也要用到这个 aar 里的资源文件。那么这个生成就有讲究了。

第一件事,提醒各位注意到,我们在写 library 的过程中,生成的 R 文件的 id —— 比如 R.id.textview 它的声明是非 final 的,直观一点,就是我们不可以在 library 中这么写:

switch(view.getId()){
   case R.id.textview: {
       //....
       break;
   }
}

为什么呢?因为如果这里的 R.id.textview 是 final 的话,可能在编译 class 的过程中,被直接内敛,编译后的代码可能是这样:

switch(view.getId()){
   case 0x7f000001: {
       //....
       break;
   }
}

假设这时候我们在 app 合并 AAR 的过程中重新生成了 id,那么这个 0x7f000001 指向的资源,几乎肯定不是原先我们想要的 R.id.textview,所以在 library module 中,AGP 不会生成带 final 的 R,我们用 if 改写 switch 的方案就是这样:

if (view.getId() == R.id.textview) {
    // .....
}

那么编译后,R 也不会被编译器优化内联。

还有一件事不要忘了,我们所有编译出的 R 都是带一个包名的,也就是说,如果你的 library module 中的 AndroidManifest.xml 写的 package 值是 com.gemini.demo.library (作为例子)的话,那么你生成的 R 的完整类名就是

com.gemini.demo.library

R 在 App 工程中的合并

那么,刚刚在 library module 编译出的 aar 中我们没有找到 R 这个类,最终在 app 中我们是如何运行的呢?群里朋友交流「手工创建 APK」的时候就碰到这样的问题:

在 app 最终编译出 apk 的过程中,因为缺少了 AAR 中的 R,导致 apk 包找不到类的错误。

这里引用下一位朋友的截图,大概是这样:

缺少类

那么,gradle 或者 android gradle plugin 是怎么解决这个问题的呢?

其实很简单,android gradle plugin 事实上是针对合并进来的 aar 的资源重新根据 aar 的 package 生成了一次 R,我们来看看证据,我们在 app 的工程中,中间产物找到这么一个文件:

R.jar

反编译后我们可以看见这些类

反编译后的 R.jar

注意到,我们所有引入的 aar,都根据 package 的值生成了一份 R。同时,我们 app package 下面也生成了一份 R。

app 的 R

这份 R 是所有引入 aar + app 工程资源的一份汇总和冗余,请注意,这里所有的 R 全是 final 的值,因为已经在最终合并阶段了。

这样我们就解决了刚刚说的 library R 的类找不到的问题。

欢迎关注我的公众号「TalkWithMobile」
公众号