Android中的ClassLoader分析

前言

这篇文章主要是讲解Android中的ClassLoader


Dalvik VM

Dalvik是Google公司自己设计用于Android平台的Java虚拟机。它可以支持已转换为.dex(即Dalvik Executable)格式的Java应用程序的运行,.dex格式是专为Dalvik设计的一种压缩格式,可以减少整体文件尺寸,提高I/o操作的类查找速度所以适合内存和处理器速度有限的系统。

关于Dalvik更多内容参考这篇文章:Dalvik概述
说的直白一点就是针对Android而生的JVM的升级。他与JVM的不同:

clipboard.png

第一点不用解释
第二点后面会详细讲解
第三点:JVM只存在一个,DVM 可以存在多个,某一个引用程序挂掉以后不会影响的其他程序,保证程序的稳定性。
第四点:基于栈 表示方法的调用时在栈中完成的 寄存器运行更快


ART与Dalvik的不同
ART模式英文全称为:Android runtime,谷歌Android 4.4系统新增的一种应用运行模式,与传统的Dalvik模式不同,ART模式可以实现更为流畅的安卓系统体验,对于大家来说,只要明白ART模式可让系统体验更加流畅,不过只有在安卓4.4以上系统中采用此功能。这里只做简单介绍。

clipboard.png

现在市面的手机基本都是ART模式了。


Android中的ClassLoader

JVM的类加载器是将字节码文件通过读取后加载到JVM运行时数据区。而Android中的ClassLoader的作用是一样的,只不过是加载到Dalvik中。

  • Android中的ClassLoader有哪些?

Android中的ClassLoader由一下4个类组成。

图片.png

1:加载Framework层字节码文件
2:加载已经安装到系统中APK文件中的字节码文件(sdk中的文件)
3:加载指定目录中的字节码文件(如lib引入的jar中的文件等)
4:是2.3的父类

从上面我们知道一个应用的运行必须要使用1和2,2中类加载器。

  • Android中的ClassLoader的特点以及作用?

前面提到了委派模式。在Android中叫双亲代理模式。2者的作用与思想是一样的 。

特点:如果字节码在整个加载器类树中被一个加载器加载过 那么在整个系统生命周期中中都不会在重新加载 提高效率
作用:类加载的共享功能与隔离功能都是基于双亲代理模式总结而来的。
共享功能:一些底层(如Framework层)的类被顶层类加载器加载过那么以后在任何地方用到就不用再加载。
隔离功能:不同继承路线类加载器中,加载的类都是一定是不相同的,避免用户写一些可见的类冒充核心的类库。如:Object.lang.String类在程序启动之前就被系统加载了。如果我们自己写的String会将系统的String类替换的话,将会出现严重的安全问题。

双亲代理模式:
Android中的classLoader当加载一个字节码文件的时候首先会询问当前加载器是否已经加载过此类 如果已经加载 那么直接返回不再重复加载。如果没有加载,他会查询当前加载器的Parent类是否已经加载过此字节码。如果加载过直接返回Parent类加载的字节码文件。如果(所有继承链都没有加载过)那么就由子加载器加载并返回。

同一个类指的是相同的类名,包名,已经是同一个类加载器加载的。


Android中的ClassLoader源码讲解

从上面我们知道一个应用的运行必须要使用BootClassLoader和PathClassLoader,2种类加载器。下面我们新建个Android项目来运行下。代码如下:

public class MainActivity extends AppCompatActivity {

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

        ClassLoader classLoader = getClassLoader();
        Log.e("ggxiaozhi", "classLoader: "+ classLoader);
        if (classLoader.getParent()!=null){
            classLoader=classLoader.getParent();
            Log.e("ggxiaozhi", "classLoader-Parent: "+ classLoader);
        }
    }
}

打印结果:

01-12 06:25:26.880 1842-1842/? E/ggxiaozhi: classLoader: dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/com.example.ggxiaozhi.hotfix-1/base.apk"],nativeLibraryDirectories=[/data/app/com.example.ggxiaozhi.hotfix-1/lib/x86, /vendor/lib, /system/lib]]]
01-12 06:25:26.880 1842-1842/? E/ggxiaozhi: classLoader-Parent: java.lang.BootClassLoader@665444a

从打印结果也可以看到确实是这样的。下面我们进入源码分析下。

 protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
            // First, check if the class has already been loaded   (1)
            Class c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
                        c = parent.loadClass(name, false); (2)
                    } 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.
                    long t1 = System.nanoTime();
                    c = findClass(name); (3)

                    // this is the defining class loader; record the stats
                }
            }
            return c;
    }

