Android类加载机制

作者: 唠嗑008 | 来源:发表于2020-04-07 17:35 被阅读0次

什么是类加载?

我们平时编写的.java文件不是可执行文件,需要先编译成.class文件才可以被虚拟机执行。所谓类加载是指通过类加载器把class文件加载到虚拟机的内存空间,具体来说是方法区。类通常是按需加载,即第一次使用该类时才加载。

Java与Android类加载的区别

首先,Java与Android都是把类加载到虚拟机内存中,然后由虚拟机转换成设备识别的机器码。但是由于二者使用的虚拟机不同,所以在类加载方面也是有所区别的。Java的虚拟机是JVM,Android的虚拟机是dalvik/art(5.0以后虚拟机是art,是对dalvik的一种升级)。Java虚拟机运行的是class文件,而Android 虚拟机运行的是dex文件。dex其实是class文件的集合,是对class文件优化的产物,是为了避免出现重复的class。

Android中的类加载器

从上面的讲解中,我们已经知道我们平时写的类是被类加载器加载尽虚拟机内存才能运行。下面就通过Framework源码来为大家讲解Android中的几个类加载器。

类加载器关系.png
  • PathClassLoader :
    加载APK中我们自己写的类;
  • DexClassLoader :
    可以从包含classes.dex的jar或者apk中,加载类的类加载器, 可用于执行动态加载,能够加载系统没有安装的apk或者jar文件, 因此很多热修复和插件化方案都是采用DexClassLoader;
  • BaseDexClassLoader :
    DexClassLoader与PathClassLoader都继承于BaseDexClassLoader;
  • BootClassLoader :
    用于加载一些Android系统框架的类;
  • ClassLoader :
    所有类加载器的基类

下面的源码解析基于Android SDK API28,这几个类加载器没办法在AS上直接查看源码,AS搜索到的是反编译的class的内容,是不可信的,为大家推荐一个在线工具查看,在线查看Android Framework源码

PathClassLoader

package dalvik.system;

/**
 * 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 librarySearchPath, ClassLoader parent) {
        super(dexPath, null, librarySearchPath, parent);
    }
}

用来加载本地文件系统上的文件或目录,通常是用来加载应用中我们自己写的类;而像Activity.class这种系统的类不是由它加载。
构造器参数解释:

  • dexPath :被加载的文件的地址,文件可以是 jar/apk/dex,文件可以有多个,多个文件用:隔开;
  • librarySearchPath :ndk相关的so库;
  • parent :该类加载器的父类加载器。

DexClassLoader

package dalvik.system;

/**
 * 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.
 *
 * <p>Prior to API level 26, this class loader requires an
 * application-private, writable directory to cache optimized classes.
 * Use {@code Context.getCodeCacheDir()} to create such a directory:
 * <pre>   {@code
 *   File dexOutputDir = context.getCodeCacheDir();
 * }</pre>
 *
 * <p><strong>Do not cache optimized classes on external storage.</strong>
 * External storage does not provide access controls necessary to protect your
 * application from code injection attacks.
 */
public class DexClassLoader extends BaseDexClassLoader {
    /**
     * Creates a {@code DexClassLoader} that finds interpreted and native
     * code.  Interpreted classes are found in a set of DEX files contained
     * in Jar or APK files.
     *
     * <p>The path lists are separated using the character specified by the
     * {@code path.separator} system property, which defaults to {@code :}.
     *
     * @param dexPath the list of jar/apk files containing classes and
     *     resources, delimited by {@code File.pathSeparator}, which
     *     defaults to {@code ":"} on Android
     * @param optimizedDirectory this parameter is deprecated and has no effect since API level 26.
     * @param librarySearchPath the list of directories containing native
     *     libraries, delimited by {@code File.pathSeparator}; may be
     *     {@code null}
     * @param parent the parent class loader
     */
    public DexClassLoader(String dexPath, String optimizedDirectory,
            String librarySearchPath, ClassLoader parent) {
        super(dexPath, null, librarySearchPath, parent);
    }
}

