Android中的类加载机制

在类加载进内存以后,Android程序是通过ClassLoader类去加载内存中的类,然后进行解析运行的,在插件化技术中,因为需要我们自己去加载插件,所以要了解系统是怎么通过ClassLoader去加载类的,然后在这个过程中找到突破口,将我们的插件APK也加载进去,这篇文章咱们就说说关于Android中ClassLoader的一些知识。

类加载流程

一个类被加载到虚拟机内存中需要经历几个过程:加载、连接、初始化。其中连接分为三个步骤:验证、准备、解析,下面一个一个说,这个几个阶段虚拟机都干了什么。

加载

加载过程主要做了三件事:

1)通过类的全限定名来获取定义此类的二进制字节流
2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
3)在内存中生成一个代表这个类的java.lang.Class对象,作为这个类的各种数据的方位入口。

第一步主要是获取一个类的二进制字节流,意思就是把类以流的形式加载进内存,类的来源没有说,可以是jar包,也可以是class文件或者是apk文件。这个特性是能够实现插件化技术的理论基础。

第二步就是在获取到这个字节流以后,虚拟机就会把类中的静态存储结果保存到方法区中,保存的过程会转化对应方法区中的数据结构,所以说静态的结构都保存在内存中的方法区中。

第三步是当类加载进内存以后,每个类都会生成一个对应的Class对象,当我们使用这个类的时候,都是通过此Class对象为入口来使用的,比如我们写程序的时候通过 new 关键字创建一个类的对象的时候,也是通过这个类的Class对象来创建的。

连接

连接阶段主要分验证、准备和解析。

验证:主要是对类中的语法结构是否合法进行验证,确认类型符合Java语言的语义。
准备:这个阶段是给类中的类变量分配内存,设置默认初始值,比如一个静态的int变量初始值是0,布尔变量初始值是false。
解析:在类型的常量池中寻找类,接口,字段和方法的符号引用,把这些符号引用替换成直接引用的过程。

解析的过程可能不好理解,关于符号引用和直接引用是什么意思可以暂时忽略,这个过程可以理解为一开始虚拟机对加载到内存中的各种类、字段等并没有一一编号,只是通过一个符号去表示,在解析阶段,虚拟机把内存中的类、方法等进行统一管理起来。

初始化

初始化阶段才真正到了类中定义的java代码的阶段,在这个阶段会对类中的变量和一些代码块进行初始化,比如以类变量进行初始化,在准备阶段对类变量进行的默认初始化,到这个阶段就对对变量进行显式的赋值,其中静态代码块就是在这个阶段来执行的。

初始化不会马上执行,当一个类被主动使用的时候才会去初始化,主要有下面这几种情况:

1)当创建某个类的新实例时(如通过new或者反射等)
2)当调用某个类的静态方法时
3)当使用某个类或接口的静态字段时
4)当调用Java API中的某些反射方法时,比如类Class中的方法,或者java.lang.reflect中的类的方法时
5)当初始化某个子类时

关于类的加载过程就是上面这几步,分析完以后可以知道,我们程序员能够控制的只有第一步「加载」还有最后一步「初始化」,第一步记载的理论基础决定了插件化可以实现,最后一步初始化就是执行我们实际程序中的代码。

Android中的ClassLoader

上面说了系统是通过ClassLoader来将类加载到内存中,然后解析使用,在java中也有ClassLoader,但是因为java编译出来的是Class文件,而Android的APK中包含的确实dex文件,dex文件是将所需的所有Class文件重新打包,打包的规则不是简单地压缩,而是完全对Class文件内部的各种函数表、变量表等进行优化,并产生一个新的文件,所以java中和Android中的ClassLoader也不一样,这里主要来说一下Android中的ClassLoader。

在Android中我们常用的ClassLoade就两个,DexClassLoader 和 PathClassLoader,这两个类的源码都很简单,下面直接看一下

/**
 * A class loader that loads classes from {@code .jar} and {@code .apk} files
 * containing a {@code classes.dex} entry. This can be used to execute code not
 * installed as part of an application.
 */
public class DexClassLoader extends BaseDexClassLoader {
   
    public DexClassLoader(String dexPath, String optimizedDirectory,
            String libraryPath, ClassLoader parent) {
        super(dexPath, new File(optimizedDirectory), libraryPath, parent);
    }
}

DexClassLoader类中就只有一个构造方法,构造方法中直接调用了父类的构造,DexClassLoader继承了BaseDexClassLoader,构造方法中的参数的含义是:

参一 dexpath:要加载的dex文件的路径

参二 optimizedDirectory:dex文件首次加载时会进行优化操作,这个参数即为优化后的odex文件的存放目录,官方推荐使用应用私有目录来缓存优化后的dex文件,dexOutputDir = context.getDir(“dex”, 0);

参三 libraryPath:动态库的路径

参四 parent:当前加载器的父类加载器

接着看PathClassLoader

