Android App 打包流程
早期Android打包都是基于Ant来做,为此我们需要熟悉Android App 打包的每一个过程。随着Gradle的问世,打包简化为几行配置代码。一套完整的Android App 打包流程如下图所示:
介绍下各部分的作用:
- 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 的代码,基本思路如下:
- 在 aapt 的命令行参数中传递打包时的前缀
- 把这个值设置给 Bundle 实体的 mApkModule 字段,作为 ResourcesTable 的构造函数参数传入
- 在 ResourcesTable 构造函数读取 mApkModule 值,也就是前缀值,设置给 packageId
实现代码: 略
在插件化项目中使用新的aapt命令
现在,可以用我们修改的aapt文件替换sdk下的aapt 命令,但是如果这么做,每当Android系统更新,我们都要替换一次aapt命令。一种可行的做法是,我们把这个新的 aapt 工具命名为 aapt_mac ,放到项目的根目录下:
之后,修改项目中 gradle 文件:
1 |
上述脚本通过反射,把aapt的路径临时修改为指向当前App根目录下的aapt_mac。此外,我们将App的资源前缀设置为 0x71 ,这样在打包后,R文件中的资源就以 0x71 作为前缀了。
public.xml 固定资源id值
如下场景:多个插件都需要同一个自定义控件,于是我们把这个自定义控件卸载宿主 App,插件调用宿主的Java 代码,使用宿主的资源(有控件肯定有资源)。考虑到App在每次打包后,随着资源的增减,同一个资源id的值可能会发生变化。为避免这种情况,我们可以把公用的资源id值固定写死,如下public.xml文件所示(注意,type和id后面的空格不可省略):
1 |
之后,把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了。