Android ClassLoader浅析

Posted zhuliyuan丶

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android ClassLoader浅析相关的知识,希望对你有一定的参考价值。

前言

最近在看Tinker的原理,发现核心是通过ClassLoader做的,由于之前也从未接触过ClassLoader趁着上周末看了安卓ClassLoader相关源码,这里分享一发安卓的ClassLoader和热更新的实现原理。

ClassLoader

首先我们要知道,程序在运行时要把对应的类加载到内存,在安卓上来说就是把Dex文件中的类加载到内存,这个加载流程是通过ClassLoader实现的。因此如果我们想要动态加载自己的类,就得从ClassLoader上做文章,那么接下我们先看看安卓的ClassLoader

public abstract class ClassLoader 
    private ClassLoader(Void unused, ClassLoader parent) 
        this.parent = parent;
    
    
    protected ClassLoader(ClassLoader parent) 
        this(checkCreateClassLoader(), parent);
    
    
    protected ClassLoader() 
        this(checkCreateClassLoader(), getSystemClassLoader());
    
    
    public static ClassLoader getSystemClassLoader() 
        return SystemClassLoader.loader;
    
    
    static private class SystemClassLoader 
        public static ClassLoader loader = ClassLoader.createSystemClassLoader();
    
    
    private static ClassLoader createSystemClassLoader() 
        String classPath = System.getProperty("java.class.path", ".");
        String librarySearchPath = System.getProperty("java.library.path", "");

        return new PathClassLoader(classPath, librarySearchPath, BootClassLoader.getInstance());
    
    
    public Class<?> loadClass(String name) throws ClassNotFoundException 
        return loadClass(name, false);
    
    
    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;
    
    
    protected Class<?> findClass(String name) throws ClassNotFoundException 
        throw new ClassNotFoundException(name);
    

可以看到在构造方法的时候我们需要传入一个父ClassLoader,默认没传情况下是以PathClassLoader作为父加载器的。

然后加载类是调用的loadClass()方法,他是先判断是本ClassLoader是否已经加载过,如果没加载过就让父加载器加载,然后父加载器看自己是否加载过,如果没加载过让祖父加载器加载,然后祖父加载器没找到,父加载器在找,父加载器没找到,子加载器再找,这也就是我们常说的双亲委派模式。

采取双亲委派模式主要有两点好处:

  1. 避免重复加载,如果已经加载过一次Class,就不需要再次加载,而是先从缓存中直接读取。
  2. 更加安全,如果不使用双亲委托模式,就可以自定义一个String类来替代系统的String类,这显然会造成安全隐患,采用双亲委托模式会使得系统的String类在Java虚拟机启动时就被加载,也就无法自定义String类来替代系统的String类,除非我们修改类加载器搜索类的默认算法。还有一点,只有两个类名一致并且被同一个类加载器加载的类,Java虚拟机才会认为它们是同一个类。

那么下面的重点是看android的类加载器如何重写findClass()方法来加载类的。

Android ClassLoader继承关系

从上图可以看到与ClassLoader相关的一共有7个,大致可以分为如下3类

  • BootClassLoaderClassLoader内部类,是Android中所有ClassLoader的parent,可见性为包内可见我们无法使用。
  • BaseDexClassLoaderPathClassLoaderDexClassLoaderInMemoryDexClassLoader的父类,类加载的主要逻辑都是在BaseDexClassLoader完成的。
  • SecureClassLoader继承了抽象类ClassLoader,拓展了ClassLoader类加入了权限方面的功能,加强了安全性,其子类URLClassLoader是用URL路径从jar文件中加载类和资源,我们用不上。

我们重点关注与我们热更新相关的PathClassLoaderDexClassLoader

  • PathClassLoader只能加载已经安装到Android系统中的apk文件(/data/app目录),是Android默认使用的类加载器。
  • DexClassLoader可以加载任意目录下的dex/jar/apk/zip文件,比PathClassLoader更灵活,是实现热修复的重点。

这里我们可以验证下PathClassLoader是安卓默认使用的类加载器。在ActivityonCreate()方法中打印它的类加载器。

    @Override
    protected void onCreate(Bundle savedInstanceState) 
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Log.i("zhuliyuan", this.getClass().getClassLoader().toString());
    

源码查看(基于API28)

