在前面的文章中我们成功的加载了外部的Dex(Apk)并执行了插件的Bean代码。这时我们会想,能不能加载并运行插件Apk的Activity。答案当然是能,否则后续我们的研究就没意义了,但是想实现Activity的插件化运行,我们必须要解决一个问题——如何使用插件中的资源。

本文我们就讲一下插件的资源加载机制,并讲述一下如何实现资源的插件化。

一、资源的加载机制

Android的资源文件分为两类:

第一类是res目录下存放的可编辑的资源文件,这类文件在编译时系统会自动在R文件中生成资源文件的16进制值。

例如:

public final classR {public static final classanim {public static final int abc_fade_in=0x7f050000;public static final int abc_fade_in=0x7f050000;
...
}
}

我们在平时的开发时,访问这类资源比较简单,使用Context的getResource方法即可得到res下的各种资源。如下面代码所示:

String content = mContext.getResource().getString(R.string.content);

第二类是assets目录下存放的原始资源文件。apk在编译时不会编译assets下的文件,我们不能使用R.的方式访问,只能使用AssetsManager类的open方法来获取assets目录下的文件资源。

而AssetsManager又源于Resources类的getAssets方法,如下面代码所示:

Resource resource =getResource();
AssetsManager am=getResource().getAssets();
InputStream is= getResource().getAssets().open("filename");

通过上面的分析,我们可以初步做出一个结论:我们能使用Resources类是一个很重要的类,通过此类提供的相关API,我们能操作资源的加载。

二、资源插件化的解决方案

谈及资源插件化,我们不得不对AssetsManager的API多说一些。

AssetsManager中有一个addAssetsPath(String Path)方法,App启动的时候就会将当前的apk路径传进去,接下来AssetsManager和Resources就能访问当前apk的所有资源了。

AssetsManager的addAssetsPath方法不对外,但是我们可以通过反射的方式,把插件apk的路径传到这个方法,这样就把插件的资源添加到一个资源池中了。App有几个插件,我们就调用几次addAssetsPath方法,把插件的资源都塞到池子里。

这里我们以加载插件Apk里面的字符串资源为目标,实战一下资源插件化:

首先我们在插件app的string.xml里面定义字符串资源:

Hello Plugin

然后我们在宿主app编写如下代码:

public class MainActivity extendsActivity {privateAssetManager mAssetManager;privateResources mResources;privateResources.Theme mTheme;private String dexPath = null; //apk文件地址
private File fileRelease = null; //释放目录
protected DexClassLoader classLoader = null;private String pluginName = "plugin1.apk";
TextView mTextView;
@Overrideprotected voidattachBaseContext(Context newBase) {super.attachBaseContext(newBase);
extractAssets(newBase, pluginName);
}
@SuppressLint("NewApi")
@Overrideprotected voidonCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
File extractFile= this.getFileStreamPath(pluginName);
dexPath=extractFile.getPath();
fileRelease= getDir("dex", 0);
classLoader= new DexClassLoader(dexPath, fileRelease.getAbsolutePath(), null, getClassLoader());
mTextView=findViewById(R.id.tv);//带资源文件的调用
findViewById(R.id.btn_6).setOnClickListener(newView.OnClickListener() {
@Overridepublic voidonClick(View arg0) {
loadResources();try{
Class mLoadClassDynamic= classLoader.loadClass("com.plugin1.Dynamic");
Object dynamicObject=mLoadClassDynamic.newInstance();
IDynamic dynamic=(IDynamic) dynamicObject;
String content= dynamic.getStringForResId(MainActivity.this);
mTextView.setText(content);
Toast.makeText(getApplicationContext(), content+ "", Toast.LENGTH_LONG).show();
}catch(Exception e) {
e.printStackTrace();
}
}
});
}protected voidloadResources() {try{
AssetManager assetManager= AssetManager.class.newInstance();
Method addAssetPath= assetManager.getClass().getMethod("addAssetPath", String.class);
addAssetPath.invoke(assetManager, dexPath);
mAssetManager=assetManager;
}catch(Exception e) {
e.printStackTrace();
}
Resources superRes= super.getResources();
superRes.getDisplayMetrics();
superRes.getConfiguration();
mResources= newResources(mAssetManager, superRes.getDisplayMetrics(), superRes.getConfiguration());
mTheme=mResources.newTheme();
mTheme.setTo(super.getTheme());
}
@OverridepublicAssetManager getAssets() {return mAssetManager == null ? super.getAssets() : mAssetManager;
}
@OverridepublicResources getResources() {return mResources == null ? super.getResources() : mResources;
}
@OverridepublicResources.Theme getTheme() {return mTheme == null ? super.getTheme() : mTheme;
}/*** 把Assets里面得文件复制到 /data/data/files 目录下
*
*@paramcontext
*@paramsourceName*/
public static voidextractAssets(Context context, String sourceName) {
AssetManager am=context.getAssets();
InputStream is= null;
FileOutputStream fos= null;try{
is=am.open(sourceName);
File extractFile=context.getFileStreamPath(sourceName);
fos= newFileOutputStream(extractFile);byte[] buffer = new byte[1024];int count = 0;while ((count = is.read(buffer)) > 0) {
fos.write(buffer,0, count);
}
fos.flush();
}catch(IOException e) {
e.printStackTrace();
}finally{
closeSilently(is);
closeSilently(fos);
}
}
private static voidcloseSilently(Closeable closeable) {if (closeable == null) {return;
}try{
closeable.close();
}catch(Throwable e) {
e.printStackTrace();
}
}
}

以上代码分为四个逻辑部分:

1).  loadResources方法。通过反射创建AssetManager对象,调用addAssetPath方法,把插件Plugin1路径添加到这个AssetManager对象中。从此这个AssetManager就只为插件Plugin1服务了。在这个AssetManager对象的基础上,创建对应的Resource和Theme对象。

2). 重写Activity的getAsset,getResource和getTheme方法。重写逻辑见上面的代码。

3). 加载外部的插件,生成这个插件对应的ClassLoader。

4). 通过反射,获取插件中的类,构造出插件类的对象,然后就可以让插件类读取插件中的资源了。

基于这个思路,我们可以尝试使用插件资源替换当前显示的内容,实现换肤效果,核心思想是一样的,这里就不过多赘述了。