Android 插件化原理(一),通过dex文件调用插件app代码

Android插件化原理,从以下三个问题切入:

  1. 什么是插件化
  2. 如何实现插件类的加载
  3. 如何实现插件资源的加载

什么是插件化

插件化技术最初是源于免安装运行APK的想法,这个免安装的APK就可以理解为插件,而支持插件的app,则称之为宿主;一方面减小了安装包的大小,另一方面可以实现 app 功能的动态扩展

  • 插件化解决的问题

  1. APP的功能越来越多,体积越来越大
  2. 模块之间的耦合度高,协同开发的沟通成本越来越大
  3. APP功能变多之后,导致方法数可能会超过65535,APP占用内存过大
  4. 应用之间的相互调用
  • 插件化与组件化的区别

  • 组件化开发就是将一个APP拆分成多个模块,每一个模块都是一个组件,在开发的过程中可以让这些组件相互依赖或者单独调试部分组件等,但是最终发布的时候是将这些组件合并为一个APK,这就是组件化开发。
  • 插件化和组件化略有不同,插件化是将整个APP拆分成多个模块,这些模块包括一个宿主和多个插件,每个模块都是一个单独的APK,最终打包的时候,宿主APK和插件APK分开打包。(最终online部署发布时,可以根据客户需求选择只发布宿主APK,或者是发布宿主APK和其中需要用到的插件APK)
  • 各大插件化对比

特性

 

DynamicAPK

dynamic- load-apk

 

Small

 

DroidPlugin

 

RePlugin

 

VirtualAPK

作者

携程

任玉刚

wequick

360

360

滴滴

支持四大组件

只支持Activity

只支持Activity

只支持

Activity

全支持

全支持

全支持

组件无需在宿主manifest 中预注册

 

×

 

 

 

 

 

插件可以依赖宿主

×

支持PendingIntent

×

×

×

Android特性支持

大部分

大部分

大部分

几乎全部

几乎全部

几乎全部

兼容性适配

一般

一般

中等

插件构建

部署aapt

Gradle

Gradle

Gradle插件

 

在选择开源框架的时候,需要根据自身的需求来,如果加载的插件不需要和宿主有任何耦合,也无须和宿主进
行通信,比如加载第三方 App,那么推荐使用 RePlugin,其他的情况推荐使用 VirtualApk

插件化的实现

如何去实现插件化呢?插件是免安装的,那么要思考几个问题:

  1. 插件也是APK,APK里面有资源和类,那么需要考虑的问题无非就是两个
    1. 如何加载插件资源
    2. 如何加载插件类
  2. 要加载Android类,as we all konw,四大组件是需要在AndroidManifest.xml中注册的,要在宿主中调用插件的四大组件,显然插件APK中的用到的四大组件相关类,是没有在宿主的AndroidMainfest.xml中注册过的,那么怎么通过宿主去调用插件的四大组件
  • 类加载

Java中会通过javac命令将.java文件编译生成.class文件,jvm会加载class文件,解析之后初始化,然后就可以运行了;Android中会将代码编译成一个APK,解压APK可以看到一个或者多个.dex文件,dex文件是DVM的可执行文件,dex就是把class合并优化后生成的(odex);下面我们来学习DVM如何加载dex文件。

  • ClassLoader

Android中是通过ClassLoader加载dex文件的,ClassLoader是一个抽象类,实现主要分为两种类型:系统类加载器和自定义加载器,其实按照我的理解,系统类加载器主要就是BootClassLoader,DexClassLoader和PathClassLoader

  1. BootClassLoader,用于加载Android Framework层class文件。
  2. PathClassLoader,用于Android应用程序类加载器。可以加载指定的dex,以及jar、zip、apk中的classes.dex
  3. DexClassLoader,用于加载指定的dex,以及jar、zip、apk中的classes.dex

类继承关系如下图:

Android 插件化原理(一),通过dex文件调用插件app代码_第1张图片


我们先来看下 PathClassLoader 和 DexClassLoader。

注意:有些帖子可能会有如下说法:

DexClassLoader:能够加载未安装的jar、apk、dex等;而PathClassLoader只能够加载系统中已经安装的apk;这个说法是有问题的,或者说针对Android8.0之后的系统,这个说法是错误的