/**
 * 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);
   


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


从注释上可以看出PathClassLoader是用于系统类和应用程序类的加载,DexClassLoader可以用来加载任意目录的dex。具体实现还得看BaseDexClassLoader的构造方法。

public class BaseDexClassLoader extends ClassLoader 
    private final DexPathList pathList;

    public BaseDexClassLoader(String dexPath, File optimizedDirectory,
                              String librarySearchPath, ClassLoader parent) 
        this(dexPath, optimizedDirectory, librarySearchPath, parent, false);
    

    public BaseDexClassLoader(String dexPath, File optimizedDirectory,
                              String librarySearchPath, ClassLoader parent, boolean isTrusted) 
        super(parent);
        this.pathList = new DexPathList(this, dexPath, librarySearchPath, null, isTrusted);
        ...
    
    
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException 
        ...
        Class c = pathList.findClass(name, suppressedExceptions);
        ...
        return c;
    
    
    @Override
    protected URL findResource(String name) 
        return pathList.findResource(name);
    
    
    @Override
    public String findLibrary(String name) 
        return pathList.findLibrary(name);
    

可以看出在构造方法中创建了一个DexPathList对象赋值给了pathList字段,然后findxxx()方法都是从DexPathList中查找。

BaseDexClassLoader的构造函数包含四个参数:

  • dexPath:包含类和资源的jar / apk文件列表,由 File.pathSeparator分隔,在Android上默认为:。
  • optimizedDirectory:由于dex文件被包含在APK或者Jar文件中,因此在装载目标类之前需要先从APK或Jar文件中解压出dex文件,该参数就是制定解压出的dex 文件存放的路径。这也是对apk中dex根据平台进行ODEX优化的过程。自API26开始无效。
  • librarySearchPath:指目标类中所使用的C/C++库存放的路径,可以为null。
  • parent:父ClassLoader引用。

接下来我们查看DexPathList的构造方法和findxxx()方法。

final class DexPathList 
	private Element[] dexElements;
    
    DexPathList(ClassLoader definingContext, String dexPath,
            String librarySearchPath, File optimizedDirectory, boolean isTrusted) 
        ...
        // 加载dexPath路径下的dex和resource
        this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
                                           suppressedExceptions, definingContext, isTrusted);
        ...
    
    
        private static Element[] makeDexElements(List<File> files, File optimizedDirectory,
            List<IOException> suppressedExceptions, ClassLoader loader, boolean isTrusted) 
      Element[] elements = new Element[files.size()];
      int elementsPos = 0;
      for (File file : files) 
          if (file.isDirectory()) 
              elements[elementsPos++] = new Element(file);
           else if (file.isFile()) 
              String name = file.getName();

              DexFile dex = null;
              if (name.endsWith(DEX_SUFFIX)) 
                  // Raw dex file (not inside a zip/jar).
                  try 
                      dex = loadDexFile(file, optimizedDirectory, loader, elements);
                      if (dex != null) 
                          elements[elementsPos++] = new Element(dex, null);
                      
                   catch (IOException suppressed) 
                      System.logE("Unable to load dex file: " + file, suppressed);
                      suppressedExceptions.add(suppressed);
                  
               else 
                  try 
                      dex = loadDexFile(file, optimizedDirectory, loader, elements);
                   catch (IOException suppressed) 
                      suppressedExceptions.add(suppressed);
                  

                  if (dex == null) 
                      elements[elementsPos++] = new Element(file);
                   else 
                      elements[elementsPos++] = new Element(dex, file);
                  
              
           else 
              System.logW("ClassLoader referenced unknown path: " + file);
          
      
      return elements;
    

构造方法中调用makeDexElements()方法获取到了Element[]数组赋值给了dexElements变量。

    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;
    

findClass()方法则是变量在构造方法初始化好的Element[]数组中从前往后遍历找到我们需要的类。

这里我们总结下类加载的过程

  • PathClassLoaderDexClassLoader调用了父类BaseDexClassLoader的构造方法。
  • BaseDexClassLoader在构造方法中创建了DexPathList对象并赋值给pathList字段,加载类的findxxx()方法都是调用DexPathList类的findxxx()方法来实现内的加载。
  • DexPathList在构造方法中调用makeDexElements()方法创建了Element[]数组赋值给dexElements字段,findClass()方法就是从前往后遍历Element[]数组找到我们要的class.

热更新实现原理

上面已经分析了类的加载流程,那么要想实现热更新我们需要拿到PathClassLoader中的DexPathList对象然后在拿到他当中的Element[]数组将我们已经修复的Element插入到数组的前面,这样在类加载时候就会先拿到我们已经修复的class了达到动态替换的效果。

加载我们自己的dex文件需要用到DexClassLoader,然后通过反射取出DexClassLoader中的DexPathList对象中的Element[]数组插入到PathClassLoaderElement[]数组前面即可。

热更新实际例子我会在另一篇Android 手动实现热更新介绍。

以上是关于Android ClassLoader浅析的主要内容,如果未能解决你的问题,请参考以下文章

Android 逆向类加载器 ClassLoader ( 加载 Android 组件的类加载器 | 双亲委派机制实例分析 )

Android ClassLoader浅析

Android 逆向类加载器 ClassLoader ( 启动类加载器 | 扩展类加载器 | 应用类加载器 | 类加载的双亲委托机制 )

Classloader、插件化开发(结合Presto)

Android类加载机制

Android类加载机制