Android 插件化之—— 加载插件中的资源

Android 资源分类:

  1. res目录下的资源
    res目录下的资源可以通过Resource对象进行访问,通过分析Resource源码可知,Resource访问res目录下的资源其实还是调用的AssetManager类的方法访问的。
  2. 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加载插件中的资源有两种方式:

  1. 将插件资源和宿主资源进行合并,这种方式访问插件中的资源就比较方便,可以像访问宿主中的资源一样的访问插件中的资源, 但是这种方式存在插件中的资源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,是一种很好的解决方案。

  2. 由于加载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()方法即可。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值