在8.0(API 26)之前,它们二者的唯一区别是第二个参数 optimizedDirectory,这个参数的意思是生成的 odex(优化的dex)存放的路径。在8.0(API 26)及之后,二者就完全一样了。

看Android源码

// API26之后和API26之前的PathClassLoader的源码是相同的
// /libcore/dalvik/src/main/java/dalvik/system/PathClassLoader.java
public class PathClassLoader extends BaseDexClassLoader {
    // optimizedDirectory 直接为 null
    public PathClassLoader(String dexPath, ClassLoader parent) {
        super(dexPath, null, null, parent);
    }
    // optimizedDirectory 直接为 null
    public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
        super(dexPath, null, librarySearchPath, parent);
    }
}

// API 小于等于26 /libcore/dalvik/src/main/java/dalvik/system/DexClassLoader.java
public class DexClassLoader extends BaseDexClassLoader {
    //从API26开始,super的构造方法,也就是BaseDexClassLoader的构造方法有变动
    public DexClassLoader(String dexPath, String optimizedDirectory,
            String librarySearchPath, ClassLoader parent) {
        super(dexPath, new File(optimizedDirectory), librarySearchPath, parent);
    }
}
// API 大于26 /libcore/dalvik/src/main/java/dalvik/system/DexClassLoader.java
public class DexClassLoader extends BaseDexClassLoader {
    /**
     * @param optimizedDirectory this parameter is deprecated and has no effect since API level 26.
    */
    public DexClassLoader(String dexPath, String optimizedDirectory,
            String librarySearchPath, ClassLoader parent) {
        //可以看到第二个参数optimizedDirectory直接为null,已经和PathClassLoader的构造方法一样了
        super(dexPath, null, librarySearchPath, parent);
    }
}

// API 26/libcore/dalvik/src/main/java/dalvik/system/BaseDexClassLoader.java
public BaseDexClassLoader(String dexPath, File optimizedDirectory,String librarySearchPath, ClassLoader parent) {
    super(parent);
    // DexPathList 的第四个参数是 optimizedDirectory,可以看到这儿为 null
    this.pathList = new DexPathList(this, dexPath, librarySearchPath, null);
}

// API 25/libcore/dalvik/src/main/java/dalvik/system/BaseDexClassLoader.java
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
            String librarySearchPath, ClassLoader parent) {
        super(parent);
        this.pathList = new DexPathList(this, dexPath, librarySearchPath, optimizedDirectory);
}

上述源码可以看出,PathClassLoader和DexClassLoader都是继承自BaseDexClassLoader的,而且他们两个都只有构造函数,其类加载相关逻辑全都是在父类BaseDexClassLoader中。

唯一的区别就是第二个参数optimizedDirectory有没有使用,从API26,也就是8.0开始,他们两个的构造函数完全一样,已经没有任何区别,故DexClassLoader能干的事,PathClassLoader也能干

在搞清楚了PathClassLoader、DexClassLoader和BaseDexClassLoader之间的关系之后,我们写一段代码,研究一下PathClassLoader和BootClassLoader之间的关系。

在AS中创建一个Android项目,创建完成之后,再New 一个Module,项目的Project结构视图如下:

Android 插件化原理(一),通过dex文件调用插件app代码_第2张图片

在宿主的MainActivity的onCreate方法中加入下代码:

@Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // Example of a call to a native method
        TextView tv = findViewById(R.id.sample_text);
        tv.setText("xxxxxxx");

        printClassloader();
        //invokePluginMethod();
        //invokePluginByLoadAPK();

    }

    private void printlassCloader() {
        ClassLoader classLoader = getClassLoader();
        while (classLoader != null) {
            Log.d(TAG, "printlassCloader classloader = " + classLoader);
            classLoader = classLoader.getParent();
        }
    }

输出结果如下:

01-13 11:44:15.513 13212-13212/com.enjoy.myplugin D/Rayman: printClassloader classloader = dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/com.enjoy.myplugin-1/base.apk", zip file "/sdcard/plugin-debug.apk"],nativeLibraryDirectories=[/data/app/com.enjoy.myplugin-1/lib/arm64, /vendor/lib64, /system/lib64]]]
01-13 11:44:15.513 13212-13212/com.enjoy.myplugin D/Rayman: printClassloader classloader = java.lang.BootClassLoader@15bc8d9c