源码也比较简单:

  1. (1)首先查找我们的ClassLoader是否加载过我们的当前的class文件。
  2. (2)如果没有找到就查找他的分类有没有加载过。
  3. (3)如果父类也没有找到过就去通过findClass(name);去加载这个类文件。

点进去findClass()这个方法发现,这是一个空实现说明他的子类实现了这个方法。而他的子类正上上面我们提到的4种类加载器。由于我们在实际中Framework层的加载器我们接触不到,所以重点分下其他三种类加载器。由于这几个方法我们都看不到所有我们通过源码网查去查询。
源码地址
使用教程文章

  • DexClassLoader
1/**
22 * A class loader that loads classes from {@code .jar} and {@code .apk} files
23 * containing a {@code classes.dex} entry. This can be used to execute code not
24 * installed as part of an application.
25
36 public class DexClassLoader extends BaseDexClassLoader {
55    public DexClassLoader(String dexPath, String optimizedDirectory,
56            String librarySearchPath, ClassLoader parent) {
57        super(dexPath, new File(optimizedDirectory), librarySearchPath, parent);
58    }
59}

可以看到它里面就一个构造方法,通过类的注释可以简单理解为它是加载来自.jar和.apk本身的class文件路径,也可以用来执行不作为应用程序的一部分安装的代码。(所以它也是我们后面要讲得动态更新加载的核心加载器)
这里面的参数含义分别为:

  • dexPath:要加载的指定文件下的dex文件路径
  • optimizedDirectory :这是一个copy路径。可以理解应用在安装时,先将dex文件copy到应用的内部路径,待需要加载dex文件时去应用内部路径找到dex文件去加载。(中间还会做一些优化)。这个路径是应用的内部路径,在DexClassLoader下这个参数一定不能为空。
  • librarySearchPath 加载native相关dex文件。
  • ClassLoader 父类加载器
  • PathClassLoader
public class PathClassLoader extends BaseDexClassLoader {

37    public PathClassLoader(String dexPath, ClassLoader parent) {
38        super(dexPath, null, null, parent);
39    }
40

63    public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
64        super(dexPath, null, librarySearchPath, parent);
65    }
66}

从类的注解上来看这个加载器是Android使用作为它的系统类加载器和它的应用程序类加载器。也就是加载Android项目工程中的类文件加载器。参数和上面一样,正是因为缺少optimizedDirectory参数所以它只能加载项目本省的dex文件中的类

  • BaseDexClassLoader

BaseDexClassLoader是上面2个类加载器的父类。由于上面2个类加载器都没有具体的逻辑方法。还记上面我们在查找ClassLoader时知道加载类的方法是findClass(name).由于这两个雷都没有实现这个方法,那么一定就是在他们的父类BaseDexClassLoader中实现的。下面看下他的源码:

29 public class BaseDexClassLoader extends ClassLoader {
30    private final DexPathList pathList;
31
45    public BaseDexClassLoader(String dexPath, File optimizedDirectory,
46            String librarySearchPath, ClassLoader parent) {
47        super(parent);
48        this.pathList = new DexPathList(this, dexPath, librarySearchPath, optimizedDirectory);
49    }
50
51    @Override
52    protected Class<?> findClass(String name) throws ClassNotFoundException {
53        List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
54        Class c = pathList.findClass(name, suppressedExceptions);
55        if (c == null) {
56            ClassNotFoundException cnfe = new ClassNotFoundException("Didn't find class \"" + name + "\" on    path: " + pathList);
57            for (Throwable t : suppressedExceptions) {
58                cnfe.addSuppressed(t);
59            }
60            throw cnfe;
61        }
62        return c;
63    }

在这个类中找到了上面我们在ClassLoader中未实现的findClass方法。发现他是调用了DexPathList类中的方法,并且这个类是在构造方法中已经初始化并肩参数传入其中了。所以我们在这个类中寻找一下DexPathList#findClass():

 final class DexPathList {
51    private static final String DEX_SUFFIX = ".dex";
52    private static final String zipSeparator = "!/";
53
54    /** class definition context */
55    private final ClassLoader definingContext;
56
57    /**
58     * List of dex/resource (class path) elements.
59     * Should be called pathElements, but the Facebook app uses reflection
60     * to modify 'dexElements' (http://b/7726934).
61     */
62    private Element[] dexElements;
63
64    /** List of native library path elements. */
65    private final Element[] nativeLibraryPathElements;
66
67    /** List of application native library directories. */
68    private final List<File> nativeLibraryDirectories;
69
70    /** List of system native library directories. */
71    private final List<File> systemNativeLibraryDirectories;
72
73    /**
74     * Exceptions thrown during creation of the dexElements list.
75     */
76    private IOException[] dexElementsSuppressedExceptions;
77
78   
96    public DexPathList(ClassLoader definingContext, String dexPath,
97            String librarySearchPath, File optimizedDirectory) {
98         ...

122        this.definingContext = definingContext;
123
124        ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
125        // save dexPath for BaseDexClassLoader
126        this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
127                                           suppressedExceptions, definingContext);
128
129 
140        this.systemNativeLibraryDirectories =
141                splitPaths(System.getProperty("java.library.path"), true);
142        List<File> allNativeLibraryDirectories = new ArrayList<>(nativeLibraryDirectories);
143        allNativeLibraryDirectories.addAll(systemNativeLibraryDirectories);
144
145        this.nativeLibraryPathElements = makePathElements(allNativeLibraryDirectories,
146                                                          suppressedExceptions,
147                                                          definingContext);
148
149        if (suppressedExceptions.size() > 0) {
150            this.dexElementsSuppressedExceptions =
151                suppressedExceptions.toArray(new IOException[suppressedExceptions.size()]);
152        } else {
153            dexElementsSuppressedExceptions = null;
154        }
155    }
156
157   
        ...
        
         private static Element[] makePathElements(List<File> files, File optimizedDirectory,
280                                              List<IOException> suppressedExceptions) {
281        return makeElements(files, optimizedDirectory, suppressedExceptions, false, null);
282    }
283
       //这个方法的作用就是将指定路径class文件转化成dexfile(dex文件) 同时存在Element[]数组中 //最后在findClass文件中使用
