FrameWork源码解析(7)-ClassLoader及dex加载过程

主目录见:Android高级进阶知识(这是总目录索引)
在线源码查看:AndroidXRef

了解这一篇的知识对后面插件化中的类加载是必不可少的,我们知道,我们的应用中的类在编译过程中会被编译成dex文件,所以把我们的dex加载进我们的程序我们就可以查找到插件中的类。我们今天就会来了解这一过程。

一.类加载器

学过java的应该知道,我们的类是通过类加载器加载到JVM的,Android也不例外,Android中有两个特别重要的ClassLoader:
1.PathClassLoader:

类描述

可以看到这边英文描述了这个类加载器不会加载网络上的类,只会加载系统类和应用类的,而且在dalvik虚拟机上只能加载已经安装的apk的dex。当然在android 5.0之后是否可以加载未安装的apk的dex,这个没做过实验。但是可以知道,用这个类加载器来加载插件中的dex是不可行的。我们看下完整的这个类:

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

我们看到构造函数都是调用了super的构造函数,所以我们待会在看BaseDexClassLoader时候会详细来说明。但是我们看第二个参数为空,这个参数是optimizedDirectory,是dex文件被加载后会被编译器优化,优化之后的dex存放路径,因为PathClassLoader只能加载系统类或者应用的类,所以这个为空,其实optimizedDirectory为null时的默认路径就是/data/dalvik-cache 目录。

2.DexClassLoader

/**
 * 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.
 *
 * 

This class loader requires an application-private, writable directory to * cache optimized classes. Use {@code Context.getDir(String, int)} to create * such a directory:

   {@code
 *   File dexOutputDir = context.getDir("dex", 0);
 * }
* *

Do not cache optimized classes on external storage. * External storage does not provide access controls necessary to protect your * application from code injection attacks. */

上面英文注释分别说明了三个方面的知识:
1).DexClassLoader支持加载APK、DEX和JAR,也可以从SD卡进行加载。
上面说dalvik不能直接识别jar,DexClassLoader却可以加载jar文件,这难道不矛盾吗?其实在BaseDexClassLoader里对".jar",".zip",".apk",".dex"后缀的文件最后都会生成一个对应的dex文件,所以最终处理的还是dex文件。
2).这个类需要提供一个optimizedDirectory路径用于存放优化后的dex。
3).optimizedDirectory路径不允许是外部存储的路径,为了防止应用被注入攻击。
我们来看下DexClassLoader完整的类:

public class DexClassLoader extends BaseDexClassLoader {
  public DexClassLoader(String dexPath, String optimizedDirectory,
            String libraryPath, ClassLoader parent) {
        super(dexPath, new File(optimizedDirectory), libraryPath, parent);
    }
}

我们看到第二个参数传了不为null的目录,跟PathDexClassLoader不同,所以这个类加载器可以从外部存储里面加载apk,dex或者jar文件,我们目标就是它了。

3.BaseDexClassLoader
PathClassLoaderDexClassLoader都继承自BaseDexClassLoader,其中的主要逻辑都是在BaseDexClassLoader完成的。

构造函数

可以看到构造函数会new出一个DexPathList对象,我们等会会说,现在我们先来看看参数的意思:
1).dexPath:待加载的类的apk,jar,dex的路径,必须是全路径。如果要包含多个路径,路径之间必须使用特定的分割符分隔,特定的分割符可以使用 File.pathSeparator获得。上面"支持加载APK、DEX和JAR,也可以从SD卡进行加载"指的就是这个路径,最终做的是将dexPath路径上的文件ODEX优化到内部位置optimizedDirectory,然后,再进行加载的。
2).libraryPath:目标类中所使用的C/C++库存放的路径,也就是so文件的路径。
3).ClassLoader:是指该装载器的父装载器,一般为当前执行类的装载器,例如在Android中以 context.getClassLoader()作为父装载器。因为类加载器的双亲委托机制,需要设置一个父装载器。

二.类加载过程

我们知道,类的加载过程最终都会通过BaseDexClassLoader中的findClass()开始的:

FrameWork源码解析(7)-ClassLoader及dex加载过程_第1张图片
findClass

可以看到我们这里的Class对象是通过pathList中的 findClass()方法获取的。这里的pathList又是什么呢?这个类在上面 BaseDexClassLoader的构造函数中初始化的。我们可以看下:

 public DexPathList(ClassLoader definingContext, String dexPath,
            String libraryPath, File optimizedDirectory) {
        if (definingContext == null) {
            throw new NullPointerException("definingContext == null");
        }
        if (dexPath == null) {
            throw new NullPointerException("dexPath == null");
        }
        if (optimizedDirectory != null) {
            if (!optimizedDirectory.exists())  {
                throw new IllegalArgumentException(
                        "optimizedDirectory doesn't exist: "
                        + optimizedDirectory);
            }
            if (!(optimizedDirectory.canRead()
                            && optimizedDirectory.canWrite())) {
                throw new IllegalArgumentException(
                        "optimizedDirectory not readable/writable: "
                        + optimizedDirectory);
            }
        }
        this.definingContext = definingContext;
      this.dexElements = makePathElements(splitDexPath(dexPath), optimizedDirectory, 121 suppressedExceptions); 
        this.nativeLibraryDirectories = splitPaths(libraryPath, false); 
        this.systemNativeLibraryDirectories = splitPaths(System.getProperty("java.library.path"), true); 
        List allNativeLibraryDirectories = new ArrayList<>(nativeLibraryDirectories); 
        allNativeLibraryDirectories.addAll(systemNativeLibraryDirectories);

        this.nativeLibraryPathElements = makePathElements(allNativeLibraryDirectories, null, 140 suppressedExceptions); 
    }

