FrameWork源码解析(7)-ClassLoader及dex

作者: ZJ_Rocky | 来源:发表于2017-12-22 15:12 被阅读81次

    主目录见: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.
     *
     * <p>This class loader requires an application-private, writable directory to
     * cache optimized classes. Use {@code Context.getDir(String, int)} to create
     * such a directory: <pre>   {@code
     *   File dexOutputDir = context.getDir("dex", 0);
     * }</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.
     */
    

    上面英文注释分别说明了三个方面的知识:
    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()开始的:

    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<File> 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<File> 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

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