/**
 * Provides a simple {@link ClassLoader} implementation that operates on a list
 * of files and directories in the local file system, but does not attempt to
 * load classes from the network. Android uses this class for its system class
 * loader and for its application class loader(s).
 */
public class PathClassLoader extends BaseDexClassLoader {

    public PathClassLoader(String dexPath, ClassLoader parent) {
        super(dexPath, null, null, parent);
    }
    public PathClassLoader(String dexPath, String libraryPath,
            ClassLoader parent) {
        super(dexPath, null, libraryPath, parent);
    }
}

PathClassLoader有两个构造方法,同样也是直接调用了父类的构造方法,从构造方法上来看,DexClassLoader和PathClassLoader的区别只有第二个参数optimizedDirectory,在PathClassLoader中optimizedDirectory默认传入的是null。

从源码中看这两个类的作用也是因为optimizedDirectory参数的不同而不同,在源码中看使用PathClassLoader由于没有传入optimizedDirectory,系统会自动生成以后缓存目录,即/data/dalvik-cache/,在这个目录存放优化以后的dex文件。

所以PathClassLoader只能加载已安装的apk的dex,即加载系统的类和已经安装的应用程序(安装的apk的dex文件会存储在/data/dalvik-cache中),而DexClassLoader可以加载指定路径的apk、dex,也可以从sd卡中进行加载。

下面是ClassLoader的部分类继承结构图

在这里插入图片描述
ClassLoader是一个抽象类,定义了ClassLoader的主要功能

BaseDexClassLoader继承自ClassLoader,是抽象类ClassLoader的具体实现类,PathClassLoader和DexClassLoader都继承它。

BootClassLoader是ClassLoader的内部类

双亲委派机制

当一个ClassLoader去加载一个类的时候,它会去判断该类是否已经加载,如果没有,它不会马上去加载,而是委托给父加载器进行查找,这样递归一直找到最上层的ClassLoader类,如果找到了,就直接返回这个类所对应的Class对象,如果都没有加载过,就从顶层的ClassLoader去开始依次向下查找,每个加载器会从自己规定的位置去查找这个类,如果没有,最后再由请求发起者去加载该类

简单说,就是第一次查找的时候,是从下到上依次从缓存中查找之前有没有加载过,如果有就返回,如果都没有,就从上到下从自己制定的位置去查找这个类,最后在交给发起者去加载该类。

双亲委派机制的好处

1:避免重复加载,如果已经加载过一次Class,就不需要再次加载,而是先从缓存中直接读取。

2:更加安全,因为虚拟机认为只有两个类名一致并且被同一个类加载器加载的类才是同一个类,所以这种机制保证了系统定义的类不会被替代。

如果不使用双亲委托模式,就可以自定义一个String类来替代系统的String类,这显然会造成安全隐患,采用双亲委托模式会使得系统的String类在Java虚拟机启动时就被加载,也就无法自定义String类来替代系统的String类。还有,只有两个类名一致并且被同一个类加载器加载的类,Java虚拟机才会认为它们是同一个类,想要骗过Java虚拟机显然不会那么容易。

在这里插入图片描述

加载流程(图片来自网络)

ClassLoader加载类的过程

下面开始从代码上说一下Android中ClassLoader的加载类的整个流程,加载的过程其实就是按照上面的那张图来进行加载,先向上委托,然后再向下查找,最后交给本身处理。

在获取到上面的DexClassLoader或者PathClassLoader以后,系统会调用loadClass方法来动态加载某个类,比如下面这样创建对象,然后调用loadClass方法加载某个类

DexClassLoader dexClassLoader = 
		new DexClassLoader(dexPath, getDir("dex", 0).getAbsolutePath(), null, getClassLoader());

在DexClassLoader、PathClassLoader和BaseDexClassLoader中都没有找到loadClass方法的实现,它的实现在他们的父类ClassLoader中,代码如下

//ClassLoader.java
protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {
    	//查找父加载器是否已经加载过
        Class<?> clazz = findLoadedClass(className);
        if (clazz == null) {
            ClassNotFoundException suppressed = null;
            try {
                //使用参数传进来的父类加载器去加载
                clazz = parent.loadClass(className, false);
            } catch (ClassNotFoundException e) {
                suppressed = e;
            }

            if (clazz == null) {
                try {
                    //使用当前的加载器去加载
                    clazz = findClass(className);
                } catch (ClassNotFoundException e) {
                    e.addSuppressed(suppressed);
                    throw e;
                }
            }
        }
        return clazz;
    }

loadClass方法中首先去询问父加载器有没有加载过,如果没有加载过会使用参数传递进来的父加载器去加载,如果还是没有加载过,会使用当前创建的加载器去加载该类,整个流程符合上面的双亲委派机制。

接下来看看findLoadedClass方法中的实现

//ClassLoader.java
protected final Class<?> findLoadedClass(String className) {
        ClassLoader loader;
        if (this == BootClassLoader.getInstance())
            loader = null;
        else
            loader = this;
        return VMClassLoader.findLoadedClass(loader, className);
}