也是被用来加载 jar 、apk、dex,可以用来加载未安装到应用中的类。从构造方法可以看出和PathClassLoader很像,只是多了一个optimizedDirectory参数:用于存放优化后的dex路径,不能为空。
注意:
1、这个路径需要是应用私有的可写的,比如说data/data,data/cache目录,这样是为了防止一些注入攻击的风险;
2、API 26以后,这个参数已经没有影响了,因为在调用父构造器的时候这个参数始终为null,也就是说Android 8.0 以后只能使用系统默认的位置;
3、在加载app的时候,apk内部的dex已经执行过优化了,优化之后放在系统目录/data/dalvik-cache下。

关于dex文件优化,可能很多人还是不理解,水平有限,我简单解释一下,

由于Android程序的apk文件为zip压缩包格式,ART虚拟机每次加载它们时需要从apk中读取classes.dex文件,这样会耗费很长时间,为了解决这个问题,出现了ODEX优化方案。ODEX是OptimizedDEX的缩写,表示经过优化的dex文件,存放在/data/dalvik-cache目录下。而采用odex方式优化的dex文件,已经包含了加载dex必须的依赖库文件列表,ART虚拟机之后也是直接读取目录下的的dex文件,这大大缩短了读取dex文件所需的时间。

BaseDexClassLoader

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

PathClassLoader 和 DexClassLoader 都继承这个类,区别是PathClassLoader传入的optimizedDirectory参数为null,而DexClassLoader在8.0之前是有值的,在8.0之后也是为null,所以在8.0之后PathClassLoader 和 DexClassLoader 其实已经没有什么区别了。BaseDexClassLoader作为加载dex的核心类之一,还有很多很重要的地方,我们放到后面再讲。

ClassLoader

public abstract class ClassLoader {
 private final ClassLoader parent;
private ClassLoader(Void unused, ClassLoader parent) {
        this.parent = parent;
    }

    protected ClassLoader(ClassLoader parent) {
        this(checkCreateClassLoader(), parent);
    }

    protected ClassLoader() {
        this(checkCreateClassLoader(), getSystemClassLoader());
    }
}

ClassLoader是一个抽象类,有3个构造方法,最终调用的还是第一个构造方法,主要功能是保存实现类传入的parent参数,也就是父类加载器。ClassLoader的实现类主要有2个,一个是前面讲过的BaseDexClassLoader,另一个是BootClassLoader。

BootClassLoader

class BootClassLoader extends ClassLoader {

    private static BootClassLoader instance;

    @FindBugsSuppressWarnings("DP_CREATE_CLASSLOADER_INSIDE_DO_PRIVILEGED")
    public static synchronized BootClassLoader getInstance() {
        if (instance == null) {
            instance = new BootClassLoader();
        }

        return instance;
    }

    public BootClassLoader() {
        super(null);
    }
}

它实际上是ClassLoader的内部类,而且继承了ClassLoader。可以看出它的创建方式是单例模式。

为什么是单例的?
在上面的介绍中我们讲过BootClassLoader这个类加载器是用来加载系统类的,在app进程创建的时候,系统类就已经被加载了,这时候会用到一次;而系统在创建PathClassLoader的时候会传入BootClassLoader作为其父类加载器。

加载类的过程

 public Class<?> loadClass(String name) throws ClassNotFoundException {
        return loadClass(name, false);
    }

  
    protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
            // 1. First, check if the class has already been loaded
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                try {
                    if (parent != null) {
                        //2. parent load class
                        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) {
                    // 3. If still not found, then invoke findClass in order
                    // to find the class.
                    c = findClass(name);
                }
            }
            return c;
    }

这是加载一个类的入口,流程如下:
1、 先检查这个类是否已经被加载,有的话直接返回Class对象;
2、如果没有加载过,通过父类加载器去加载,可以看出parent是通过递归的方式去加载class的;
3、如果所有的父类加载器都没有加载过,就由当前的类加载器去加载。

