Android中插件化实现的原理,宿主app运行插件中的类 (一)

本文深入探讨插件化概念,解析插件化与组件化的区别,以及在Android环境中实现插件化的主要思路。通过加载插件类、资源和启动组件,文章详细介绍了类加载器在插件化中的作用,包括DexClassLoader、PathClassLoader和BaseDexClassLoader的工作原理,以及如何通过自定义类加载器加载外部插件。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

https://blog.youkuaiyun.com/lin20044140410/article/details/104584877

什么是插件化?

直白点说,就是去运行没有安装的apk,把这些没有安装过的apk理解为插件,把运行这些插件的apk称为宿主.

为什么需要插件化?

由于宿主可以在运行时动态去加载和运行插件,这就可以把apk中不常用的功能模块做成插件,减小了apk包的大小,实现了apk功能的动态扩展.

插件化和组件化的区别:

组件化,是把app分成多个模块,独立开发,根据需求,利用gradle配置模块间的依赖关系,但是最终发布时模块和app会打包成一个apk.

插件化,也是把app分成多个模块,这些模块中有一个宿主,多个插件,最终打包时宿主apk和插件apk可以分开打包,插件apk也可以在需要时由网络下载,然后加载运行.

插件化实现的思路:

1)加载插件的类

2)加载插件的资源

3)启动插件中的组件(四大组件)

这篇文章先来实现第一个问题:加载插件的类,这里插件的类,是普通的类,不是四大组件,四大组件的加载运行会放在后面的文章来讨论.

加载类就要用到类加载器,java中jvm加载的class文件,android中的虚拟机加载的dex文件(就是多个class文件的合并),这里主要看android中如何加载dex文件的.

android中的类加载分两种:系统类加载器,自定义的类加载器,他们的关系如图:

BootClassLoader 用于加载framework层的class文件

PathClassLoader 用于加载应用程序中class文件,这里也包括应用中依赖的库文件,具体形式可以是apk,jar,zip中的dex文件.

DexClassLoader 用于加载制定的dex文件.

在一个自动生成的app中,MainActivity.java 这个类是由PathClassLoader来加载的, 而android.app.Activity.java这个framework中类时由BootClassLoader加载的.

从源码看,DexClassLoader,PathClassLoader除了构造函数外,没有实现任何代码,类加载的工作都是由父类BaseDexClassLoader时完成的.

这里有一点需要注意的是:PathCalssLoader的parent类加载器是BootClassLoader,并不是其继承关系的父类.

在分析类加载流程之前,先看下类的生命周期:

加载 是 类加载过程中的一个阶段,在加载阶段,虚拟机要完成3件事情:

1)通过类的全限定名,获取类的二进制字节流

2)将这个字节流代表的静态存储结构,转化为方法区的运行时数据结构

3)在内存中生成一个代表这个类的java.lang.class对象,作为方法区这个类的各种数据的访问入口,这个Class对象是比较特殊的,虽然它是对象,却是放在方法区的.

下面就通过源码看下加载一个类的过程,从loadClass方法的实现看起,DexClassLoader,PathClassLoader及父类BaseDexClassLoader,一直往上查找,这个方法的实现在ClassLoader.java中.

   protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
            // First, check if the class has already been loaded
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                try {
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    c = findClass(name);
                }
            }
            return c;
    }

这里的实现就是双亲委派模型, 

1,首先去查找这个类是不是已经加载过,是就直接返回,否则,

2, 递归的去请求parent加载器去完成加载,在递归过程中,走到BootClassLoader后,就不会再请求其parent来加载,而是通过其findClass来加载,如果无法完成这个加载请求,就返回null.

3,如果都没有加载成功,才由自己来完成加载findClass, 

