Android 资源分类:
- res目录下的资源
res目录下的资源可以通过Resource对象进行访问,通过分析Resource源码可知,Resource访问res目录下的资源其实还是调用的AssetManager类的方法访问的。 - assets目录下的资源
assets目录的访问,通常在Activity中,是获取到AssetManager对象后,使用AssetManager来访问asset中的资源文件。Activity中获取AssetManager对象的方法是getAssets(),这个方法是ContextThemeWrapper类的方法,下面是ContextThemeWrapper类的getAssets方法的具体实现:
@Override
public AssetManager getAssets() {
// Ensure we're returning assets with the correct configuration.
return getResourcesInternal().getAssets();
}
@Override
public Resources getResources() {
return getResourcesInternal();
}
从上面的代码中,可以发现,这个方法内部是调用了getResourcesInternal()方法,getResourcesInternal()这个方法返回了一个Resource对象,再调用Resource对象的getAssets()方法来来返回一个AssetManager对象,这样ContextThemeWrapper的getAssets方法获取的AssetManager对象,其实就是通过Resource方法的getAssets方法返回的。
通过上面对两个不同目录下的资源文件的加载,可以知道,加载apk中的资源其实本质都是通过AssetManager来完成的。app在启动时,会将apk所在的path传给AssetManager的addAssetPath方法,这样,就可以通过R.id.XXX,来访问资源了,具体访问资源是通过AssetManager内部的一个NDK方法完成的,之所以能够通过R.id.XXX的方式访问资源,是因为在打包成apk的过程中,会为每一个资源在R文件中生成一个十六进制的值,同时也会生成一个resources.arsc的文件,这个文件就是一个hash表,里面记录了十六进制的id和资源的对应关系。
所以,如果要加载插件apk中的资源,就可以通过AssetManager来完成。由于AssetManager的addAssetPath方法无法直接使用,需要通过反射了调用。
宿主app加载插件中的资源有两种方式:
-
将插件资源和宿主资源进行合并,这种方式访问插件中的资源就比较方便,可以像访问宿主中的资源一样的访问插件中的资源, 但是这种方式存在插件中的资源id和宿主中的资源id冲突的问题。
解决资源id冲突可以有下面三种解决方案:
方案A:
可以通过修改Android SDK目录下的AAPT命令来改变打包时,资源id的前缀。AAPT生成的资源值默认都是以0x7f开头的,可以用0x71 ~ 0xff 这个区间内的值作为前缀,在实战中,不会把0x00 和0x11这两个系统占用的值作为插件资源值的前缀,但是仍然不能随心所欲地使用任何其他值。一些手机厂商的操作系统会占用0x10 之类的前缀,如果我们的插件也使用了0x10 ,那么就又要产生冲突了。
方案B:
如果觉得修改AAPT命令麻烦,可以采用B计划,可以修改打包后的插件apk文件中的R文件和resources.arsc文件中的id的前缀。B计划这种解决方案,需要自定义Gradle插件。具体如何自定义Gradle插件解决资源id重复的问题,可以参考Small这个框架的gradle-smamll这个Gradle插件,具体的原理分析,会在后续的文章中给出。
方案C:
自己定一个public.xml的文件固定插件中的所有的资源,这种方案实现起来不现实,但是对于固定一个资源id,是一种很好的解决方案。 -
由于加载apk是通过AssetManager类的addAssetPath方法来完成的,所以,可以通过这种方式类加载插件中的资源。当给AssetManager的addAssetPath方法指定了插件的apk路径后,这样以后,这个插件中的资源就可以通过这个AssetManager来访问了。
下面通过一个案例演示,通过给AssetManager类的addAssetPath方法指定插件路径的方式来加载插件中的资源。
本示例的前提是,已经将插件apk复制到assets目录下。
第一步:首先将assets目录下的插件apk复制到一个内部路径下:
public static void copyApk2Inner(Context context,String apkName){
AssetManager assetManager = context.getAssets();
InputStream inputStream = null;
BufferedOutputStream bos = null;
try {
inputStream = assetManager.open(apkName);
File plugin_odex_dir = context.getDir("plugin_odex", Context.MODE_PRIVATE);
LogUtil.i("文件夹目录的路径是 "+plugin_odex_dir.getAbsolutePath());
String filePath = plugin_odex_dir.getAbsolutePath()+File.separator+apkName;
File file = new File(filePath);
if(file.exists()){
boolean delete = file.delete();
LogUtil.i("删除存在的插件apk "+delete);
}
//注意,这里是要传具体的文件路径构建的File对象,不是文件所在的文件夹的路径构建的File对象
bos = new BufferedOutputStream(new FileOutputStream(file));
byte[] bytes = new byte[1024];
int len = 0;
while ((len= inputStream.read(bytes))!=-1){
bos.write(bytes,0,len);
}
//完成了将插件apk从assets目录复制到apk内部的odex目录下面
if(file.exists()){
LogUtil.i("文件复制成功 "+file.getAbsolutePath());
}
} catch (IOException e) {
e.printStackTrace();
}finally {
if(null != bos){
try {
bos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(null != inputStream){
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
第二步:给AssetManager的addAssetPath方法指定插件apk的路径,构建专门复制加载这个插件apk的AssetManager对象pluginAssetManager,然后通过这个专门加载插件apk的AssetManager对象,来构建专门复制加载插件资源的Resource对象pluginResource和DexClassLoader这个加载这个插件中的类的类加载器。构建完专门加载这个插件的pluginAssetManager,pluginResource,dexClassLoader对象后,通过dexClassLoader的loadClass方法来获取插件中的资源相关的字节码文件,在通过反射获取到想要加载的资源的id,获取到插件中的资源id后,通过pluginResource对象,就可以加载插件中这个资源id对象的资源了。以下便是这个过程的具体实现:
public void loadPluginResource(){
File dir = getDir(HookHelper.PLUGIN_ODEX,Context.MODE_PRIVATE);
LogUtil.i(dir.getAbsolutePath());
String dexPath = dir.getAbsolutePath()+File.separator+"plugin1-debug.apk";
try {
AssetManager pluginAssetManager = AssetManager.class.newInstance();
Method addAssetPathMethod = AssetManager.class.getDeclaredMethod("addAssetPath",String.class);
addAssetPathMethod.setAccessible(true);
addAssetPathMethod.invoke(pluginAssetManager,dexPath);
//1.需要一个ClassLader,所以先构建一个ClassLoader
//public DexClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent) {
// super((String)null, (File)null, (String)null, (ClassLoader)null);
// throw new RuntimeException("Stub!");
//}
File optDir = getDir("dex",Context.MODE_PRIVATE);
String optimizedDirectory = optDir.getAbsolutePath();
DexClassLoader dexClassLoader = new DexClassLoader(dexPath,optimizedDirectory,null,getClassLoader());
Resources pluginResource = new Resources(pluginAssetManager,super.getResources().getDisplayMetrics(),super.getResources().getConfiguration());
//设置插件主题
Resources.Theme pluginTheme = pluginResource.newTheme();
pluginTheme.setTo(super.getTheme());
//获取插件中的app_name字段对应的id
Class<?> stringClazz = dexClassLoader.loadClass("com.android.skill.R$string");
Field appNameField = stringClazz.getField("app_name");
appNameField.setAccessible(true);
int appNameId = (int) appNameField.get(null);
LogUtil.i("appNameId= "+appNameId);
String pluingAppName = pluginResource.getString(appNameId);
ToastUtils.shortToast(this,pluingAppName);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
}
完成了上面两个步骤后,建议在自定义Application的attachBaseContext方法中调用copyApk2Inner方法
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
MultiDex.install(this);
//需要提前将assets目录下的插件apk复制到app的内部存储路径
HookHelper.copyApk2Inner(this,"plugin1-debug.apk");
}
本案例的前提是将一个插件apk文件已经复制到了asstes目录下了。接着在要使用插件资源的地方调用loadPluginResource()方法即可。