为了账号安全,请及时绑定邮箱和手机立即绑定

类加载原理分析&动态加载Jar/Dex(二)

标签:
Java Android

上篇文章讲解了《类加载的原理》,本篇是《动态加载Jar/Dex》实战篇。

同一个Class = ClassName + PackageName + ClassLoaderId(instance)

只要是写在Eclipse中的类(其实就是指classPath)都是被AppClassLoader加载的,其他以classLoader.load("com.xx.xx") 形式的都是自定义ClassLoader 经过 处理的。

知道这一点是很重要的,后面我们自定义ClassLoader可以利用该特性。

Paste_Image.png

同理,Android中能够写在 Android Studio中的代码 (classPath)都是被PathClassLoader加载的,其他以classLoader.load("com.xx.xx") 形式的都是自定义ClassLoader 经过 处理的。

所以一般情况下Java和Android我们经常利用的是
Java -> AppClassLoader
Android -> PathClassLoader

而自定义ClassLoader利用的是
Java -> URLClassLoader(可选) or ClassLoader的子类
Android -> DexClassLoader(可选) or ClassLoader的子类

动态加载方案(Java版)

  1. 反射方式
    插件类全部写在远端,然后用自定义ClassLoader加载,只留一个Object引用(Object由super.loadClass() 让parent处理), 然后用反射调用插件类的方法。这种比较简单,案例后面代码会给出。
  2. 接口方式
    本地项目里面的留一个接口类,远端实现该接口的方法,然后打包远端实现类和接口类,经过测试,Java提供给我们的URLClassLoader不需要去除jar里面的接口类的,原理就是利用双亲委派 优先使用parent加载项目里面的接口类,所以才能够多态引用到该jar中的实现类。

再来试试我们自己的解决方案

待加载的类Dog

package com.less.bean;
public interface Animal {
    void say();
}

package com.less.bean;
public class Dog implements Animal {

    @Override
    public void say() {
        System.out.println("I am a Dog");
    }
}

文件加载ClassLoader

public class FileBadClassLoader extends ClassLoader {

    @Override
    public Class<?> loadClass(String name) throws ClassNotFoundException {
        try {
            String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
            File file = new File("F:/" + fileName);
            if(!file.exists()){
                System.out.println("========> 没找到文件,使用默认逻辑加载 " + name);
                return super.loadClass(name);
            }else{
                System.out.println("========> 找到文件 ,开始加载 " + name);
                InputStream inputStream = new FileInputStream(file);
                byte[] data = new byte[inputStream.available()];
                inputStream.read(data);
                inputStream.close();
                // Android和Java的重要实现区别就在于这里,Android不支持直接加载.class或.jar,而是.dex,所以被修改为能够动态加载dex的逻辑。
                return defineClass(name, data, 0, data.length);
            }
        } catch (Exception e) {
            throw new ClassNotFoundException(name);
        }
    }
}

这是一个破坏了双亲委派模式的 自定义ClassLoader,加载顺序和双亲委派 正好相反。
检测磁盘是否存在我们先要加载的class文件,如果存在就自己加载,不存在就交给super.loadClass();默认逻辑处理。

public static void main(String[] args){
    FileBadClassLoader badClassLoader = new FileBadClassLoader ();
    Class<?> clazz = badClassLoader.loadClass("com.less.bean.Dog");
    Animal animal = (Animal) clazz.newInstance();
    animal.say();
}

Paste_Image.png

Paste_Image.png

如图所示,F盘存在两个class,Animal(接口)和Dog(实现类)
运行main,结果报了一个ClassCastException。

Paste_Image.png

为什么会出现这种错
Animal animal = (Animal) clazz.newInstance(); 不是正常引用吗?
根据上面的分析,这个错误很容易判断,因为Animal和Dog都存在F盘,所以都被FileBadClassLoader加载了,而 上面有个结论 提到 写在Eclipse代码中的类(classpath)都是被APPClassLoader加载的,这行代码用了两个不同的ClassLoader实例,所以Animal animal = (Animal) 这里的引用和clazz.newInstance()不是一个类型的,所以不能互相转换。