这个双亲委托机制,让类的加载有了一种优先级的层次关系,比如我们Activity这个类,不管是谁请求加载这个类,最终都是委托给这个模型最顶端的BootClassLoader来加载,所以activity在各种类加载环境中都是同一个类,如果没有这种机制,自己去定义类加载器,然后自己定义一个android.app.Activity类,那么系统中就会出现多个不同的Activity类,那就乱了.

从上面双亲委派模型的加载流程,如果要自己加载类,时调用findClass方法,下面看BaseDexClassLoader.java中的这个方法的实现:

(因为DexClassLoader,PathClassLoader都没定义这个方法,所以实际调用的是BaseDexClassLoader.java中的)

  public BaseDexClassLoader(ByteBuffer[] dexFiles, ClassLoader parent) {
        // TODO We should support giving this a library search path maybe.
        super(parent);
        this.pathList = new DexPathList(this, dexFiles);
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
        Class c = pathList.findClass(name, suppressedExceptions);
        if (c == null) {
            ClassNotFoundException cnfe = new ClassNotFoundException(
                    "Didn't find class \"" + name + "\" on path: " + pathList);
            for (Throwable t : suppressedExceptions) {
                cnfe.addSuppressed(t);
            }
            throw cnfe;
        }
        return c;
    }

它又是通过DexPathList来实现的.

DexPathList.java

    public Class<?> findClass(String name, List<Throwable> suppressed) {
        for (Element element : dexElements) {
            Class<?> clazz = element.findClass(name, definingContext, suppressed);
            if (clazz != null) {
                return clazz;
            }
        }

        if (dexElementsSuppressedExceptions != null) {
            suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
        }
        return null;
    }

dexElements是一个数组,其中的元素 Element里面包含了dex文件,如果app中有多个dex文件,这个数组就会有多个Element元素,每个Element元素中包含一个dex文件,

DexPathList#Element.java

        public Class<?> findClass(String name, ClassLoader definingContext,
                List<Throwable> suppressed) {
            return dexFile != null ? dexFile.loadClassBinaryName(name, definingContext, suppressed)
                    : null;
        }

最终是通过Element中的dexfile来获取类的二进制字节流.

在回头去看看dexElements这个数组怎么来的?

        // save dexPath for BaseDexClassLoader
        this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
                                           suppressedExceptions, definingContext, isTrusted);

从构造函数的回溯,可以看到dexElements中元素,来自与第一个参数dexpath,这个路径通常就是apk的路径,或者jar,zip,dex文件的路径,具体的值是有创建类加载器传入的参数确定.

分析到这里,应该能明白一点:如果要让app能加载插件的class文件,只要把插件的dex文件,也存入dexElement数组就OK 了.

通过代码验证,使用DexClassLoader,PathClassLoader能不能成功加载插件的类.

先定义一个插件module,新建测试类:

package com.test.plugintest;

import android.util.Log;

public class PlugInDemo {
    public static void testPlugIn() {
        Log.d("PlugIn","from plugin PlugInDemo.");
    }
}

把这个类打包成一个dex文件,具体做法是使用sdk下dx工具,把class文件打包成dex,

D:\NDKDemo\plugintest\build\intermediates\javac\debug\compileDebugJavaWithJavac\classes>
dx --dex --output=plugIn.dex com\test\plugintest\PlugInDemo.class
D:\NDKDemo\plugintest\build\intermediates\javac\debug\compileDebugJavaWithJavac\classes>

执行上面的命令,会在当前目录下生成一个plugIn.dex文件,这里有一点值得注意,执行dx命令的位置最好在classes这个目录,然后指定具体class文件时,使用全类名

