0
点赞
收藏
分享

微信扫一扫

基于类加载器的“插件”架构

用一个例子来说明DexClassLoader 的使用方法 ,假设有两个APK,第一个叫Host端(个人测试AndroidFace),一个Plugin端(个人测试Demo)。在Plugin 端,定义一个类PluginClass ,该类定义一个函数叫 function1.

    package com.zcwfeng.demo;
    import android.util.Log
    public class PluginClass {
        public PluginClass() {
            Log.e("PluginClass","PluginClass client initialied");
        }
        public int function1(int a,int b){
     return a + b;
     }
    }

接下来,我们的做法是,在Host端AndroidFace中使用DexClassLoader 动态装载PluginClass类,并调用其function1函数
新建Host工程AndroidFace,在主Activity中调用如下代码

// 测试调用DexClassLoader
public void useDexClassLoader(){
    Intent intent = new Intent("com.zcwfeng.demo",null);
    intent.setComponent(new ComponentName("com.zcwfeng.demo","com.zcwfeng.demo.view.MainActivity"));


    PackageManager pm = getPackageManager();
    final List<ResolveInfo> plugins = pm.queryIntentActivities(intent,0);
    Log.e("Host","plugins size:" + plugins.size());
    if(plugins.size() > 0) {
        ResolveInfo rinfo = plugins.get(0);
        ActivityInfo ainfo = rinfo.activityInfo;

        String div = System.getProperty("path.separator");
        String packageName = ainfo.packageName;
        String dexPath = ainfo.applicationInfo.sourceDir;
        String dexOutputDir = getApplicationInfo().dataDir;
        String libPath = ainfo.applicationInfo.nativeLibraryDir;

        DexClassLoader cl = new DexClassLoader(dexPath,dexOutputDir,libPath,this.getClass().getClassLoader());
        try {
            Class<?> clazz = cl.loadClass(packageName + ".PluginClass");
            Object obj = clazz.newInstance();
            Class[] params = new Class[2];
            params[0] = Integer.TYPE;
            params[1] = Integer.TYPE;
            Method action = clazz.getMethod("function1",params);
            Integer ret = (Integer) action.invoke(obj,12,34);
            Log.i("Host","return value is " + ret);
        } 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();
        }
    }
}

解释依稀DexClassLoader 的参数

  • dexPath 指的是目标类所在的APK或者Jar文件的路径。类加载器从该路径中寻找指定的目标类,路径必须是Apk或者jar的全路径,比如/data/data/com.zcwfeng.demo.plugin.apk。如果要包含多个路径,路径之间必须使用特定分隔符进行分隔,这个特定分隔符使用System.getProperty("path.separator")获得

  • dexOutputDir 由于dex 文件被包含在Apk或者jar中,因此加载目标类之前要从Apk或者jar中解压出dex文件,该参数就是指定dex文件的存放路径。一个应用程序一般对应一个Linux 用户id,应用程序金属与自己的数据目录系统中,因此该参数可以使用该程序的数据路径,我这里是/data/data/com.zcwfeng.androidface就是host的apk

  • libPath 指目标类中所使用的c/c++库存放的路径

  • 最后一个参数指的是该装载器的父类装载器,一般是指当前执行类的装载器

代码最终输出:
AndroidFace host端

2019-08-14 21:07:18.061 7134-7134/com.zcwfeng.face I/Host: return value is 46

Demo client端

2019-08-14 21:07:18.061 7134-7134/com.zcwfeng.face I/PluginClass: PluginClass client initialied

基于类装载器实际一种 “插件” 架构

改进上面的结构,定义interface 接口,interface 仅仅定义函数的输入和输出,不定义具体实现。该接口存在Host和Plugin两个项目,也就是该类同事参与连个项目的编译

添加Comm接口

package com.zcwfeng.demo;
public interface Comm {
    public int function1(int a,int b);
}

Plugin 不需要改动,只要在实现类实现Comm接口即可

    DexClassLoader cl = new DexClassLoader(dexPath,dexOutputDir,libPath,this.getClass().getClassLoader());
        try {
            Class<?> clazz = cl.loadClass(packageName + ".PluginClass");
            Comm comm = (Comm)clazz.newInstance();
            Integer ret = (Integer) comm.function1(12,34);
            Log.i("Host","return value is " + ret);


        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        }

Host 可以去掉动态Method反射可以提高效率,减少代码量

一般的做法,我们会把公共接口打包成jar。

  • 接口类一般被定义在Host项目中,比如本例中的Comm.java

  • Plugin 项目中需要引用Comm 类时,则必须通过一个外部的jar包,并且该jar包必须是以Library的方式被添加到Plugin项目的build path路径中的,而不能用“外部jar”方式。原因,外部jar虽然报名相同但是验证码不同的类,会导致出错

  • 再该代码结构宿主程序Host要想知道系统中有哪些插件,可以定义一个特定的action字符串,这里使用“com.zcwfeng.demo.MYACTION" 每一个插件内部必须定义一个空Activity,并在ActivityManifest.xml 文件声明对改action的处理,如下:

      <activity android:name=".Client"   android:launchMode="standard">
          <intent-filter>
              <action android:name="com.zcwfeng.demo.MYACTION" />
          </intent-filter>
      </activity>
    

这样宿主程序就可以查看PackageManager类queryIntentActivities()查询相关插件列表了。为了保证宿主程序和插件的兼容性,插件要定义一些版本号和相关名称信息。可以放在插件Plugin的res目录中,比如插件成续res/values/string.xml 定义相关信息。

        Resources res = pm.getResourcesForApplication(packageName);
        int id = 0;
        id = getResources().getIdentifier("version","string",packageName);
        String version = res.getString(id);

总结: 想一下这种结构其实挺巧妙,很灵活。
有两种思路:
第一种,系统主题架构。Android中的theme概念并不是真正的系统主题,所有的系统图标可以通过一种主题文件进行切换,比如状态栏上的图标,各种UI控件的图标桌面等。设计思路就是把每一种主题文件作为一个插件,在系统设置中可以使用不同的插件作为当前主题,然后修改Framework中读取Resources资源相关代码,达到切换的目的

第二种,AppStore架构。标准的Android电子商店程序只能连接Google的服务,但我们很多厂商希望提供独立的应用商店,如果让用户安装多个商店客户端显然是比较麻烦的。因此可以设计一个通用电子商店客户端,并定义一种标准的和电子商店服务器通信的机制。把这种机制定义为一个或多个interface文件,不同厂商只要实现这些interface即可。然后把实现后的程序作为插件安装到手机中,从而让用户选择不同的插件,而不再需要安装多个客户端。

举报

相关推荐

类加载 类加载器 详细

0 条评论