• Android插件化学习之初识类加载机制


    前言

    什么是插件化技术

    插件化技术最初源于免安装运行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 ClassLoader类继承关系图
    我们来看下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);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    ### DexClassLoader9.0package dalvik.system;
    public class DexClassLoader extends BaseDexClassLoader {
        public DexClassLoader(String dexPath, String optimizedDirectory,
                String librarySearchPath, ClassLoader parent) {
            super(dexPath, null, librarySearchPath, parent);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    ### DexClassLoader8.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);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    注意 在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;
        }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    小结 首先检查当前类是否已经被加载,如果已经加载,直接获取并返回,如果没有被加载,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;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    我们继续看下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;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    至此,我们发现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打印方法;
      插件app

    • 打包插件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
       }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 验证插件中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)
       }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 结果打印输出
      测试结果

    小结

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

  • 相关阅读:
    CLion 2023:专注于C和C++编程的智能IDE mac/win版
    Acwing 830. 单调栈
    centOs云服务器安装Docker
    linux自动化部署脚本讲解
    Redis——Linux下安装以及命令操作
    [漏洞分析] CVE-2023-38545 curl“史上最严重的漏洞“分析
    100 行 C++ 代码,教你快速实现视频画面动态分割!
    Linux课程四课---Linux开发环境的使用(gcc/g++编译器的相关)
    【图像处理】图像基础滤波处理:盒式滤波、均值滤波、高斯滤波、中值滤波、双边滤波
    RabbitMQ基本概念和工作原理
  • 原文地址:https://blog.csdn.net/a734474820/article/details/126032919