前言

什么是插件化技术

插件化技术最初源于免安装运行apk的想法,对于免安装的apk可以理解为插件,而支持插件的app称为宿主app,宿主app在运行时加载运行插件apk,这个过程称之为插件化;

插件化的好处

  • 减小安装包的大小;(相较于组件化需要所有组件都打包进apk,插件化更灵活,需要加载运行插件时才去安装)
  • 实现app功能的动态扩展;(一个宿主可以支持多个插件apk,可插拔,拓展性更强)

类加载机制源码分析

java类加载与android类加载区别

我们知道,java源码文件编译后会生成一个个class文件,而在Android中,代码编译后会生成一个apk文件,将apk文件解压后可以看到有一个或多个dex文件,它就是安卓把所有的class文件进行合并,优化后生成的;

在java中JVM加载的是class文件,而安卓中DVM和ART加载的dex文件,两者都是通过ClassLoader进行加载的,但还是有些区别的,我们这里主要看下安卓的ClassLoader是如何加载dex文件的;

ClassLoader类关系

ClassLoader是一个抽象类,实现类主要分为两种:系统类加载器和自定义类加载器;

而系统类加载器主要包括三种

1.BootClassLoader,用于加载Android SDK层的class文件;

2.PathClassLoader,用于Android应用程序类加载器,可以加载指定的dex、jar、zip、apk中的class.dex文件;

3.DexClassLoader,用于加载指定的dex、jar、zip、apk中的class.dex文件;

相关类继承关系如下:

android 加载的项目中 安卓加载机制_加载


我们来看下DexClassLoader和PathClassLoader源码

### PathClassLoader
package dalvik.system;
public class PathClassLoader extends BaseDexClassLoader {
  
    public PathClassLoader(String dexPath, ClassLoader parent) {
        super(dexPath, null, null, parent);
    }
    public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
        super(dexPath, null, librarySearchPath, parent);
    }

    public PathClassLoader(
            String dexPath, String librarySearchPath, ClassLoader parent,
            ClassLoader[] sharedLibraryLoaders) {
        super(dexPath, librarySearchPath, parent, sharedLibraryLoaders);
    }
}
### DexClassLoader【9.0】
package dalvik.system;
public class DexClassLoader extends BaseDexClassLoader {
    public DexClassLoader(String dexPath, String optimizedDirectory,
            String librarySearchPath, ClassLoader parent) {
        super(dexPath, null, librarySearchPath, parent);
    }
}
### DexClassLoader【8.0及以前】
package dalvik.system;
public class DexClassLoader extends BaseDexClassLoader {
    public DexClassLoader(String dexPath, String optimizedDirectory,
            String librarySearchPath, ClassLoader parent) {
        super(dexPath, new File(optimizedDirectory), librarySearchPath, parent);
    }
}

注意 在8.0之前,他们二者唯一的区别就是第二个参数optimizedDirectory,这个参数的含义是生成的odex【优化后dex】存放的路径,PathClassLoader中直接为null,而DexClassLoader是使用用户传递进来的路径,而在8.0以后,二者实现完全一致;

那我们如果使用类加载器去加载一个类呢?接下来我们先看下源码中是如何实现加载dex文件的;

类加载原理

我们直接找到类加载方法调用的地方ClassLoader.loadClass方法

### java.lang.ClassLoader
  protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
            // 首先检查class是否已经被加载过
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                try {
                    if (parent != null) {
                    	//如果parent【成员变量,类型是ClassLoader】不为null,则调用parent的loadClass去加载
                        c = parent.loadClass(name, false);
                    } else {
                    	//正常情况下不会走这里,因为BootClassLoader重写了loadClass方法,结束了递归
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                  
                }

                if (c == null) {
                    //如果仍然没找到,就调用自己的findclass方法去查找;
                    c = findClass(name);
                }
            }
            return c;
    }

小结 首先检查当前类是否已经被加载,如果已经加载,直接获取并返回,如果没有被加载,parent不为null,则调用parent.loadClass进行加载,依次递归,如果找到了或者加载了就返回,如果没有找到也没有加载,才自己去加载,这个过程就是我们常说的双亲委派机制

双亲委派机制的好处:
1.避免类重复加载;【已加载的类不会再次加载】
2.安全性;【例如自己重写Activity.class方法瞎比搞,系统也不会加载你的类,保证了类加载的安全】