284    private static Element[] makeElements(List<File> files, File optimizedDirectory,
285                                          List<IOException> suppressedExceptions,
286                                          boolean ignoreDexFiles,
287                                          ClassLoader loader) {
        //创建Element数组
288        Element[] elements = new Element[files.size()];
289        int elementsPos = 0;
290        /*
291         * Open all files and load the (direct or contained) dex files
292         * up front.
293         */
294        for (File file : files) {//遍历dex文件集合
295            File zip = null;
296            File dir = new File("");
            //dex文件对应的java类
297            DexFile dex = null;
               //获取文件路径
298            String path = file.getPath();
               //获取文件名
299            String name = file.getName();
300
                //path是文件夹继续往下遍历
301            if (path.contains(zipSeparator)) {
302                String split[] = path.split(zipSeparator, 2);
303                zip = new File(split[0]);
304                dir = new File(split[1]);
305            } else if (file.isDirectory()) {
306                // We support directories for looking up resources and native libraries.
307                // Looking up resources in directories is useful for running libcore tests.
308                elements[elementsPos++] = new Element(file, true, null, null);
309            } else if (file.isFile()) {//如果是文件 最后都会调用loadDexFile()f方法创建dex文件
310                if (!ignoreDexFiles && name.endsWith(DEX_SUFFIX)) {//这个文件是不是以.dex文件为后缀的
311                    // Raw dex file (not inside a zip/jar).
312                    try {
                           //如果是就创建一个dex文件
313                        dex = loadDexFile(file, optimizedDirectory, loader, elements);
314                    } catch (IOException suppressed) {
315                        System.logE("Unable to load dex file: " + file, suppressed);
316                        suppressedExceptions.add(suppressed);
317                    }
318                } else {//如果这个文件值zip格式
319                    zip = file;
320
321                    if (!ignoreDexFiles) {
322                        try {
323                            dex = loadDexFile(file, optimizedDirectory, loader, elements);
324                        } catch (IOException suppressed) {
325                            /*
326                             * IOException might get thrown "legitimately" by the DexFile constructor if
327                             * the zip file turns out to be resource-only (that is, no classes.dex file
328                             * in it).
329                             * Let dex == null and hang on to the exception to add to the tea-leaves for
330                             * when findClass returns null.
331                             */
332                            suppressedExceptions.add(suppressed);
333                        }
334                    }
335                }
336            } else {
337                System.logW("ClassLoader referenced unknown path: " + file);
338            }
339
340            if ((zip != null) || (dex != null)) {
341                elements[elementsPos++] = new Element(dir, false, zip, dex);
342            }
343        }
344        if (elementsPos != elements.length) {
345            elements = Arrays.copyOf(elements, elementsPos);
346        }
347        return elements;
348    }

        /**
351     * Constructs a {@code DexFile} instance, as appropriate depending on whether
352     * {@code optimizedDirectory} is {@code null}. An application image file may be associated with
353     * the {@code loader} if it is not null.
354     */
355    private static DexFile loadDexFile(File file, File optimizedDirectory, ClassLoader loader,
356                                       Element[] elements)
357            throws IOException {
358        if (optimizedDirectory == null) {//这个路径是空就说明一个dex文件没有 我们就要创建一个dex文件
359            return new DexFile(file, loader, elements);
360        } else {//否自会通过解压等处理最后得到DexFile
361            String optimizedPath = optimizedPathFor(file, optimizedDirectory);
362            return DexFile.loadDex(file.getPath(), optimizedPath, 0, loader, elements);
363        }
364    }
413    public Class findClass(String name, List<Throwable> suppressed) {
414        for (Element element : dexElements) {
415            DexFile dex = element.dexFile;
416
417            if (dex != null) {
418                Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
419                if (clazz != null) {
420                    return clazz;
421                }
422            }
423        }
424        if (dexElementsSuppressedExceptions != null) {
425            suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
426        }
427        return null;
428    }
429
             ....
