JAVA进阶(1)—— 类加载器

类加载器

一、前言

1、动态加载

1)插件化 —— 当我们项目越来越大,我们可以通过插件化来减少应用的内存,然后动态加载那些插件。
2)热修复 —— 如果我们的应用频繁的更新,频繁的发布新版本,肯定会造成用户体验下降 ,那么可以用动态加载技术在不发布新版本的情况下更新一些模块。

那么既然要用动态加载,就肯定涉及到类加载器。

2、JVM使用Java类

Java源程序(.java 文件)在经过Java 编译器编译之后就被转换成Java字节码(.class 文件)。类加载器负责读取Java字节码,并转换成java.lang.Class类的一个实例。每个这样的实例用来表示一个Java类。通过此实例的newInstance()方法就可以创建出该类的一个对象

二、Java中类加载器

1、类加载器与类本身确定类的唯一性

对于一个类,这个类本身和真正加载它的类加载器共同确定其在虚拟机中的唯一性。 使用两个类加载器进行加载同一个类,那么这两个类是不相等的,那么虚拟机中会存在两个同名的类。同一类加载器实例,同名的类仅加载一次,下次通过取缓存获取。

2、类加载器

2.1、作用

根据一个指定类,找到或者生成其对应的字节代码,然后从这些字节代码中定义出一个 Java 类,即 java.lang.Class类的一个实例

2.2、ClassLoader方法
  • getParent() 返回该类加载器的父类加载器
  • loadClass(String name) 启动类加载
  • defineClass(String name, byte[] b, int off, int len) 从二进制流中加载Class,final修饰
  • getSystemClassLoader() 获取系统类加载器,static修饰
  • findClass(String name) Class<?> 其中调用了defineClass方法
  • findLoadedClass(String name) Class<?> 返回已被虚拟机加载的类
2.3、类加载器的树状结构
类加载器的树状结构

说明:对应的是类的双亲委派机制的逻辑关系

2.4、Java三种预定义类型类加载器

启动类加载器(Bootstrap ClassLoader,也称为引导类加载器)

  • 加载JAVA核心库,并且是虚拟机识别的(这点很重要)类加载到虚拟机中。
  • 用本地代码实现的类加载器,不继承java.lang.ClassLoader
  • 无法直接获取引用并使用

扩展类加载器(Extension ClassLoader)

  • 加载JAVA的扩展库
  • getParnet()返回null
  • 可以获取并使用

应用程序类加载器(Application ClassLoader,也称为系统类加载器)

  • 根据应用的类路径(CLASSPATH)来加载类
  • ClassLoader中的getSystemClassLoader()方法的返回值
  • 可以直接使用这个类加载器。如果没有自定义类加载器,一般情况下该加载器是程序中的默认的类加载器
2.5、类加载双亲委派机制

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

实现双亲委派模型的代码在loadClass()方法中:

protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {  
    //检查请求加载的类是否已经加载过了
    Class<?> c = findLoadedClass(className);  
    if (clazz == null) {  
        try {  
            //尝试使用父类加载器加载,父类加载器不为空,则使用父类加载器尝试加载
            if(parent != null){
                c = parent.loadClass(className, false);
            }
            //如果父类加载器为null,则使用启动类加载器作为父加载器
            else{
                c = findBootstrapClassOrNull(name);  
            }
        } catch (ClassNotFoundException e) {  
            // 如果父类加载器抛出异常,说明父类加载器无法完成加载请求
        }  
        if (c == null) {  
            //父类无法完成加载时调用本身的findClass方法来进行类加载
            c = findClass(className);  //其中调用了defineClass()方法
        }  
    }  
    if(resolve){
        resolveClass(c);
    }
    //如果加载过了,就直接返回已经加载的类
    return clazz;  
} 

初始化加载器:启动类的加载过程,通过调用loadClass来实现
定义加载器:真正完成类的加载工作,通过调用defineClass来实现

优点

大家都知道Object类是个基础类,如果我们自己写了一个Object类,那么如果没有双亲委派模型的话,再加上我们没有用启动类加载器去加载我们写的这个Object类的话,系统中会存在两个Object类(参考上述的类在虚拟机中的唯一性)。

有了双亲委派模型,我们写了一个Object类,会先去检查它是否加载了(肯定已经加载了),那么我们写的这个就不会
被重复加载,也就保证了基础类的唯一性。就算没有检查,根据上面关于启动类加载器的介绍,必须是
虚拟机识别的,Object存放在rt.jar中,我们写的不会被识别。

基础类在任何环境下都是同一个类(即加载器在任何情况下都是同一个),这就是
双亲委派模型的作用。每次加载请求都会委派给处于最顶端的启动类加载器进行加载,虚拟机识别rt.jar,那么就
保证了每次都是由启动类加载器加载Object。

2.6、自定义类加载器