把plugIn.dex拷贝到sdcard目录下,

    public void loadClassPlugIn() {
        DexClassLoader dexClassLoader = new DexClassLoader("/sdcard/plugIn.dex",
                MainActivity.this.getCacheDir().getAbsolutePath(),
                null,
                MainActivity.this.getClassLoader());
//        PathClassLoader pathClassLoader = new PathClassLoader("/sdcard/plugIn.dex",
//                MainActivity.this.getClassLoader());
        try {
            Class<?> clazz = dexClassLoader.loadClass("com.test.plugintest.PlugInDemo");
            Method method = clazz.getMethod("testPlugIn");
            method.invoke(null);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

在宿主App的MainActivity中,可以通过上面的ClassLoader加载PlugInDemo.java类,通过反射调用其中方法。

通过这个测试,只是去验证通过自定义类加载器可以去加载执行一个插件中的类.但是这个插件的类并没有合并到app中.

下面就来实现把插件的类,合并到宿主app中,实现步骤:

1,创建插件的DexClassLoader类加载器,然后反射拿到插件的dexElements数组

2,获取宿主的PathClassLoader类加载器,然后反射拿到宿主的dexElements数组

3,把宿主和插件的两个dexElements数组合并

DexPathList.java
   private Element[] dexElements;
BaseDexClassLoader.java
    private final DexPathList pathList;

dexElements在DexPathList中,而DexPathList在BaseDexClassLoader中,所以先获取类加载器,然后反射获取到DexPathList,在反射拿到dexElements.


    public void loadPlugInModule() {
        try {
//BastDexClassLoader,DexPathList,宿主和插件可公用
            Class<?> baseDexClassLoader = Class.forName("dalvik.system.BaseDexClassLoader");
            Field pathListField = baseDexClassLoader.getDeclaredField("pathList");
            pathListField.setAccessible(true);

            Class<?>  dexPathListClassLoader = Class.forName("dalvik.system.DexPathList");
            Field dexElementsField = dexPathListClassLoader.getDeclaredField("dexElements");
            dexElementsField.setAccessible(true);
            //创建插件的DexClassLoader类加载器,然后反射拿到插件的dexElements数组
            DexClassLoader plugInDexClassLoader = new DexClassLoader(
                    "/sdcard/plugintest-debug.apk",
                    mContext.getCacheDir().getAbsolutePath(),
                    null,
                    mContext.getClassLoader());
            //获取插件类加载器中DexPathList实例对象
            Object plugInDexPathList =  pathListField.get(plugInDexClassLoader);
            //拿到插件的dexElements数组
            Object[] plugInDexElements = (Object[]) dexElementsField.get(plugInDexPathList);

//获取宿主的PathClassLoader类加载器,然后反射拿到宿主的dexElements数组
            PathClassLoader hostPathClassLoader = (PathClassLoader) mContext.getClassLoader();
            //获取宿主类加载器中DexPathList实例对象
            Object hostDexPathList =  pathListField.get(hostPathClassLoader);
            //拿到宿主的dexElements数组
            Object[] hostDexElements = (Object[]) dexElementsField.get(hostDexPathList);

            //创建新的DexElements数组
            Object[] dexElements = (Object[]) Array.newInstance(hostDexElements.getClass().getComponentType(),
                    hostDexElements.length + plugInDexElements.length);
            //合并宿主和插件的dexElements数组
            System.arraycopy(hostDexElements,0, dexElements, 0, hostDexElements.length);
            System.arraycopy(plugInDexElements, 0, dexElements, hostDexElements.length, plugInDexElements.length);

            //用合并后的数组替换宿主的原dexElements数组
            dexElementsField.set(hostDexPathList, dexElements);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

在将插件apk中class加载后,就可以通过反射访问其中的class属性了.

    public void loadClassPlugIn() {
//        DexClassLoader dexClassLoader = new DexClassLoader("/sdcard/plugIn.dex",
//                MainActivity.this.getCacheDir().getAbsolutePath(),
//                null,
//                MainActivity.this.getClassLoader());
        PathClassLoader pathClassLoader = new PathClassLoader("/sdcard/plugIn.dex",
                MainActivity.this.getClassLoader());

        try {
            Class<?> clazz = Class.forName("com.test.plugintest.PlugInDemo");
            Method method = clazz.getMethod("testPlugIn");
            method.invoke(null);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值