引言
Android应用程序的编译中,负责资源打包的是aapt,如果不对打包后的资源ID进行控制,就会导致插件中的资源ID冲突。所以,我们需要改写aapt的源码,以达到通过某种方式传递资源ID的Package ID,通过aapt打包时获取到这个Package ID并且应用才插件资源的命名中。
1.改造aapt的目的:防止资源冲突
我们前面知道了插件中的类的加载是通过DelegateClassLoader来进行的,那么插件中资源的加载呢?
其实也是通过DelegateResources进行的,只不过DelegateResources的更新是发生在每个插件安装完成后。在BundleLifecycleHandler的bundleChanged()方法中,监听到BundleEvent.LOADED便开始加载,loaded()方法如下:
|
|
显然是调用newDelegateResources()方法:
|
|
再进入到DelegateResources.newDelegateResourcesInternal()方法中:
|
|
这个方法主要做了以下事情:
- 新建一个AssetManager对象
- 通过反射调用AssetManager的addAssetPath将当前插件的资源路径添加进去,如果添加失败则尝试3次,3次之后还是失败则给出log
- 如果添加成功,则根据该AssetManager对象生成delegateResources这个DelegateResources或Resources对象,其中对于Miui做了兼容
- 将最新的delegateResources对象赋予RuntimVariables.delegateResources,并且将当前的插件路径赋值给assetPathsHistory
总结起来可以发现:OpenAtlas中管理资源的方式是每安装一个插件,就新建一个AssetManager,并且将之前的资源和插件中的资源都加入到唯这个AssetManager对象的的管理中,之后利用这个AssetManager对象生成新的delegate Resources对象,再利用反射将这个对象注入到LoadedApk中。这样统一管理的好处是一些基础资源(如主题,logo等)可以由宿主提供即可,减小插件包的大小。
但是,这样的话,由于不隔离,如果两个插件的资源ID相同(但是却对应不同的资源),就会造成资源ID的冲突。
首先了解一下Android应用程序的编译和打包过程。用一张图概括如下:
从图中可以清楚地看到Android的编译过程:先是利用aapt编译Manifest,Resources和Assets资源,生成R文件和打包好的资源文件,之后利用javac将java源码编译成字节码,利用NDK将native源码编译成.so库,之后利用dx将所有的字节码(jar包和之前编译的字节码)编译成dex文件(如果有混淆的话需要加上混淆规则),最后利用apkbuilder将dex文件、so库和打包好的资源文件一起编译成apk,如果需要签名的话,再利用签名程序(jarsigner)进行签名。
其中aapt称为Android Asset Package Tool,它的作用是将XML资源文件从文本格式编译成二进制格式,并且会执行以下两个额外的操作:
- 赋予每个非assets资源一个ID值,这些ID值以常量的形式定义在一个R.java文件中
- 生成一个resources.arsc文件,用来描述那些具有ID值的资源的配置信息,它的内容就相当于是一个资源索引表
有了资源ID以及资源索引表之后,Android资源管理框架就可以迅速将根据设备当前配置信息来定位最匹配的资源了。
如果大家对Android应用程序的编译和打包过程不熟悉,可以看老罗的这篇博客 Android应用程序资源的编译和打包过程分析.
我们在编译一个Android应用程序的资源的时候,至少会涉及到两个包,其中一个是被引用的系统资源包,另外一个就跟当前正在编译的应用程序资源包。每个包都可以定义自己的资源,同时它也可以引用其他包的资源。
那么,一个包是通过什么方式来引用其它包的资源的呢?这就是我们熟悉的资源ID了。资源ID是一个4字节的无符号整数,其中,最高字节表示Package ID,次高字节表示Type ID,最低两字节表示Entry ID。
Package ID相当于是一个命名空间,限定资源的来源。Android系统当前定义了两个资源命令空间,其中一个系统资源命令空间,它的Package ID等于0x01,另外一个是应用程序资源命令空间,它的Package ID等于0x7f。所有位于[0x01, 0x7f]之间的Package ID都是合法的,而在这个范围之外的都是非法的Package ID。前面提到的系统资源包package-export.apk的Package ID就等于0x01,而我们在应用程序中定义的资源的Package ID的值都等于0x7f,这一点可以通过生成的R.java文件来验证。
Type ID是指资源的类型ID。资源的类型有animator、anim、color、drawable、layout、menu、raw、string和xml等等若干种,每一种都会被赋予一个ID。
Entry ID是指每一个资源在其所属的资源类型中所出现的次序。注意,不同类型的资源的Entry ID有可能是相同的,但是由于它们的类型不同,我们仍然可以通过其资源ID来区别开来。
显然,要使各插件的资源ID不冲突,可以通过控制各个插件的Package ID来达到,即使用0x02-0x7E之间的id,如下是一种插件id的架构:
那么如何达到控制package id的目的呢?
当然要通过修改aapt的源码来达到。
2.aapt的改写
aapt的源码在/frameworks/base/tools/aapt下,这里以Android API 22的appt源码为例进行分析。先看Main.cpp中的main()函数:
|
|
这里省略了main()中对于aapt参数的处理,直接进入handleCommand()函数中:
|
|
显然,handleCommand()是用于分发命令的,我们的是打包资源的命令,所以是调用doPackage(bundle);其中doPackage()方法如下:
|
|
这里省略了编译资源之后输出R.java文件等代码,可以看出编译资源的代码是buildResources():
|
|
这里省略了很多无关的代码,其中的PackageType就是与Package ID有关的,它的定义如下:
|
|
显然,分为普通应用类型,系统类型,共享库类型和AppFeature类型。其中ResourceTable类的构造函数如下:
|
|
在这里就可以很明显的看出PackageType与packageId的关系.
看到这里,就可以知道,需要控制插件的packageId,就需要修改ResourceTable的构造函数,在其中传入对应插件的packageId。
这里有两个思路:第一种是将在Bundle中增加字段,将这个参数放在bundle中(因为bundle是从main()函数中一路传递下来的);第二种是通过全局变量来引用。
而bunnyblue采用的是第二种方法(其实这种方法不优美).
bunnyblue具体的实现方法是:在插件的build.gradle中的versionName中同时声明versionName和packageId,如下:
|
|
到了aapt中在进行处理,分离为”1.0”这个versionName和0x20这个插件的packageId.
具体是如何分离的呢?
在Main.cpp的main()方法中,有这么一行:
|
|
注意其中的hack_getVersionName(&bundle);该方法在Resourcehack.cpp中,如下:
|
|
显然,由于bundle.gradle中的versionName会写入到Manifest文件中,所以这里通过解析Manifest文件来获取插件的packageId,在hack_messageManifest()中:
|
|
显然,在这里分离出了插件的packageId并赋值给了全局变量pkgIdOffset,而这个pkgIdOffset是在Main.cpp中定义的:
|
|
显然,默认值为0x7f;而pkgIdOffset的使用当然是在ResourceTable的构造函数中:
|
|
显然,对于mPackageType为App和AppFeature的,packageId=pkgIdOffset.这样就获取到了我们写在插件项目的build.gradle中的packageId值。
不过,我自己觉得最好的处理方案是在Manifest中增加一个packageId的attr,之后在aapt的main()中解析出这个结果,并且放入bundle的字段中,最终在ResoureTable中对于App和AppFeature,去bundle中的该字段作为packageId.
改造后的源码可以在OpenAtlasExtension 看到。