场景1:应用通过网络传输的加密字节码,此时需要先解密再定义类
场景2:加载存储在文件系统上的 Java 字节代码

自定义类加载器符合双亲委派模型

我们根据上面的介绍知道,双亲委派模型的逻辑都在loadClass()方法中,那么我们为了不破坏双亲委派模型,自定义类加载
器时不去重写loadClass()方法,而是重写findClass()方法,将自己的类加载逻辑写到findClass()方法中,在loadClass()方法中,最后父类加载器无法加载的时候,调用的就是findClass()方法。这样我们就保证了我们自定义的类加载器是符合双亲委派模型的。如果重写loadClass()方法,会出现一系列错误,比如基础类加载不上等。

父类加载器是加载此类加载器 Java 类的类加载器(一般为系统类加载器)

二、Android类加载器

1、基本介绍

android中的虚拟机是Dalvik,它不是标准的Java虚拟机,所以在类加载机制上,和Java中的类加载器有一些区别。

在java标准的虚拟机中,如果自定义类加载器,会继承ClassLoader,并重写findClass()方法,在内部调用defineClass()去从一个二进制流中加载Class

在Android中,defineClass()方法什么都没做。那么在Dalvik虚拟中,动态加载类就需要另外由ClassLoader派生出的两个类:DexClassLoaderPathClassLoader。这两个类重载了ClassLoader的findClass()方法,并没有重写loadClass()方法,所以这两个类加载器符合双亲委派模型。

注意:Dalvik虚拟机识别的是dex文件,而不是class文件,因此,加载的是dex文件、apk文件(包含dex文件)或jar文件(dx命令执行过后的jar,首先将.jar编译成.dex文件,然后再压缩成.jar)

2、相关类方法

PackageManager

  • queryIntentActivities(Intent intent, int flags) List<ResolveInfo> 返回匹配该intent的Activity信息。

ResolveInfo

  • activityInfo ActivityInfo

ActivityInfo

  • applicationInfo ApplicationInfo

ApplicationInfo

  • sourceDir 应用的apk的安装目录,重要!
  • nativeLibraryDir 应用的jni库的目录

3、PathClassLoader

  • PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) dexPath:指定要加载的dex文件路径;librarySearchPath:c/c++依赖的本地库路径,可以为null;parent:上一级的类加载器,一般为this.getClassLoader()
  • loadClass(String name) Class<?> 继承自ClassLoader的方法

4、DexClassLoader

  • DexClassLoader(String dexPath, String optimizedDirectory,String librarySearchPath, ClassLoader parent) dexPath:dex文件路径;optimizedDirectory:dex文件解压缩后存放目录;librarySearchPath:c/c++依赖的本地库路径,可以为null;parent:上一级的类加载器,一般为this.getClassLoader()

5、对比

PathClassLoader:不能主动从zip包中释放出dex,只支持直接操作dex格式文件,或者已经安装的apk(因为已经安装的apk在手机的data/dalvik-cache中存在缓存的dex文件)。

DexClassLoader:支持.apk、.jar和.dex文件,并且会在指定的outpath路径释放出dex文件。

6、动态加载的类使用方式

  • 反射调用,但过多的反射有一定的性能开销,代码复杂凌乱。
  • 使用接口编程的方式来调用对应的方法,毕竟.dex文件也是我们自己维护的,所以可以把方法抽象成公共接口,把这些接口也复制到主项目里面去,就可以通过这些接口调用动态加载得到的实例的方法。

7、热修复原理

原理:BaseDexClassLoader调用findClass去加载类的时候,会调用DexPathList#findClass()如下图:

因此把修复后的dex插入到最前面,遍历开始找到class就直接返回,那么有bug的dex或class就不会被加载。

public class HotFixEngine {
    public static final String DEX_OPT_DIR = "optimize_dex";//dex的优化路径
    public static final String DEX_FILE_E = "dex";//扩展名
    public static final String FIX_DEX_PATH = "fix_dex";//fixDex存储的路径