如何解决
删除F盘中的Animal.class即可,删除后FileBadClassLoader找不到Animal.class就交给APPClassLoader加载,加载成功后
Animal animal = (Animal) clazz.newInstance()就可以相互引用了。

Paste_Image.png

备注:贴上测试代码

public class Main {

    /**
     * 个人吐槽下,ClassLoader的加载类的loadClass和findClass方法的名称互换下感觉更贴切些,毕竟名字和代码逻辑反了,搞得有时候犯迷糊。find是先找后加载,注意下这里就行了。
     * 注: 即使是自己实现的类加载器,如果myClassLoader.loadClass(clazz);加载的clazz被parent加载了,那么clazz.getClassLoader()就是parent而不是myClassLoader.
     * @throws Exception
     */
    public static void main(String[] args) throws Exception {
        testIsSameClassLoader();
//      testIsSameClass();
//      testObeyParent1();
//      testObeyParent2();
//      testDisObeyParent();
//      testDynamicByReflect();
//      testDynamicByInterface1();
//      testDynamicByInterface2();
    }

    private static void testIsSameClassLoader() {
        ClassLoader classLoader1 = ClassLoader.getSystemClassLoader();
        ClassLoader classLoader2 = Main.class.getClassLoader();
        ClassLoader classLoader3 = Cat.class.getClassLoader();

        ClassLoader classLoader4 = ClassLoader.class.getClassLoader();
        System.out.println(classLoader1 == classLoader2);
        System.out.println(classLoader2 == classLoader3);

        System.out.println(classLoader4);// BootstrapClassLoader
    }

    private static void testIsSameClass() throws ClassNotFoundException {
        // 测试两个ClassLoader加载一个类的关系。
        ClassLoader badclassLoader1 = new BadClassLoader();
        ClassLoader badclassLoader2 = new BadClassLoader();
        Class<?> badClazz1 = badclassLoader1.loadClass("com.less.bean.Cat");
        Class<?> badClazz2 = badclassLoader2.loadClass("com.less.bean.Cat");
        // 说明判断两个类是否是同一个类型的前提是: 同类 + 同包 + 同类加载器实例,即使两者都是加载远端的同一个类文件,但是却不是一个类型,故无法强制转换或者互相引用等等。
        System.out.println(badClazz1 == badClazz2);

    }

    private static void testObeyParent1() throws Exception {
        // 通常推荐重写的是findClass,而不是loadClass, 因为双亲委派的具体逻辑就写在loadClass中,loadClass的逻辑里如果parent加载失败,则会调用我们重写的findClass
        // 完成加载,这样就可以保证新写出的类加载器是符合双亲委派规则的,保证了各个类加载器基础类的 统一问题(越基础的类越有上层的类加载器加载)。
        // 但是双亲委派也是可以破坏掉的,常见的使用场景就是热修复,OGSi是这方面非常好的应用。

        // 无参的ClassLoader会默认设置  getSystemClassLoader() 即AppClassLoader为parent,见ClassLoader的构造器源码。
        ClassLoader mygoodClassLoader1 = new ClassLoader() {

            @Override
            protected Class<?> findClass(String name) throws ClassNotFoundException {
                return super.findClass(name);
            }
        };
        System.out.println("mygoodClassLoader1 ---> " + mygoodClassLoader1.getParent());
        Class<?> clazz1 = mygoodClassLoader1.loadClass("com.less.bean.Person");
        Object obj1 = clazz1.newInstance();
        message("[real classLoader] clazz1 => " + obj1.getClass().getClassLoader());
    }

    private static void testObeyParent2() {
        // 但是如果我们设置ClassLoader的parent为null,那么就没有parent替我们找了,然后直接交给BootstrapClassLoader找,肯定也找不到了,最后我们自己找好了,
        // 结果发现ClassLoader.findClass默认实现只是抛出一个异常,throw new ClassNotFoundException(name);所以要想要我们自己的ClassLoader能够加载类就必须实现findClass。
        ClassLoader mygoodClassLoader2 = new ClassLoader(null) {
            @Override
            protected Class<?> findClass(String name) throws ClassNotFoundException {
                // 这里默认实现是--> throw new ClassNotFoundException(name);
                return super.findClass(name);
            }
        };
        System.out.println("mygoodClassLoader2 ---> " + mygoodClassLoader2.getParent());

        try {
            Class<?> clazz2 = mygoodClassLoader2.loadClass("com.less.bean.Dog");
            Object obj2 = clazz2.newInstance();
            message("[real classLoader] clazz2 => " + obj2.getClass().getClassLoader());
        } catch (Exception e) {
            message("[real classLoader] clazz2 => " + e.toString());
        }
    }