从打印结果来看,当前应用程序的类加载器,或者说MainActivity的ClassLoader就是PathClassLoader,而PathClassLoader的parent属性的值是BootClassLoader,需要说明的是,这儿的parent并不是父类,parent是ClassLoader类本身的一个成员属性。

那么在修改一下printClassLoader方法,看看Activity和AppCompactActivity的类加载器具体都是什么:

 private void printClassloader() {
        ClassLoader classLoader = getClassLoader();
        while (classLoader != null) {
            Log.d(TAG, "printClassloader classloader = " + classLoader);
            classLoader = classLoader.getParent();
        }
        Log.d(TAG,"activity clsloader = "+ Activity.class.getClassLoader());
        Log.d(TAG,"appcompactactivity clsloader = "+ AppCompatActivity.class.getClassLoader());
    }

打印结果如下:

01-13 11:55:37.838 13435-13435/? D/Rayman: printClassloader classloader = dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/com.enjoy.myplugin-2/base.apk", zip file "/sdcard/plugin-debug.apk"],nativeLibraryDirectories=[/data/app/com.enjoy.myplugin-2/lib/arm64, /vendor/lib64, /system/lib64]]]
01-13 11:55:37.838 13435-13435/? D/Rayman: printClassloader classloader = java.lang.BootClassLoader@15bc8d9c
01-13 11:55:37.838 13435-13435/? D/Rayman: activity clsloader = java.lang.BootClassLoader@15bc8d9c
01-13 11:55:37.838 13435-13435/? D/Rayman: appcompactactivity clsloader = dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/com.enjoy.myplugin-2/base.apk", zip file "/sdcard/plugin-debug.apk"],nativeLibraryDirectories=[/data/app/com.enjoy.myplugin-2/lib/arm64, /vendor/lib64, /system/lib64]]]

从打印结果可知:

正好印证了,前面谈到的BootClassLoader和PathClassLoader各自的作用范围

  1. BootClassLoader,用于加载Android Framework层class文件。
  2. PathClassLoader,用于Android应用程序类加载器。可以加载指定的dex,以及jar、zip、apk中的classes.dex

说到这儿,可能会有些疑问:

为什么Activity和AppCompactActivity的类加载器不同,需要说明的是BootClassLoader用于加载Android Framework中的类,或者说是BootClassLoader加载的类一定是在Android SDK中的,而AppCompactActivity并不是在SDK中,而是我们在创建项目的时候添加的依赖库,在build.gradle中添加的dependencies,

implementation 'androidx.appcompat:appcompat:1.1.0'
  • 如何实现类加载

如何利用ClassLoader去加载一个类呢?其实非常简单,在之前创建的项目中有个Module是plugin,在plugin中添加一个java类,叫做PluginTest,如下图:

Android 插件化原理(一),通过dex文件调用插件app代码_第3张图片

PluginTest的代码如下:

package com.enjoy.plugin;

import android.util.Log;

public class PluginTest {

    private final static String TAG = "Rayman";

    private void ShowPluginMsg(String msg){
        Log.d(TAG,"PluginTest ShowPluginMsg msg = "+msg);
    }
}

现在我们有一个apk文件,路径是 apkPath,然后里面有个类 com.enjoy.plugin.PluginTest,可以参照如下代码,实现对PluginTest类的加载,在宿主APP的MainActivity中实现如下方法:

private void invokePluginMethod() {
        //在这里我们需要先生成PluginTest.dex文件
        DexClassLoader dexClassLoader = new DexClassLoader("/sdcard/PluginTest.dex",
                getCacheDir().getAbsolutePath(), null, getClassLoader());
        //注意此处如果替换为PathClassLoader也是可以正常执行的
        PathClassLoader pathClassLoader = new PathClassLoader("/sdcard/PluginTest.dex",
                null,getClassLoader());
        try {
            //通过DexClassLoader加载PluginTest类
            Class clz = dexClassLoader.loadClass("com.enjoy.plugin.PluginTest");
            //用DexClassLoader或者PathClassLoader都是可以的
            //Class clz = pathClassLoader.loadClass("com.enjoy.plugin.PluginTest");
            //通过反射调用PluginTest类中的方法
            Object obj = clz.newInstance();
            Method method = clz.getDeclaredMethod("ShowPluginMsg",String.class);
            method.setAccessible(true);
            method.invoke(obj,"This is invoke Plugin method...");
        } catch (Exception e) {
            e.printStackTrace();
        }

}