接下来,我们看下当所有的parent都没有加载成功时,DexClassLoader是如何加载的,我们发现它的父类BaseDexClassLoader中,重新了findClass方法

### BaseDexClassLoader
   public BaseDexClassLoader(ByteBuffer[] dexFiles, String librarySearchPath, ClassLoader parent) {
        super(parent);
        this.sharedLibraryLoaders = null;
        //初始化pathList
        this.pathList = new DexPathList(this, librarySearchPath);
        this.pathList.initByteBufferDexPath(dexFiles);
    }


  @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
       	...
       	//在pathList中查找指定的class
        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;
    }

我们继续看下pathList.findClass方法

### DexPathList

    //dex文件数组
    private Element[] dexElements;
    
    public Class<?> findClass(String name, List<Throwable> suppressed) {
    	//在dex文件数组中遍历查找class对象
        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;
    }

至此,我们发现Class对象就是从Element中获取的,每一个Element就对应一个dex文件,因为我们的dex文件可能存在多个,这里就对应数组Elements[]

反射实现插件类方法调用

明白了类加载的原理,我们自己尝试模拟宿主app中加载插件apk,并调用其中的class方法;
对应步骤如下:
1.获取宿主的ClassLoader类加载器,通过反射获取宿主对应的dexElements的值;
2.获取插件的ClassLoader类加载器,通过反射获取插件对应的dexElements的值;
3.合并宿主的dexElements和插件的dexElements数组,生成新的dexElements数组;
4.赋值替换宿主的原有的dexElements数组;
5.根据插件中调用类的包名,反射加载该类,调用其方法;

  • 我们首先创建插件app,里面就一个Print类对应print打印方法;
  • android 加载的项目中 安卓加载机制_android_02

  • 打包插件app,生成plugin.apk放到sdcard目录下;【开发环境下应该从云端下载存放到/data/data/包名/目录下,这里为了省事】
  • 利用反射技术合并宿主和插件dex文件
private fun mergeDexFile() {
       //先获取pathList字段
       val baseDexClassLoader = Class.forName("dalvik.system.BaseDexClassLoader")
       //是属于类的字段,可共用
       val pathListField = baseDexClassLoader.getDeclaredField("pathList")
       pathListField.isAccessible = true

       //获取dexElements字段
       val dexPathList = Class.forName("dalvik.system.DexPathList")
       //是属于类的字段,可共用
       val dexElements = dexPathList.getDeclaredField("dexElements")
       dexElements.isAccessible = true

       //获取宿主的类加载器
       val hostPathList = pathListField.get(classLoader)
       //获取宿主中的dexElement[]
       val hostDexElements: Array<Any> = dexElements.get(hostPathList) as Array<Any>

       //获取插件的类加载器,这里apk我们为了方便放在sdcard卡下,正常开发应放在/data/data/包名文件夹下
       val pluginClassLoader =
           DexClassLoader("/sdcard/plugin.apk", cacheDir.absolutePath, null, classLoader)
       //插件对应的pathList对象
       val pluginPathList = pathListField.get(pluginClassLoader)
       //插件对应的dexElements数组
       val pluginDexElements = dexElements.get(pluginPathList) as Array<Any>

       //进行数组合并
       val resultElements = java.lang.reflect.Array.newInstance(
           hostDexElements.javaClass.componentType,
           hostDexElements.size + pluginDexElements.size
       ) as Array<Any>
       System.arraycopy(hostDexElements, 0, resultElements, 0, hostDexElements.size)
       System.arraycopy(
           pluginDexElements,
           0,
           resultElements,
           hostDexElements.size,
           pluginDexElements.size
       )
       //赋值给宿主dexElements数组
       dexElements[hostPathList] = resultElements
   }
  • 验证插件中Print.class是否已加载;
private fun loadPluginDexFile() {
       var clazz = Class.forName("com.dongxian.plugin.Print")
       var newInstance = clazz.newInstance()
       val method = clazz.getMethod("print")
        //如果print是静态方法,则直接传null即可;
       method.invoke(newInstance)
   }
  • 结果打印输出

小结

通过上面的内容,我们了解了类加载机制的原理,明白了双亲委派机制,对插件化的有了进一步的认知!
如果以上文章对您有一点点帮助,希望您不要吝啬的点个赞加个关注,您每一次小小的举动都是我坚持写作的不懈动力!ღ( ´・ᴗ・` )