    private static void testDisObeyParent() throws Exception {
        // 破坏双亲委派,直接自己加载
        ClassLoader mybadClassLoader = new BadClassLoader();
        System.out.println("mybadClassLoader ---> " + mybadClassLoader.getParent());
        Class<?> clazz3 = mybadClassLoader.loadClass("com.less.bean.Cat");
        Object obj3 =  clazz3.newInstance();
        message("[real classLoader] clazz3 => " + obj3.getClass().getClassLoader());

        /********************** 测试classpath(即我们自己的项目package下的类 和 远程加载的类是否相等) **********************
         * 分析: 从BadClassLoader打印的日志可以看出,动态加载一个类的时候,此类里面关联的类(如成员变量,extend,局部变量等等)都会交给此ClassLoader的loadClass进行处理。
         * 而且如果没有任何继承的情况下,其[隐式父类Object]都会交给其处理,这时候我们需要把这个Object或它包含的类 都交给 parent处理,否则我们这里的代码都没法有类型去引用这个生成的实例。
         * 所有 Java 应用都至少需要引用 java.lang.Object类,也就是说在运行的时候,java.lang.Object这个类需要被加载到 Java 虚拟机中。如果这个加载过程由 Java 应用自己的类加载器来完成的话,很可能就存在多个版本的 java.lang.Object类
         * 而且这些类之间是不兼容的。对于 Java 核心库的类的加载工作由引导类加载器来统一完成,需要保证Java 应用所使用的都是同一个版本的 Java 核心库的类,是互相兼容的。
         ***********************************************************************************************/
        System.out.println(obj3.getClass().getClassLoader());
        System.out.println(Cat.class.getClassLoader());
        System.out.println(obj3 instanceof Cat);
    }

    private static void testDynamicByReflect() throws Exception {
        // URLClassLoader 只能加载jar文件,可以替代我们自定义的ClassLoader加载远程jar,Android也给我们提供了DexClassLoader来实现动态加载dex.
        File file = new File("F:/Monkey.jar");
        URL url = file.toURL();
        URLClassLoader urlClassLoader = new URLClassLoader(new URL[] { url });
        Class<?> clazz = urlClassLoader.loadClass("com.less.bean.Monkey");
        Object monkey = clazz.newInstance();
        Method method = clazz.getDeclaredMethod("say");
        method.invoke(null);// 使用静态方法
        method.invoke(monkey);// 使用对象调用

    }

    private static void testDynamicByInterface1() throws Exception {
        BadClassLoader badClassLoader = new BadClassLoader();
        Class<?> clazz = badClassLoader.loadClass("com.less.bean.Dog");
        Animal animal = (Animal) clazz.newInstance();
        animal.say();
    }

    private static void testDynamicByInterface2() throws Exception {
        File file = new File("F:/Dog.jar");
        URL url = file.toURL();
        URLClassLoader urlClassLoader = new URLClassLoader(new URL[] { url });
        Class<?> clazz = urlClassLoader.loadClass("com.less.bean.Dog");
        Animal animal = (Animal) clazz.newInstance();
        animal.say();
    }

    private static void message(String string) {
        StringBuilder builder = new StringBuilder();
        builder.append("\r\n");
        builder.append("[********************* ");
        builder.append(string);
        builder.append(" *********************]");
        builder.append("\r\n");
        System.out.println(builder.toString());
    }

    static class BadClassLoader extends ClassLoader {