生成dex文件的步骤:

  1. 首先在AS中编译plugin这个Module,生成相应的.class文件,即生成PuginTest.class文件,.class的路径如下:Android 插件化原理(一),通过dex文件调用插件app代码_第4张图片Android 插件化原理(一),通过dex文件调用插件app代码_第5张图片
  2. 在Android SDK路径下找到dx.bat(Windows系统)这个文件,也就是找到自己的SDK路径,dx.bat在Android\Sdk\build-tools\28.0.3(这儿是Android SDK版本,也有可能是27.0.3或者25.0.2等),可参考如下截图:Android 插件化原理(一),通过dex文件调用插件app代码_第6张图片

将这个路径配置到环境变量,打开cmd,然后用如下命令生成dex文件:

dx --dex --output=output.dex input.class

使用这个命令需要注意,一定要在包名的外层目录执行,比如要生成前面提到的PluginTest.dex,需要按照下面方式执行,在cmd中先进入PluginTest.class文件的包名外层目录,如下:

进入到这儿之后,执行dx --dex --output=PluginTest.dex com\enjoy\plugin\PluginTest.class

执行完上述命令,就会在MyPlugin\plugin\build\intermediates\javac\debug\classes这个目录下生成PluginTest.dex文件

注意:

执行dx命令,只能在包名的外层目录执行,比如说在cmd里面直接进入到.class的目录MyPlugin\plugin\build\intermediates\javac\debug\classes\com\enjoy\plugin,那么在执行dx命令是会报错,无法生成相应的dex文件。

OK,到这儿dex文件就生成了,也就是生成了ClassLoader可以加载的dex文件,亦即DVM可以执行的dex文件。

接下来只需将PluginTest.dex拷贝或者push到手机的/sdcard中,或者是Android虚拟机的/sdcard目录下(/sdcard/PuginTest.dex),然后执行上面已经写好的invokePluginMethod方法,这里我直接在MainActivity的OnCreate方法中调用:

@Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // Example of a call to a native method
        TextView tv = findViewById(R.id.sample_text);
        tv.setText(stringFromJNI());

        printClassloader();
        invokePluginMethod();
        

    }

运行结果如下:

01-13 14:43:48.050 13645-13645/? D/Rayman: printClassloader classloader = dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/com.enjoy.myplugin-1/base.apk", zip file "/sdcard/plugin-debug.apk"],nativeLibraryDirectories=[/data/app/com.enjoy.myplugin-1/lib/arm64, /vendor/lib64, /system/lib64]]]
01-13 14:43:48.050 13645-13645/? D/Rayman: printClassloader classloader = java.lang.BootClassLoader@15bc8d9c
01-13 14:43:48.050 13645-13645/? D/Rayman: activity clsloader = java.lang.BootClassLoader@15bc8d9c
01-13 14:43:48.050 13645-13645/? D/Rayman: appcompactactivity clsloader = dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/com.enjoy.myplugin-1/base.apk", zip file "/sdcard/plugin-debug.apk"],nativeLibraryDirectories=[/data/app/com.enjoy.myplugin-1/lib/arm64, /vendor/lib64, /system/lib64]]]
01-13 14:43:48.054 13645-13645/? D/Rayman: PluginTest ShowPluginMsg msg = This is invoke Plugin method...

LOG中的最后一句,就是我们在PluginTest.java中打印出来的,到这儿说明在宿主APP中成功的调用了插件APP中的代码。

需要知道插件代码的包名,类名,方法名,以及参数,并不需要安装插件APP,就可以通过ClassLoader加载dex文件,再利用反射调用插件代码,只需要dex文件就实现了免安装调用app代码的功能。

本章节讲述的是通过dex文件加载调用插件APP的代码,到这儿就暂时结束了,下一章节会讲述ClassLoader类加载机制,双亲委托机制,不用生成dex文件,只需要插件APK就可以通过ClassLoader和反射调用插件APP的代码。

下一章节链接:https://blog.csdn.net/qq_31429205/article/details/103959814

你可能感兴趣的:(Android 插件化原理(一),通过dex文件调用插件app代码)