    /**
     * 入口方法,给外部调用<br/>
     * 复制SD卡中的补丁文件到dex目录
     */
    public static void copyDexFileToAppAndFix(Context context, String dexFileName) {
        File path = new File(Environment.getExternalStorageDirectory(), dexFileName);
        if (!path.exists()) {
            Toast.makeText(context, "没有找到补丁文件", Toast.LENGTH_SHORT).show();
            return;
        }
        if (!path.getAbsolutePath().endsWith(DEX_FILE_E)) {
            Toast.makeText(context, "补丁文件格式不正确", Toast.LENGTH_SHORT).show();
            return;
        }
        File dexFilePath = context.getDir(FIX_DEX_PATH, Context.MODE_PRIVATE);
        File dexFile = new File(dexFilePath, dexFileName);
        if (dexFile.exists()) {
            dexFile.delete();
        }
        //copy
        InputStream is = null;
        FileOutputStream os = null;
        try {
            is = new FileInputStream(path);
            os = new FileOutputStream(dexFile);
            int len;
            byte[] buffer = new byte[1024];
            while ((len = is.read(buffer)) != -1) {
                os.write(buffer, 0, len);
            }
            if (dexFile.exists()) {
                //复制成功,进行修复,重要!!!!
                new HotFixEngine().loadDex(context, dexFile);
            }
            path.delete();//删除sdcard中的补丁文件,或者你可以直接下载到app的路径中
            is.close();
            os.close();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (is != null) {
                try {
                    is.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (os != null) {
                try {
                    os.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    /**
     * fix
     *
     * @param context
     */
    public void loadDex(Context context, File dexFile) {
        if (context == null) {
            return;
        }
        File fixDir = context.getDir(FIX_DEX_PATH, Context.MODE_PRIVATE);
        //mrege and fix
        mergeDex(context, fixDir, dexFile);
    }

    /**
     * 合并dex
     *
     * @param context
     * @param fixDexPath
     */
    public void mergeDex(Context context, File fixDexPath, File dexFile) {
        try {
            //创建dex的optimize路径
            File optimizeDir = new File(fixDexPath.getAbsolutePath(), DEX_OPT_DIR);
            if (!optimizeDir.exists()) {
                optimizeDir.mkdir();
            }
            //加载自身Apk的dex,通过PathClassLoader
            PathClassLoader pathClassLoader = (PathClassLoader) context.getClassLoader();
            //找到dex并通过DexClassLoader去加载
            //dex文件路径,优化输出路径,null,父加载器
            DexClassLoader dexClassLoader = new DexClassLoader(dexFile.getAbsolutePath(), optimizeDir.getAbsolutePath(), null, pathClassLoader);
            //获取app自身的BaseDexClassLoader中的pathList字段
            Object appDexPathList = getDexPathListField(pathClassLoader);
            //获取补丁的BaseDexClassLoader中的pathList字段
            Object fixDexPathList = getDexPathListField(dexClassLoader);

            Object appDexElements = getDexElements(appDexPathList);
            Object fixDexElements = getDexElements(fixDexPathList);
            //合并两个elements的数据,将修复的dex插入到数组最前面
            Object finalElements = combineArray(fixDexElements, appDexElements);
            //给app 中的dex pathList 中的dexElements 重新赋值
            setFiledValue(appDexPathList, appDexPathList.getClass(), "dexElements", finalElements);
            Toast.makeText(context, "修复成功!", Toast.LENGTH_SHORT).show();
            //最后需要通过Android build-tools 中的dx命令打包一个没有bug的dex
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 获得pathList中的dexElements
     *
     * @param obj
     * @return
     * @throws NoSuchFieldException
     * @throws IllegalAccessException
     */
    public Object getDexElements(Object obj) throws NoSuchFieldException, IllegalAccessException {
        return getField(obj, obj.getClass(), "dexElements");
    }

    /**
     * 获取指定classloader中的pathList字段的值(DexPathList类型)
     * BaseDexClassLoader是PathClassLoader和DexClassLoader的父类
     */
    public Object getDexPathListField(Object classLoader) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
        return getField(classLoader, Class.forName("dalvik.system.BaseDexClassLoader"), "pathList");
    }

    /**
     * 获取一个字段的值
     *
     * @return
     */
    public Object getField(Object obj, Class<?> clz, String fieldName) throws NoSuchFieldException, IllegalAccessException {
        Field field = clz.getDeclaredField(fieldName);
        field.setAccessible(true);
        return field.get(obj);
    }

    /**
     * 为指定对象中的字段重新赋值
     *
     * @param obj
     * @param claz
     * @param filed
     * @param value
     */
    public void setFiledValue(Object obj, Class<?> claz, String filed, Object value) throws NoSuchFieldException, IllegalAccessException {
        Field field = claz.getDeclaredField(filed);
        field.setAccessible(true);
        field.set(obj, value);
    }


    /**
     * 两个数组合并
     *
     * @param arrayLhs
     * @param arrayRhs
     * @return
     */
    private static Object combineArray(Object arrayLhs, Object arrayRhs) {
        Class<?> localClass = arrayLhs.getClass().getComponentType();
        int i = Array.getLength(arrayLhs);
        int j = i + Array.getLength(arrayRhs);
        Object result = Array.newInstance(localClass, j);
        for (int k = 0; k < j; ++k) {
            if (k < i) {
                Array.set(result, k, Array.get(arrayLhs, k));
            } else {
                Array.set(result, k, Array.get(arrayRhs, k - i));
            }
        }
        return result;
    }
}

参考文献

Android中的动态加载机制
Android动态加载学习总结(一):类加载器
Android 热修复(全网最简单的热修复讲解)

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

推荐阅读更多精彩内容