这个类很重要,我们可以看到首先是dexPath,optimizedDirectory的非空判断。然后是dexElements的赋值,这里我们说下dexElements

/**  
* List of dex/resource (class path) elements. 
* Should be called pathElements, but the Facebook app uses reflection 
* to modify 'dexElements' (http://b/7726934).  */  
private final Element[] dexElements;

这个数组就是放我们dex的数组,我们的不同的dex作为数组存放。里面注释很有意思有句话,FaceBook使用反射来修改dexElements,很明确告诉我们也可以通过修改这个数组来加载我们的dex。接着我们来看看makePathElements方法,在看这个方法之前我们看到里面有个参数是调用splitDexPath方法,这个方法是用于根据分隔符取到文件列表的:

 private static Element[] makePathElements(ArrayList files,
           File optimizedDirectory) {
      for (File file : files) {
           File zip = null; 
           File dir = new File(""); 
           DexFile dex = null;
           String path = file.getPath(); 
          String name = file.getName(); 
           if (path.contains(zipSeparator)) { 
                String split[] = path.split(zipSeparator, 2);
                 zip = new File(split[0]); 
                 dir = new File(split[1]);
            } else if (file.isDirectory()) {
              // We support directories for looking up resources and native libraries.
               // Looking up resources in directories is useful for running libcore tests. 
               elements.add(new Element(file, true, null, null)); 
            } else if (file.isFile()) {     
                    if (name.endsWith(DEX_SUFFIX)) { 
                        // Raw dex file (not inside a zip/jar). 
                        try {
                                 dex = loadDexFile(file, optimizedDirectory); 
                         } catch (IOException ex) { 
                                  System.logE("Unable to load dex file: " + file, ex); 
                              } 
                    } else { 
                          zip = file;
                          try { 
                                dex = loadDexFile(file, optimizedDirectory); 
                          } catch (IOException suppressed) { 
                          ......
                          }
               }
           } else {
               System.logW("Unknown file type for: " + file);
           }
           if ((zip != null) || (dex != null)) {
              elements.add(new Element(dir, false, zip, dex)); 
           }
       }
       return elements.toArray(new Element[elements.size()]);
   }

这个方法就是遍历之前得到的文件列表,然后判断传进来的文件是目录,zip文件或者dex文件,如果是目录的话,直接将文件file作为参数传给Element然后添加进elements中。否则其他情况都会调用loadDexFile方法进行加载,我们看下这个方法:

private static DexFile loadDexFile(File file, File optimizedDirectory) throws IOException { 
      if (optimizedDirectory == null) {
           return new DexFile(file); 
       } else { 
           String optimizedPath = optimizedPathFor(file, optimizedDirectory); 
           return DexFile.loadDex(file.getPath(), optimizedPath, 0);
       } 
 } 

我们看到这个方法很简单,如果optimizedDirectory == null则直接new 一个DexFile,否则就使用DexFile#loadDex来创建一个DexFile实例。

  private static String optimizedPathFor(File path,
           File optimizedDirectory) {
       /*
        * Get the filename component of the path, and replace the
        * suffix with ".dex" if that's not already the suffix.
        *
        * We don't want to use ".odex", because the build system uses
        * that for files that are paired with resource-only jar
        * files. If the VM can assume that there's no classes.dex in
        * the matching jar, it doesn't need to open the jar to check
        * for updated dependencies, providing a slight performance
        * boost at startup. The use of ".dex" here matches the use on
        * files in /data/dalvik-cache.
        */
       String fileName = path.getName();
       if (!fileName.endsWith(DEX_SUFFIX)) {
           int lastDot = fileName.lastIndexOf(".");
           if (lastDot < 0) {
               fileName += DEX_SUFFIX;
           } else {
               StringBuilder sb = new StringBuilder(lastDot + 4);
               sb.append(fileName, 0, lastDot);
               sb.append(DEX_SUFFIX);
               fileName = sb.toString();
           }
       }
       File result = new File(optimizedDirectory, fileName);
       return result.getPath();
   }

这个方法获取被加载的dexpath的文件名,如果不是“.dex”结尾的就改成“.dex”结尾,然后用optimizedDirectory和新的文件名构造一个File并返回该File的路径,所以DexFile#loadDex方法的第二个参数其实是dexpath文件对应的优化文件的输出路径。

接着 DexPathList构造函数中会获取so文件库的路径,然后传给makePathElements方法,同样地,也是添加到DexElement中,到这里我们已经将我们的类和so文件添加进DexElement数组中了。所以我们插件化框架只要将我们的插件中的类想办法添加进DexElement数组中就可以了。

然后我们继续分析我们DexPathList#findClass():

public Class findClass(String name) { 
    for (Element element : dexElements) { 
        DexFile dex = element.dexFile;
        if (dex != null) { 
            Class clazz = dex.loadClassBinaryName(name, definingContext); 
          if (clazz != null) { 
              return clazz; 
          } 
        } 
    } 
    return null;
}

我们看到这里会遍历我们的dexElements数组,然后取出数组中的DexFile对象,调用他的DexFile#loadClassBinaryName方法:

public Class loadClassBinaryName(String name, ClassLoader loader) { 
    return defineClass(name, loader, mCookie);
}
private native static Class defineClass(String name, ClassLoader loader, int cookie);

我们看到最终查找类会调用到native的defineClass()方法。这样,我们的加载流程就算讲完了。

总结:到这里类的加载已经讲完了,这里只是说明了一下流程,希望对大家会有点帮助,这也是插件化中很重要的一步。

你可能感兴趣的:(FrameWork源码解析(7)-ClassLoader及dex加载过程)