本篇博客说一下我们的宿主APP怎样加载别的APK文件。
首先需要说一些知识点,我们的Java文件要想在Android环境运行,需要将.java文件通过转为class文件,然后为了能在DVM上面运行,再转为dex文件。同理反过来,我们在代码中要操作的基本都是class文件,但是class文件怎么来呢? 从DexClassLoader加载获取。
DexClassLoader和PathClassLoader什么区别呢?

  1. DexClassLoader可以加载jar/apk/dex,可以从SD卡中加载未安装的apk
  2. PathClassLoader只能加载系统中已经安装过的apk

至于具体源码区别:建议读一下DVM源码。本篇不再赘述,之后专门写一篇博客讲述DexClassLoader和PathClassLoader的区别。

**

加载外部APK

**
其实这个场景是这样的:

  1. 从服务器下载APK,保存在我们的手机储存卡内
  2. 读取APK文件,然后生成对应的DexClassLoader
  3. 通过DexClassLoader的loadClass方法读取插件APK dex中的任何一个类。

说干就干,首先我们创建一个项目MyPluginProject,在这个项目中创建一个Java类:TestModel

/**
 * author: liumengqiang
 * Date : 2019/7/27
 * Description :
 */
public class TestModel {

    private String name;

    public void setName(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }
}

然后打包此项目生成:app-debug.apk。
由于插件APK基本都是从服务器下载,为了模拟这个场景,我们需要在宿主项目中新建一个assets文件,将插件APK复制进去,然后在复制到宿主APP的data/data/files文件夹下。
注意:这里之前钻牛角尖了,就是为什么我不直接手动将插件APK直接复制到data/data/files,文件夹下呢? 说干就干,但是问题来了,我在文件管理器的当前Android/data/<包名>下找不到此包名,也就是说没有生成包路径。 说实话卡了很长时间,我一直以为是不是版本问题,最后求助朋友,折腾了一番,最终手动调用:getExternalCacheDir即可解决。生成路径是生成了,那么接下来就是复制APK了吧,当我复制到里面之后,我发现,尼玛,死活获取不到复制进去的插件APK,真的是活见鬼,最后猛然发现那个包路径是系统路径!!!这个需要系统签名权限才能有权限访问! 而我们代码中assets中的APK是复制到内存中。。。
然后在我们的宿主项目中,新建assets,然后将app-debug.apk复制进去。接下来就是将app-debug.apk加载到内存中。

    /**
     * 把Assets里面得文件复制到 /data/data/files 目录下
     *
     * @param context
     * @param sourceName
     */
    public static void extractAssets(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 = new FileOutputStream(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);
        }

    }

然后在APP启动的时候调用:

extractAssets(this, "app-debug.apk");

接下来创建DexClassLoader ,加载插件app-debug.apk中的dex:

        try {
            File extractFile = this.getFileStreamPath("app-debug.apk");
            String dexpath = extractFile.getPath();
            File fileRelease = getDir("dex", 0); //0 表示Context.MODE_PRIVATE

            classLoader = new DexClassLoader(dexpath,
                    fileRelease.getAbsolutePath(), null, getClassLoader());
        } catch (Throwable e) {
            e.printStackTrace();
        }

然后就是我们通过反射获取TestModel类,并且设置setName,获取getName:

   private void modelTestClick() {
        try {
            Class<?> loadClass = classLoader.loadClass("com.liumengqiang.mypluginproject.TestModel");
            Object newInstance = loadClass.newInstance();
            Class[] paramClass = new Class[]{String.class};
            Object[] valueObj = new Object[]{"Hello world"};
            Method setName = loadClass.getMethod("setName", paramClass);
            setName.setAccessible(true);
            setName.invoke(newInstance, valueObj);

            Method getName = loadClass.getMethod("getName");
            getName.setAccessible(true);
            String value = (String) getName.invoke(newInstance);
            Toast.makeText(this, "Value:" + value, Toast.LENGTH_SHORT).show();
        } catch (Throwable e) {
            e.printStackTrace();
        }
    }

至此就调用到了插件中的类。

细看上面的代码丑得一逼,每次都要通过反射获取类里面的方法,如果这个类很多方法,那代码量可想而知。可以升级一下:面向接口编程

**

面向接口编程

**
面向接口编程是面向对象编程的一种实现方式,它的核心思想是将抽象与实现分离,从组件的级别来设计代码,达到高内聚低耦合的目的。最简单的面向接口编程方法是,先定义底层接口模块,再定义高层实现模块。具体的之后在单独开博客讲述。
上述代码优化的思路:

  1. 创建一个interfaceLibrary, 是个Library,里面定义一个接口:IBaseInterface :
/**
 1. author: liumengqiang
 2. Date : 2019/8/20
 3. Description :
 */
public interface IBaseInterface {
    void setName(String name);

    String getName();
}
  1. 然后插件MyPluginProject以及宿主项目分别依赖此Library
  2. 对于插件MyPluginProject中TestModel类,实现IBaseInterface接口:
/**
 * author: liumengqiang
 * Date : 2019/7/27
 * Description :
 */
public class TestModel implements IBaseInterface {

    private String name;

    @Override
    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String getName() {
        return name;
    }
}

  1. 然后打包APK,按照上面的流程。
  2. 最后在宿主反射获取TestModel类的时候,就可以通过接口接收创建的TestModel类了。
        try {
            Class<?> loadClass = classLoader.loadClass("com.liumengqiang.mypluginproject.TestModel");
            IBaseInterface newInstance = (IBaseInterface) loadClass.newInstance();
            newInstance.setName("hello world");
            Toast.makeText(this, "" + newInstance.getName(), Toast.LENGTH_SHORT).show();
        } catch (Throwable e) {
            e.printStackTrace();
        }

是不是精简了看着。 这是简单的面向接口编程,也就是先定义接口:然后再实现。

Android开发交流:

更多推荐

Android 插件化开发——宿主APP加载APK插件