0%

第15章:再谈资源

Android App 打包流程

早期Android打包都是基于Ant来做,为此我们需要熟悉Android App 打包的每一个过程。随着Gradle的问世,打包简化为几行配置代码。一套完整的Android App 打包流程如下图所示:

Android打包流程图

介绍下各部分的作用:

  • aapt: 为res目录下的资源生成 R.java 文件,同时为AndroidManifest生成Manifest.java文件
  • aidl: 把项目中自定义的aidl文件生成相应的java代码文件
  • javac: 把项目中所有的Java 代码编译成 class 文件。包括3部分: 自己写的代码;aapt生成的代码;aidl生成的Java文件
  • proguard: 混淆的同时生成proguardMapping.txt,这个步骤是可选的
  • dex: 把所有的class文件(包括第三方库的class 文件)转换为dex文件
  • aapt: 这里还是使用aapt,这里是它的另一个功能:打包。即将res目录下的资源、assets下的文件,打包成一个 .ap_ 文件
  • apkbuilder:将所有的dex、ap_文件、AndroidManifest.xml 打包为.apk文件,此时未签名
  • jarsigner: 签名
  • zipalign: 对齐,以便运行时节省内存

资源冲突解决方案一:修改AAPT

插件中的资源id可能会和宿主资源id是同一个值,为了解决资源id冲突,有3中解决方案:

  • 修改打包流程中的aapt命令,为插件资源id指定 0x71 之类的前缀,就可以避免冲突
  • 仍然是将插件资源的id前缀改为 0x71,但是在Android打包生成 resources.arsc 文件后,对这个文件进行修改(具体可见21.2节)
  • 进入到哪个插件,就为这个插件生成新的 AssetManager 和 Resources 对象,使用这两个新对象加载资源,就只能是插件中的资源,永远不会和宿主中冲突(详见 7.2)

修改并生成新的aapt命令

R文件中有十六进制整数变量,内容如下:

1
//代码

这些十六进制的变量,由三部分组成: packageId(apk包id,默认 0x7f) + typeId(资源类型,如attr=0x01,drawable=0x02,还有layout、string等) + entryId(typeId下的资源编码,从0开始递增)。以 0x7f0b006d 为例,packageId 为 0x7f,typeId为 0b,entryId 为 006d。

插件中为防止资源冲突,会为每个插件设置不同的packageId,比如游戏大厅中,斗地主插件可能是 0x71开头,斗牛可能是 0x72。为asset 生成 R 文件 是通过 aapt 完成的,为了实现上述目的,我们要修改 aapt 源码,定位到 Android SDK,找到 aapt 目录,里面有一堆 C 代码, 命令行工具就是用这些代码编译成的,可以在这个目录直接搜索 0x7f,在 ResourcesTable.cpp 中可以找到如下代码:

1
//

在 ResourcesTable 的构造函数中,有一个 Bundle 类型的参数,其次,判断 mPackageType 如果是 App,则都是 0x7f,此外 0x01 和 0x00 都被系统占用了,所以我们不要将这两个值设置为插件的 id前缀(事实上,有些手机厂商会占用其他的一些值,为了保险,我们一般只使用 0x71~0xff 作为插件的前缀)。修改 AAPT 的代码,基本思路如下:

  1. 在 aapt 的命令行参数中传递打包时的前缀
  2. 把这个值设置给 Bundle 实体的 mApkModule 字段,作为 ResourcesTable 的构造函数参数传入
  3. 在 ResourcesTable 构造函数读取 mApkModule 值,也就是前缀值,设置给 packageId

实现代码: 略

在插件化项目中使用新的aapt命令

现在,可以用我们修改的aapt文件替换sdk下的aapt 命令,但是如果这么做,每当Android系统更新,我们都要替换一次aapt命令。一种可行的做法是,我们把这个新的 aapt 工具命名为 aapt_mac ,放到项目的根目录下:

自定义的aapt放置

之后,修改项目中 gradle 文件:

1

上述脚本通过反射,把aapt的路径临时修改为指向当前App根目录下的aapt_mac。此外,我们将App的资源前缀设置为 0x71 ,这样在打包后,R文件中的资源就以 0x71 作为前缀了。

public.xml 固定资源id值

如下场景:多个插件都需要同一个自定义控件,于是我们把这个自定义控件卸载宿主 App,插件调用宿主的Java 代码,使用宿主的资源(有控件肯定有资源)。考虑到App在每次打包后,随着资源的增减,同一个资源id的值可能会发生变化。为避免这种情况,我们可以把公用的资源id值固定写死,如下public.xml文件所示(注意,type和id后面的空格不可省略):

1
2
3
4
<?xml version="1.0" encoding="utf-8">
<resources>
<public type="string" name="string1" id="0x7f050024"/>
</resources>

之后,把public.xml放到 res/values 目录下,R.string.string1 这个资源就会固定成 0x7f050024。当然,还可以指定资源值的一个区间,将上述代码中间那行改成如下代码即可:

1
<public-padding name="my_" end="0x7f02000f" start="0x7f020001" type="drawable"/>

但是从gradle 1.3开始,就忽略 public.xml了,因此需要我们自己使用gradle 脚本来实现,代码如下:

1

之后,打包宿主 ActivityHost1 ,使用Jadx-GUI 查看资源id,可以看到 R.string.string1 的值永远是 2131034148(也即十六进制0x7f050024)。

插件使用宿主的资源

宿主资源值固定了,但是插件怎么访问宿主中的资源呢?如果插件内部能保持一个对宿主项目的引用,那就可以随便访问宿主的任何资源了。我们需要编写gradle脚本,把宿主打包成 jar 包。之后设置插件的gradle文件,通过provided来引用这个jar包。之前介绍过,provided方式引用只在编码时候有用,正式打包的时候不会被引用进去。代码如下:

1

小结

本章给出插件化中资源id冲突的解决方案:

  • 把宿主和插件的资源都合并到一起,通过AssetManager的addAssetPath 来实现。只不过,这种方案会产生资源id冲突的问题
  • 如果不事先合并资源,那就为每个插件创建一个 AssetManager,每个 AssetManager 都是通过反射调用 addAssetPath 方法,把插件资源加进去。当宿主进入一个插件时,就把 AssetManager 切换为 插件的AssetManager ;反之,当从插件回到宿主的时候,再把 AssetManager 切换回宿主的 AssetManager(详见第5章的loadResource方法)。

第一种方案,主要缺陷是资源冲突,并且资源id的前缀是有限的,也就256个值,如果超过256个插件,就要使用方案2了。

谢谢你的鼓励