        @Override
        public Class<?> loadClass(String name) throws ClassNotFoundException {
            try {
                String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
                File file = new File("F:/" + fileName);
                if(!file.exists()){
                    System.out.println("========> 没找到文件,使用默认逻辑加载 " + name);
                    return super.loadClass(name);
                }else{
                    System.out.println("========> 找到文件 ,开始加载 " + name);
                    InputStream inputStream = new FileInputStream(file);
                    byte[] data = new byte[inputStream.available()];
                    inputStream.read(data);
                    inputStream.close();
                    // Android和Java的重要实现区别就在于这里,Android不支持直接加载.class或.jar,而是.dex,所以被修改为能够动态加载dex的逻辑。
                    return defineClass(name, data, 0, data.length);
                }
            } catch (Exception e) {
                throw new ClassNotFoundException(name);
            }
        }
    }
}

动态加载(Android版)
经过Java版的测试,我们基本上使用URLClassLoader即可解决大部分需求。
但是经过Android版的测试,发现直接使用DexClassLoader加载类(接口方式调用)却跟我们上面自定义的FileClassLoader一样的结果。

远端没有去掉接口文件,调用报如下错误:

Class resolved by unexpected DEX: Lcom/less/plugin/Dog;(0x94f51010):0x87a01000 ref [Lcom/less/plugin/Animal;] Lcom/less/plugin/Animal;(0x94f08288):0x84988000
W/dalvikvm: (Lcom/less/plugin/Dog; had used a different Lcom/less/plugin/Animal; during pre-verification)
I/dalvikvm: Failed resolving Lcom/less/plugin/Dog; interface 0 'Lcom/less/plugin/Animal;'
W/dalvikvm: Link of class 'Lcom/less/plugin/Dog;' failed

这个错误按前面的分析不会产生才会,因为DexClassLoader也是双亲委派。

File dexOutputDir = getDir("dex", 0);

DexClassLoader classLoader = new DexClassLoader(outPath, dexOutputDir.getAbsolutePath(), null, getClassLoader());
Class<?> clazz = classLoader.loadClass("com.less.plugin.Dog");
Animal animal = (Animal) clazz.newInstance();

分析:当加载插件Dog时,根据双亲委派模型,首先让parent(getClassLoader即PathClassLoader)加载,PathClassLoader并不能加载Dog,所以给了DexClassLoader加载,Dog被加载后,Dog类里面引用(implement)的Animal开始被加载,Animal存在于本地和远端,优先被PathClassLoader加载,故 Animal是可以成功引用Dog的,思路基本和URLClassLoader一致,且URLClassLoader没有任何问题。

请查看 http://androidxref.com/4.4.4_r1/xref/dalvik/vm/oo/Resolve.cpp

#include "Dalvik.h"
#include <stdlib.h>
/*
 * Find the class corresponding to "classIdx", which maps to a class name
 * string.  It might be in the same DEX file as "referrer", in a different
 * DEX file, generated by a class loader, or generated by the VM (e.g.
 * array classes).
 *
 * Because the DexTypeId is associated with the referring class' DEX file,
 * we may have to resolve the same class more than once if it's referred
 * to from classes in multiple DEX files.  This is a necessary property for
 * DEX files associated with different class loaders.
 *
 * We cache a copy of the lookup in the DexFile's "resolved class" table,
 * so future references to "classIdx" are faster.
 *
 * Note that "referrer" may be in the process of being linked.
 *
 * Traditional VMs might do access checks here, but in Dalvik the class
 * "constant pool" is shared between all classes in the DEX file.  We rely
 * on the verifier to do the checks for us.
 *
 * Does not initialize the class.
 *
 * "fromUnverifiedConstant" should only be set if this call is the direct
 * result of executing a "const-class" or "instance-of" instruction, which
 * use class constants not resolved by the bytecode verifier.
 *
 * Returns NULL with an exception raised on failure.
 */
ClassObject* dvmResolveClass(const ClassObject* referrer, u4 classIdx,
    bool fromUnverifiedConstant)
{
    // 略
}

上面有一段注释关键注释:Because the DexTypeId ....
大致翻译为:因为DexTypeId是和DEX文件相关联的,我们必须防止相同的类被多个DEX文件引用,这是不同类加载器关联的DEX文件s的必须的特性。

struct DexTypeItem {
    u2  typeIdx;// DexTypeId中的索引下标
};
//rect-mapped "type_list".
struct DexTypeList {
    u4  size;// DexTypeItem的个数
    DexTypeItem list[1];// DexTypeItem变长数组
};