448
489
490    /**
491     * Element of the dex/resource/native library path
492     */
493    /*package*/ static class Element {
494        private final File dir;
495        private final boolean isDirectory;
496        private final File zip;
497        private final DexFile dexFile;
498
499        private ClassPathURLStreamHandler urlHandler;
500        private boolean initialized;
501
502        public Element(File dir, boolean isDirectory, File zip, DexFile dexFile) {
503            this.dir = dir;
504            this.isDirectory = isDirectory;
505            this.zip = zip;
506            this.dexFile = dexFile;
507        }
508
509        @Override public String toString() {
510            if (isDirectory) {
511                return "directory \"" + dir + "\"";
512            } else if (zip != null) {
513                return "zip file \"" + zip + "\"" +
514                       (dir != null && !dir.getPath().isEmpty() ? ", dir \"" + dir + "\"" : "");
515            } else {
516                return "dex file \"" + dexFile + "\"";
517            }
518        }
566
567       ...
590    }
591}
592

这个类比较长,这里简单讲解下:首先定义一些常量来规定加载.dex文件格式,同时定义了Element属性。在构造方法中先对一些异常处理并初始化一些常量。下面只看我们上步跟踪的方法findClass。发现这个方法先遍历了Element这个数组,而这个数组是通过在构造方法中调用makeElements()方法初始化,然后调用DexFile#loadClassBinaryName()方法,说明这个类也不是最终加载类的地方。不过在继续跟踪之前我们先对Element有个理解。其实他就是DexPathList中的一个内部类,谁对dex文件的包装,将路径与最终加载的类DexFile封装在一起,并进行一些字符串的拼凑。接着我们在进入DexFile#loadClassBinaryName()方法:

public Class loadClassBinaryName(String name, ClassLoader loader, List<Throwable> suppressed) {
289        return defineClass(name, loader, mCookie, this, suppressed);
290    }
291
292    private static Class defineClass(String name, ClassLoader loader, Object cookie,
293                                     DexFile dexFile, List<Throwable> suppressed) {
294        Class result = null;
295        try {
296            result = defineClassNative(name, loader, cookie, dexFile);
297        } catch (NoClassDefFoundError e) {
298            if (suppressed != null) {
299                suppressed.add(e);
300            }
301        } catch (ClassNotFoundException e) {
302            if (suppressed != null) {
303                suppressed.add(e);
304            }
305        }
306        return result;
307    }
387      private static native Class defineClassNative(String name, ClassLoader loader, Object cookie,
388                                                  DexFile dexFile)
389            throws ClassNotFoundException, NoClassDefFoundError;

这里直接看loadClassBinaryName方法他调用了defineClass方法,最后调用defineClassNative方法。defineClassNative()这个方法是native,是用C/C++ 实现的,往后我们就无法查看了。不过经过前面分析,最后native方法是大概就是通过C/C++
根据指定传入类的name去查找dex文件中对应的class文件相关信息数据,然后将dex文件的中的运行数据区中的数据拼成一个class字节码返回。应用层使用。


总结:
注意dex可以理解成把所有的class文件压缩成了一个dex文件 dex对应转化的是jar不是class

我的理解dex文件包含各个路径的jar文件.zip文件 不管用没有用到 等用到了采用类加载器去根据类名去加载这个类 信息然后最后通过native层在dex文件查找返回这个类的信息并返回。(这个整个串联的流程我也好串联起来。待后期有更深的研究在完善这部分) 如果大家有相关的书籍推荐下。

Android中的类加载器流程。

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

推荐阅读更多精彩内容