Java基础-类加载机制(ClassLoader)

Android知识总结

一、JVM 的类加载架构

ClassLoader
  • 1、Bootstrap ClassLoader(启动类加载器)
    Bootstrp加载器是用C++语言写的,它是在Java虚拟机启动后初始化的,它主要负责加载%JAVA_HOME%/jre/lib,-Xbootclasspath参数指定的路径及%JAVA_HOME%/jre/classes中的类。
  • 2、Extension ClassLoader(扩展类加载器)
    Bootstrp loader加载ExtClassLoader,并且将ExtClassLoader的父加载器设Bootstrp loader.ExtClassLoader是用Java写的,具体来说就是sun.misc.Launcher$ExtClassLoader,ExtClassLoader主要加载%JAVA_HOME%/jre/lib/ext,此路径下的所有classes目录以及java.ext.dirs系统变量指定的路径中类库
  • 3、Application ClassLoader(系统类加载器) Bootstrp loader加载完ExtClassLoader后,就会加载AppClassLoader,并且将AppClassLoader`的父加载器指定为 ExtClassLoader。AppClassLoader也是用Java写成的,它的实现类是 sun.misc.Launcher$AppClassLoader,另外我们知道ClassLoader中有个getSystemClassLoader方法,此方法返回的正是AppclassLoader.AppClassLoader主要负责加载classpath所指定的位置的类或者是jar文档,它也是Java程序默认的类加载器。
  • 4、Custom ClassLoader(自定义类加载器java.lang.ClassLoader的子类)
    在程序运行期间, 通过java.lang.ClassLoader的子类动态加载class文件, 体现java动态实时类装入特性。

二、类加载器的特性

每个ClassLoader都维护了一份自己的名称空间, 同一个名称空间里不能出现两个同名的类。
为了实现java安全沙箱模型顶层的类加载器安全机制, java默认采用了 双亲委派的加载链 结构。

双亲委派的加载链

2.1、双亲委托模式(Android 中的类加载)

某个类加载器在加载类时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务或者没有父类加载器时,才自己去加载。

每个类加载器都有自己的加载缓存,当一个类被加载了以后就会放入缓存,等下次加载的时候就可以直接返回了。

当前classLoader的缓存中没有找到被加载的类的时候,委托父类加载器去加载,父类加载器采用同样的策略,首先查看自己的缓存,然后委托父类的父类去加载,一直到BootClassLoader。

双亲委托机制特点:

  • 因为这样可以避免重复加载,当父亲已经加载了该类的时候,就没有必要子ClassLoader再加载一次。

  • 考虑到安全因素,我们试想一下,如果不使用这种委托模式,那我们就可以随时使用自定义的String来动态替代java核心api中定义类型,这样会存在非常大的安全隐患,而双亲委托的方式,就可以避免这种情况,因为String已经在启动时被加载,所以用户自定义类是无法加载一个自定义的ClassLoader。(沙箱安全机制)。
    防止核心类APi被篡改,通过委托的方式,保证核心.class不被篡改,即使被篡改也不会被加载,即使被加载也不会是同一个class对象,因为不同的加载器加载同一个.class也不是同一个Class对象。只有 同一个类加载器加载的 同一个 class 文件才是同一个对象。 这样则保证了Class的执行安全。

ClassLoader.java

    //parent 是父类 BootClassLoader
    private final ClassLoader parent;

    protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException{
        // 检查class是否有被加载
        Class c = findLoadedClass(name);
        if (c == null) {
            long t0 = System.nanoTime();
            try {
                if (parent != null) {
                    //如果parent不为null,则调用parent的loadClass进行加载
                    c = parent.loadClass(name, false);
                } else {
                    //parent为null,则调用BootClassLoader进行加载
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
            }
            if (c == null) {
                // 如果都找不到就自己查找
                long t1 = System.nanoTime();
                c = findClass(name);
            }
        }
        return c;
    }

因此我们自己创建的ClassLoader: new PathClassLoader("/sdcard/xx.dex", getClassLoader()); 并不仅仅只能加载 xx.dex中的class。
值得注意的是: c = findBootstrapClassOrNull(name);
按照方法名理解,应该是当parent为null时候,也能够加BootClassLoader加载的类。
new PathClassLoader("/sdcard/xx.dex", null),能否加载Activity.class?
但是实际上,Android当中的实现为:(Java不同)

    private Class findBootstrapClassOrNull(String name) {
        return null;
    }

2.1、findClass

可以看到在所有父ClassLoader无法加载Class时,则会调用自己的 findClass 方法。 findClass 在ClassLoader中的定义为:

protected Class<?> findClass(String name) throws ClassNotFoundException {
    throw new ClassNotFoundException(name);
}

其实任何ClassLoader子类,都可以重写loadClass 与 findClass 。一般如果你不想使用双亲委托,则重写loadClass 修改其实现。而重写 findClass 则表示在双亲委托下,父ClassLoader都找不到Class的情况下,定义自己如何去查找一个Class。而我们的PathClassLoader 会自己负责加载 MainActivity 这样的程序中自己编写的类,利用双亲委托父ClassLoader加载Framework中的 Activity 。说PathClassLoader 并没有重写 loadClass ,因此我们可以来看看PathClassLoader中的 findClass 是如何实现的。最终在BaseDexClassLoader中调用。

    public BaseDexClassLoader(String dexPath, File optimizedDirectory,String
            librarySearchPath, ClassLoader parent) {
        super(parent);
        //创建DexPathList
        this.pathList = new DexPathList(this, dexPath, librarySearchPath,
                optimizedDirectory);
    }
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
        //查找指定的class
        Class c = pathList.findClass(name, suppressedExceptions);
        if (c == null) {
            ClassNotFoundException cnfe = new ClassNotFoundException("Didn't find class \"" +
                    name + "\" on path: " + pathList);
            for (Throwable t : suppressedExceptions) {
                cnfe.addSuppressed(t);
            }
            throw cnfe;
        }
        return c;
    }

实现非常简单,从 pathList 中查找class。继续查看 DexPathList

/*package*/ final class DexPathList {
    public DexPathList(ClassLoader definingContext, String dexPath,
                       String librarySearchPath, File optimizedDirectory) {
        //.........
        // splitDexPath 实现为返回 List<File>.add(dexPath)
        // makeDexElements 会去 List<File>.add(dexPath) 中使用DexFile加载dex文件返回 Element数组
        this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
                suppressedExceptions, definingContext);
        //.........
    }

    public Class findClass(String name, List<Throwable> suppressed) {
        //从element中获得代表Dex的 DexFile
        for (Element element : dexElements) {
            DexFile dex = element.dexFile;
            if (dex != null) {
                //查找class
                Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
                if (clazz != null) {
                    return clazz;
                }
            }
        }
        if (dexElementsSuppressedExceptions != null) {
            suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
        }
        return null;
    }

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

从上面可以得到 dexElements 数组里面保存的每个Element就对应一个dex文件,即把dex文件保存在dexElements数组中。最后便利dex文件数组,拿到对应dex文件的class文件。

public final class DexFile {
    private static native Class defineClassNative(String name, ClassLoader loader, Object cookie, DexFile dexFile)

    private DexFile(String sourceName, String outputName, int flags, ClassLoader loader,
            DexPathList.Element[] elements) throws IOException {
        if (outputName != null) {
            try {
                String parent = new File(outputName).getParent();
                if (Libcore.os.getuid() != Libcore.os.stat(parent).st_uid) {
                    throw new IllegalArgumentException("Optimized data directory " + parent
                            + " is not owned by the current user. Shared storage cannot protect"
                            + " your application from code injection attacks.");
                }
            } catch (ErrnoException ignored) {
                // assume we'll fail with a more contextual error later
            }
        }

        mCookie = openDexFile(sourceName, outputName, flags, loader, elements);
        mInternalCookie = mCookie;
        mFileName = sourceName;
        //System.out.println("DEX FILE cookie is " + mCookie + " sourceName=" + sourceName + " outputName=" + outputName);
    }


    public Class loadClassBinaryName(String name, ClassLoader loader, List<Throwable> suppressed) {
        return defineClass(name, loader, mCookie, this, suppressed);
    }

    private static Class defineClass(String name, ClassLoader loader, Object cookie,
                                     DexFile dexFile, List<Throwable> suppressed) {
        Class result = null;
        try {
            result = defineClassNative(name, loader, cookie, dexFile);
        } catch (NoClassDefFoundError e) {
            if (suppressed != null) {
                suppressed.add(e);
            }
        } catch (ClassNotFoundException e) {
            if (suppressed != null) {
                suppressed.add(e);
            }
        }
        return result;
    }

    static public DexFile loadDex(String sourcePathName, String outputPathName,
        int flags) throws IOException {
        return loadDex(sourcePathName, outputPathName, flags, null, null);
    }

    static DexFile loadDex(String sourcePathName, String outputPathName,
        int flags, ClassLoader loader, DexPathList.Element[] elements) throws IOException {
        return new DexFile(sourcePathName, outputPathName, flags, loader, elements);
    }
}
类加载关系图

三、Android 中 CloassLoader示意图

任何一个 Java 程序都是由一个或多个 class 文件组成,在程序运行时,需要将 class 文件加载到 JVM 中才可以使用,负责加载这些 class 文件的就是 Java 的类加载机制。ClassLoader 的作用简单来说就是加载 class 文件,提供给程序运行时使用。每个 Class 对象的内部都有一个 classLoader 字段来标识自己是由哪个 ClassLoader 加载的。

class Class<T> {
   ... 
  private transient ClassLoader classLoader;
   ...
 }
CloassLoader

从图中可以看出ClassLoader是一个抽象类,而它的具体实现类主要有:

  • ClassLoader
    ClassLoader是一个抽象类,其中定义了ClassLoader的主要功能;
  • BootClassLoader
    BootClassLoader是ClassLoader的内部类,用于预加载preload()常用类以及一些系统Framework层级需要的类;
  • BaseDexClassLoader
    BaseDexClassLoader继承ClassLoader,是抽象类ClassLoader的具体实现类,PathClassLoader和DexClassLoader都继承它
  • PathClassLoader
    PathClassLoader用于Android应用程序类加载器(自定义的类)。如果是加载非系统应用程序类,则会加载data/app/目录下的dex文件以及jar、zip、apk中 classes.dex。
  • DexClassLoader
    DexClassLoader可以加载自定义的dex文件以及jar、zip、apk中的classes.dex,,也支持从SD卡进行加载。

四、类加载示意图

类加载示意图

4.1、热修复流程

PathClassLoader 中存在一个Element数组,Element类中存在一个dexFile成员表示dex文件,即:APK中有X个dex,则Element数组就有X个元素。

在 PathClassLoader 中的Element数组为:[patch.dex , classes.dex , classes2.dex]。如果存在Key.class位于 patch.dex与classes2.dex中都存在一份,当进行类查找时,循环获得 dexElements 中的DexFile,查找到了 Key.class则立即返回,不会再管后续的element中的DexFile是否能加载到Key.class了。

因此实际上,一种热修复实现可以将出现Bug的class单独的制作一份fix.dex文件(补丁包),然后在程序启动时,从服务器下载fix.dex保存到某个路径,再通过fix.dex的文件路径,用其创建 Element 对象,然后将这个 Element 对象插入到我们程序的类加载器 PathClassLoader 的 pathList 中的 dexElements 数组头部。这样在加载出现Bug的class时会优先加载fix.dex中的修复类,从而解决Bug。

步骤

  • 1、获取当前应用的 PathClassLoader
  • 2、反射获取 DexPathList 的属性对象 pathList
  • 3、反射修改 pathList 的 dexElements;
    3.1、把补丁包 path.dex 转化为 Element
    3.2、获取 pathList 的 dexElements 属性(old);
    3.3、patch + old 合并,并反射赋值给 pathList 的 dexElements。

五、自定义类加载器

自定义类加载器,主要有两种方法:

-(1)遵守双亲委派模型:继承 ClassLoader,重写 findClass()方法。
-(2)破坏双亲委派模型:继承 ClassLoader,重写 loadClass()方法。 通常我们推荐采用第一种方法自定义类加载器,最大程度上的遵守双亲委派模型

我们看一下实现步骤:

(1)创建一个类继承 ClassLoader 抽象类
(2)重写 findClass()方法
(3)在 findClass()方法中调用 defineClass()

public class MyClassLoader extends ClassLoader {
    private String libPath;

    public DiskClassLoader(String path) {
        libPath = path;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        String fileName = getFileName(name);
        File file = new File(libPath, fileName);
        try {
            FileInputStream is = new FileInputStream(file);
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            int len = 0;
            try {
                while ((len = is.read()) != -1) {
                    bos.write(len);
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
            byte[] data = bos.toByteArray();
            is.close();
            bos.close();
            return defineClass(name, data, 0, data.length);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return super.findClass(name);
    }

    //获取要加载的 class 文件名
    private String getFileName(String name) {
        int index = name.lastIndexOf('.');
        if (index == -1) {
            return name + ".class";
        } else {
            return name.substring(index + 1) + ".class";
        }
    }
}

接下来我们就可以自己去加载类了,使用方法大体就是两行

    MyClassLoader diskLoader = new MyClassLoader("D:\\lib");// 加 载class 文 件 
    TestClass c = diskLoader.loadClass("com.fdd.Test");

小福利

在线源码阅读:
ANDROID社区

AndroidXRef

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

推荐阅读更多精彩内容