解决方案
本地只保留接口,远端只保留实现类。

总结:

  1. Java的动态加载jar或类非常简单,你可以直接使用URLClassLoader或者灵活自定义ClassLoader。
  2. Android 基本DexClassLoader就足够了,但要注意上面提到的问题,接口和实现 必须只有一份。
  3. 因为使用动态加载,所以项目里面只能有接口,所以每次加载dex都需要下载,如果希望有一份默认的实现,推荐打包后的dex放入assets目录中,需要更新的时候再根据file.lastModified()判断是否从网络下载新的。
  4. 插件类 如果希望用到第三方库,如okhttp,一般建议在项目里面依赖okhttp,而打包插件的时候去除okhttp依赖即可。除非你十分确定,主项目不会使用某个库,总之确保主项目dex和插件 永远没有重复的类。
动态加载dex案例

AS新建一个plugin library,利用gradle将非常方便生成Jar文件并dx化。

建一个接口文件

public interface Animal {
    public interface Callback {
        void done(String message);
    }
    void say(Callback callback);
}

创建实现类

public class Dog implements Animal {
    OkHttpClient okhttp = new OkHttpClient();

    public void say(final Callback callback) {
        Builder builder = (new Builder()).url("http://www.baidu.com");
        Call call = this.okhttp.newCall(builder.build());
        call.enqueue(new okhttp3.Callback() {
            public void onFailure(Call call, IOException e) {
                callback.done("error");
            }

            public void onResponse(Call call, Response response) throws IOException {
                String content = response.body().string();
                callback.done(content);
            }
        });
    }
}

在gradle中生成3个Task,分别用于以下用途:

  1. 打Jar包
  2. Jar包转为dex格式的Jar
  3. 删除Plugin Library的实现类

Paste_Image.png

Paste_Image.png

然后写个测试类就可以运行app了,下面测试类是把assets下的插件dynamic.jar-1.0.jar 写入SDcard,点击Button时 使用DexClassLoader动态加载即可,代码很简单。

public class TestActivity extends AppCompatActivity {
    private static final String TAG = "less";
    private String fileName = "dynamic.jar-1.0.jar";
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_test);
        writeToApp();
    }

    public void handle(View view) {
        try {
            String outPath = Environment.getExternalStorageDirectory() + File.separator + fileName;
            // 注意这个输出dex的路径需要在自己的目录里
            File dexOutputDir = getDir("dex", 0);

            DynamicClassLoader classLoader = new DynamicClassLoader(outPath, dexOutputDir.getAbsolutePath());
            Class<?> clazz = classLoader.loadClass("com.less.plugin.Dog");
            Animal animal = (Animal) clazz.newInstance();

            animal.say(new Animal.Callback() {
                @Override
                public void done(String message) {
                    Log.i(TAG, " ===> " + message);
                }
            });

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private void writeToApp() {
        String outPath = Environment.getExternalStorageDirectory() + File.separator + fileName;

        InputStream inputStream = null;
        BufferedInputStream bufferedInputStream = null;
        FileOutputStream fileOutputStream = null;
        BufferedOutputStream bufferedOutputStream = null;

        try {
            inputStream = getResources().getAssets().open(fileName);
            bufferedInputStream = new BufferedInputStream(inputStream);
            fileOutputStream = new FileOutputStream(new File(outPath));
            bufferedOutputStream = new BufferedOutputStream(fileOutputStream);
            byte[] buffer = new byte[1024];
            int hasRead = -1;
            while ((hasRead = bufferedInputStream.read(buffer) ) != -1) {
                bufferedOutputStream.write(buffer,0,hasRead);
                bufferedOutputStream.flush();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            try {
                bufferedInputStream.close();
                fileOutputStream.close();
                bufferedInputStream.close();
                inputStream.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        if (BuildConfig.DEBUG) {
            Log.d(TAG, "写入成功");
        }
    }
}
点击查看更多内容
1人点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
移动开发工程师
手记
粉丝
30
获赞与收藏
274

关注作者,订阅最新文章

阅读免费教程

感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消