序:

1.本文是安卓插件化课程的第一篇

2.本篇文章主要讲解的是如何使用插件APK中的class文件,实现的功能是使用插件APK中的一个Util类,功能和原理都较为简单,有一定基础的同学可以跳过直接看下一章。

一:原理简述

1.JAVA中的三层ClassLoader

java中有三层classLoader,如下:

启动类加载器(Bootstrap ClassLoader):这个类加载器负责加载存放在<JAVA_HOME>\lib目录,或者被-Xbootclasspath参数指定的路径中存放的,且是JVM能够识别的类库。只有这个类加载器是使用C++语言实现,是JVM的一部分,其他类加载器全部由java语言实现,独立存在于JVM外部,且都继承于java.lang.Classloader抽象类。
扩展类加载器(Extension ClassLoader):负责加载<JAVA_HOME>\home\ext目录中,或者被java.ext.dirs系统变量所指定的路径中所有的类库,开发者可以直接使用此类加载器加载Class文件。
应用程序类加载器(Application ClassLoader):负责加载用户类路径(ClassPath)上的所有类库,开发者同样可以直接使用此类加载器,如果没有指定,则默认使用此类加载器。

2.安卓中的类加载机制和java中是类似的,但只有两层

在安卓中,classLoader只有两层,没有扩展类加载器。分别为应用程序类加载器和启动类加载器。

应用程序类加载器有两种,分别为PathClassLoader和DexClassLoader。系统默认使用的是PathClassLoader来加载APP中的dex的。

两者的区别就是PathClassLoader只能加载固定目录下的apk文件,而DexClassLoader没有限制,加载APK或者dex文件都可以, 并且没有目录的限制。

3.利用DexClassLoader来实现类加载

所以我们可以利用DexClassLoader来初步的实现一个很小的插件化的功能。创建一个DexClassLoader去动态的加载一个APK文件,然后通过这个DexClassLoader去获取我们的目标类,然后通过反射调用类里面的方法。这样,一个小的插件化功能就完成。

二:执行流程

1.基础介绍

每次都重新创建一个demo项目比较麻烦,所以这次做实验的对象还是我一直使用的DemoClient项目,项目地址:https://github.com/aa5279aa/android_all_demo

或者直接跳转第四章,欢迎fork或star。

2.创建插件项目

第一步,在工程下,创建一个module,类型为application。MainActivity什么的都是默认的就好。

第二步,创建一个PluginUtil类,等会我们调用的就是这个类里面的内容。

第三部,PluginUtil类内容如下,简单的两个方法,字符串拼接和两数求和。

package com.xt.appplugin.util;

public class PluginUtil {


    public static String pluginMethodStaticSplicing(String str1, String str2) {
        return str1 + str2;
    }

    public static int pluginMethodAdd(int int1, int int2) {
        return int1 + int2;
    }
}

3.生成拷贝插件APK

1.点击切换项目,APP切换到appplugin

android插件化类名一样 android插件化加载另一个app_java

2.点击run,运行完项目后。apk文件就生成了,位置在build/outputs/apk/debug/里面。

android插件化类名一样 android插件化加载另一个app_加载_02

3.把这个apk文件拷贝到app项目的asset文件夹下:

android插件化类名一样 android插件化加载另一个app_android_03

4.代码编写

基于通用模版,创建DynamicFragment类,以后插件化的工作都会在这个类里面进行。

1.首先我们要把插件APK拷贝到SD卡上

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        //拷贝apk到data文件夹下
        Thread {
            context?.let {
                val file = File(it.filesDir.absolutePath + File.separator + APK_NAME)
                val open = context?.assets?.open(APK_NAME) ?: return@let
                FileUtil.copyfile(open, file, true)
                loadDone = true;
            }
        }.start()
    }

2.加载插件:

这里我们首先获取到apk地址,然后基于apk地址创建一个odex目录,然后就生成可以一个DexClassLoader了。

我们记录下这个生成的classLoader,这就算加载成功了,很简单吧。

if (position == 0) {
            val apkPath = context.filesDir.absolutePath + File.separator + APK_NAME
            val odexPath = context.filesDir.absolutePath + File.separator + APK_ODEX
            selfClassLoader = DexClassLoader(apkPath, odexPath, null, javaClass.classLoader)
            ObjectCache.getInstance().setParams(MyInstrumentation.ClassLoader, selfClassLoader)
            return
        }

3.使用插件中的类。

通过创建的classLoader取加载目标类,获取到类之后反射获取方法,然后执行。

if (position == 1) {
            if (selfClassLoader == null) {
                return
            }
            selfClassLoader?.let {
                val loadClass = it.loadClass("com.xt.appplugin.util.PluginUtil")
                LogUtil.logI("loadClass.name:${loadClass.name}")
                val method: Method = loadClass.getDeclaredMethod(
                    "pluginMethodStaticSplicing",
                    String::class.java,
                    String::class.java
                )
                val invoke = method.invoke(null, "Hello", "World")
                (invoke as? String)?.let { it1 ->
                    showResult(it1)
                }
                return
            }
        }

5.测试验证

点击加载插件后,在点击调用插件中的方法。

因为我们的输入值是Hello和World,所以最终显示的内容就是HelloWorld。

android插件化类名一样 android插件化加载另一个app_加载_04

三:总结

挺简单的,核心就是DexClassLoader可以加载apk或者dex文件。

但是仅加载插件APK中的class又有什么用呢?

当然有用,而且用处很大,这里可以加载的并不只是一个工具类,理论上只要不涉及到资源和Activity这些需要在manifest注册的东西,我们都可以动态去加载了。

我们项目中如果是严格按照MVC,MVP或者MVVM分层的,这里的M指的是Model,代表着数据来源,并且是和页面脱钩的,那么我们是不是在宿主中,实现其他层级的逻辑,以及定义好Model层的接口,而实现类则放到插件当中?如果这样,取数据的逻辑变了,或者换服务接口了,我们就可以通过动态发一版插件的方式来解决,而避免了发版。

我之前任职的公司,最开始的动态发布也就是这么做的。在宿主中封装好UI层,以及定义好数据层的接口。而取数据的逻辑很复杂,并且很容易出BUG,所以我们则放到了插件当中。每次启动之后动态的去加载插件,在去加载这些数据逻辑的代码,并且轻松就能实现热更新。

四。代码地址:

项目地址:https://github.com/aa5279aa/android_all_demo

插件项目位置:https://github.com/aa5279aa/android_all_demo/tree/master/DemoClient/appplugin

调用类位置:https://github.com/aa5279aa/android_all_demo/blob/master/DemoClient/app/src/main/java/com/xt/client/fragment/DynamicFragment.kt