上面加载类的过程就是我们经常听到的双亲委托机制。这种设计模式的好处在于:
1、保证class只会被加载一次,也就是说类的数据结构只会在第一次创建的时候被加载进内存(方法区),以后要创建这个类的对象的时候,直接用方法区中的class在堆内存创建一个对象即可,这样的话创建对象就会比较快;
2、保证系统类的安全性。因为在启动应用进程的时候就已经加载好了系统类(BootClassLoader),那后面运行期就不可能通过恶意伪造加载的方式去造成一些系统安全问题。

通常我们自己写的类是通过当前类加载器调用findClass方法去加载的,但是在ClassLoader中这是个空方法,具体的实现在它的子类BaseDexClassLoader中。

BaseDexClassLoader

findClass

@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;
    }

可以看到是通过pathList去查找class的,这个对象其实之前讲过,它是在BaseDexClassLoader 的构造方法中初始化的,它实际上是一个DexPathList对象。

DexPathList

findClass()

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;
    }

对Element数组遍历,再通过Element对象的findClass方法去查找class,有的话就直接返回这个class,找不到则返回null。现在又产生2个新的问题:dexElements是如何产生的?Element是什么结构?

Element
Element其实是DexPathList的内部类。

static class Element {
     
        private final File path;

        private final DexFile dexFile;

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

从数据结构可以看出,一个Element对象对应一个dex文件,而一个dex文件则包含多个class,最后是通过dex文件去加载类的。

再来看一下Element数组dexElements对象是如何生成的。

 DexPathList(ClassLoader definingContext, String dexPath,
            String librarySearchPath, File optimizedDirectory, boolean isTrusted) {
      ...
      this.definingContext = definingContext;
        // save dexPath for BaseDexClassLoader
        this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
                                           suppressedExceptions, definingContext, isTrusted);
      ...
}

原来是在DexPathList构造方法中调用了makeDexElements()得到Element数组。splitDexPath(dexPath)方法是把dexPath目录下的所有文件转换成一个File集合,如果是多个文件的话,会用:作为分隔符。

makeDexElements()
这个方法代码量稍微大了一点,我不贴出来了,我说一下关键的地方,大家可以根据我提供的思路自行查看源码。它做的事情不复杂,就是遍历所有jar/apk/dex文件,内部会调用loadDexFile()方法去加载dex文件,然后包装成Element对象,再加到数组中。

总结

1、加载一个类是通过双亲委托机制来实现的。
2、如果是第一次加载class,那是通过BaseDexClassLoader中的findClass方法实现的;接着进入DexPathList中的findClass方法,内部通过遍历Element数组,从Element对象中去查找类;Element实际上是对Dex文件的包装,最终还是从dexfile去查找的class。
3、一般加载apk中的类主要用到2个类加载器,一个是PathClassLoader:主要用于加载自己写的类;另一个是BootClassLoader:用于加载Framework中的类;
4、热修复和插件化一般是利用DexClassLoader来实现。
5、PathClassLoader和DexClassLoader其实都可以加载apk/jar/dex,区别是 DexClassLoader 可以指定 optimizedDirectory,也就是 dex2oat 的产物 .odex 存放的位置,而 PathClassLoader 只能使用系统默认位置。但是在8.0 以后二者是没有区别的,只能使用系统默认的位置了。

预告

在类加载流程分析中,我们已经知道,查找class是通过DexPathList来完成的,实际上DexPathList最终还是遍历其Element数组,获取DexFile对象来加载Class文件。由于数组是有序的,如果2个dex文件中存在相同类名的class,那么类加载器就只会加载数组前面的dex中的class。如果apk中出现了有bug的class,那只要把修复的class打包成dex文件并且放在DexPathList中Element数组`的前面,就可以实现bug修复了。下一篇为大家带来的手写热修复。

Android类加载机制的细枝末节
从JVM到Dalivk再到ART(class,dex,odex,vdex,ELF)

相关文章

网友评论

    本文标题:Android类加载机制

    本文链接:https://www.haomeiwen.com/subject/vjcauhtx.html