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.
 *
 * <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()方法。这样,我们的加载流程就算讲完了。

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,132评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,802评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,566评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,858评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,867评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,695评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,064评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,705评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,915评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,677评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,796评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,432评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,041评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,992评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,223评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,185评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,535评论 2 343

推荐阅读更多精彩内容