接着我们就不在往下看了,由于该类还没有加载过,这里肯定是返回null,然后接着会调用 parent.loadClass(className, false) 方法,parent是我们传入的加载器,通过Context的getClassLoader方法返回的,在Context中getClassLoader是个抽象方法,具体的实现在ContextImpl中,代码如下

//ContextImpl.java
  @Override
    public ClassLoader getClassLoader() {
        return mPackageInfo != null ?
                mPackageInfo.getClassLoader() : ClassLoader.getSystemClassLoader();
    }
    
//ClassLoader.java

    public static ClassLoader getSystemClassLoader() {
        return SystemClassLoader.loader;
    }
    
    //SystemClassLoader为ClassLoader的内部类
     static private class SystemClassLoader {
        public static ClassLoader loader = ClassLoader.createSystemClassLoader();
    }

    private static ClassLoader createSystemClassLoader() {
        String classPath = System.getProperty("java.class.path", ".");
        //最终返回了PathClassLoader作为系统加载器SystemClassLoader,而其父类为根加载器BootClassLoader
        return new PathClassLoader(classPath, BootClassLoader.getInstance());
    }

从上面的源码中可以看到,最后返回的是一个PathClassLoader对象,这个对象的父加载器是BootClassLoader,BootClassLoader上面介绍过,它是ClassLoader的一个内部类,从分析来看是Android平台上所有ClassLoader的最终parent。

返回PathClassLoader以后,它会接着调用loadClass,很显然最终会来到BootClassLoader中的loadClass方法中,代码如下

//ClassLoader $ BootClassLoader.java
@Override
protected Class<?> loadClass(String className, boolean resolve)
          throws ClassNotFoundException {
     Class<?> clazz = findLoadedClass(className);

     if (clazz == null) {
          clazz = findClass(className);
     }

     return clazz;
}

可以看到它在调用完findLoadClass以后,由于它已经是根加载器,所以肯定返回null,然后直接就调用了findClass方法,到这里再回看上面的ClassLoader加载图,就已经到达了最顶端,开始向下查找,最后一级一级的到达BaseDexClassLoader中的findClass方法中,代码如下

//BaseDexClassLoader.java
@Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
        //从pathList中去查找类
        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;
    }

public BaseDexClassLoader(String dexPath, File optimizedDirectory,
            String libraryPath, ClassLoader parent) {
        super(parent);
        this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
}

从现在开始进入到真正的加载类的过程了,上面的全部都是在说ClassLoader按照双亲委派机制流程一层层的查找。

上面最重要的即时第二行代码,从pathList对象中去查找对应的类,查找不到会抛出异常,找到了直接返回。看下面我贴上了BaseDexClassLoader的构造方法,在构造方法中可以看到 pathLIst 是一个DexPathList对象,接下来看看pathList的findClass方法是怎么实现的。

//DexPathList.java
private final Element[] dexElements;
//... 省略一大堆代码
public Class findClass(String name, List<Throwable> suppressed) {
    	//遍历dexElements数组,拿到里面的dex
        for (Element element : dexElements) {
            DexFile dex = element.dexFile;

            if (dex != null) {
                Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
                if (clazz != null) {
                    return clazz;
                }
            }
        }
        if (dexElementsSuppressedExceptions != null) {
            suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
        }
        return null;
    }

在findClass方法中遍历数组dexElements获取到里面的dex返回,可以知道dexElements里面保存的是apk中所有的dex,具体的保存过程这里就不贴代码里,保存过程是在上面创建DexPathList对象的构造方法中进行的。

到这里ClassLoader加载dex的流程基本上就完毕了,总结一下:

1:因为ClassLoader的双亲委派机制,如果要加载第三方dex,最好加载的还是本身的DexClassLoader
2:加载流程最后来到BaseDexClassLoader中的findClass方法中
3:接着来到DexPathList对象中的findClass方法中
4:最终apk中的dex文件会被解析保存到一个名为dexElements的Element类型的数组中,遍历数组拿到class对象。

插件化中加载dex的原理

理解了上面的流程就很好理解在插件化中加载第三方插件的原理,因为插件化中我们的插件apk不会让系统加载,所以我们需要自己加载,然后把插件apk的dex插入到数组apk的dex中,插入到哪里呢?看上面的加载过程,就是通过Hook的方式把插件apk中的dex插入到宿主apk的 dexElements数组中,这样系统在加载宿主的dex的时候就会加载插件的dex,通过这样的方式就骗过了系统。

这里多思考一下,如果插件的dex中存在了和宿主相同的class,那么最后会使用哪个class呢?根据上面的findClass源码,在找到需要的Class对象以后就会返回,不会在继续遍历dexElements数组,所以前面的类会覆盖掉后面的类,热修复框架实现的原理就只这个。

上面就是插件化加载dex的原理,具体的代码实现在后面的文章中会通过VirtualAPK的源码来讲解。

最后欢迎关注我的公众号,谢谢。

在这里插入图片描述

关注我的公众号,我们一起进步
  • 7
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值