动态加载技术(也叫插件化技术),当项目越来越庞大的时候,我们通过插件化开发不仅可以减轻应用的内存和CPU占用,还可以实现热插拔,即在不发布新版本的情况下更新某些模块。
通常我们把安卓资源文件制作成插件的形式,无外乎有一下几种:
zip、jar、dex、APK(未安装APK、安装APK)
对于用户来讲未安装的APK才是用户所需要的,不安装、不重启,无声无息的加载资源文件,这正是我们开发者追求的结果。
但是,开发中宿主程序调起未安装的插件apk,一个很大的问题就是资源如何访问,这些资源文件的ID都映射在gen文件夹下的R.java中,而插件中凡是以R开头的资源都不能访问。究其原因是因为宿主程序中并没有插件的资源,所以通过R来加载插件的资源是行不通的,程序会抛出异常:无法找到某某id所对应的资源。
那么开发中该怎么办呢,今天我们来一起探讨一下插件化开发中资源文件访问的解决方案。
想必大家在开发中都写过类似代码,例如,在主程序访问字符串文件
this.getResources().getString(R.string.app_name);
这里的this,其实就是Context,上下文对象。通常我们的的APK安装路径为:
/data/apk/packagename~1/base.apk
APK启动,Context通过类加载器加载完毕后,会去APK中加载资源文件。想必大家都知道,Activity的工作主要是通过ContextImpl来完成的, Activity中有一个叫mBase的成员变量,它的类型就是ContextImpl。注意到Context中有如下两个抽象方法,看起来是和资源有关的,实际上Context就是通过它们来获取资源的。这两个抽象方法的真正实现在ContextImpl中,也就是说,只要实现这两个方法,就可以解决资源问题了。
/** Return an AssetManager instance for your application's package. */
public abstract AssetManager getAssets();
/** Return a Resources instance for your application's package. */
public abstract Resources getResources();
我们若是想使用这两个方法,需要实例化Context对象,通常我们可以根据APK中的包名完成Context对象的创建:
Context pluginContext = this.createPackageContext("com.castiel.demo",flags);
但是这样做有个前提,必须要求初始化时加载的是自己APK,如果我们加载的是未安装的插件APK,这么做肯定就不可取了。为啥呢,看源码:
Resources resources = packageInfo.getResources(mainThread);
if (resources != null) {
if (activityToken != null
|| displayId != Display.DEFAULT_DISPLAY
|| overrideConfiguration != null
|| (compatInfo != null && compatInfo.applicationScale
!= resources.getCompatibilityInfo().applicationScale)) {
resources = mResourcesManager.getTopLevelResources(packageInfo.getResDir(),
packageInfo.getSplitResDirs(), packageInfo.getOverlayDirs(),
packageInfo.getApplicationInfo().sharedLibraryFiles, displayId,
overrideConfiguration, compatInfo, activityToken);
}
}
mResources = resources;
Resources在这里被赋值,我们再去代码中第一行的packageInfo,它来自LoadedApk