Android Multidex原理及实现

Android Multidex原理及实现

一、什么是分包,分包能解决什么问题?

      正常apk打包后的文件目录是含有AndroidManifest.xml、R、resource.arcs(资源的索引)、assets、lib、classes.dex这几个模块,而分包后又是怎么样的情况呢?分包之后dex将会多出几个dex,出现classes2.dex,classes3.dex等.
      分包解决的问题就是为了解决65536问题,安卓内部使用的short来存放方法数,而short的最大范围就是65536,所以一旦一个dex的方法超过了65536,就会抛出异常.

二、Java中的常见类加载器

    Java中的常见类加载器有以下:
      BootstrapClassLoader:纯c++实现的类加载器,没有对应的Java类,主要加载jre/lib/目录下的核心库
      ExtClassLoader:类的全名是sun.misc.launcher ExtClassLoader,jre/lib/ext/AppClassLoader:sun.misc.Launcher AppClassLoader,主要加载CLASSPATH路径下的包

为了验证上面的说法,特地写了例子:

public class Main {
    public static void main(String[] args) {
        //第一步 获取 Main的类加载器
        Class mainClass = Main.class;
        ClassLoader mainLoader = mainClass.getClassLoader();
        //输出AppClassLoader
        System.out.println("mainLoader's name:"+mainLoader.toString());

        //第二步 打印AppClassLoader的加载路径
        URL[] murls = ((URLClassLoader)mainLoader).getURLs();
        //输出 classPath路径
        print(murls);

        //第三步 通过getParent方法,获取mainLoader中的parent字段
        ClassLoader parentLoader = mainLoader.getParent();
        //输出ExtClassLoader
        System.out.println("parentLoader's name:"+parentLoader.toString());

        //第四步 打印ExtClassLoader的加载路径
        URL[] murlsExt = ((URLClassLoader)parentLoader).getURLs();
        //输出 jre/lib/ext/下的扩展包
        print(murlsExt);

        //第五步 通过getParent方法,获取parentLoader中的parent字段
        ClassLoader parentLoaderTwo = parentLoader.getParent();
        //输出空指针异常
        System.out.println("parentLoaderTwo's name:"+parentLoaderTwo.toString());

        //第六步 打印BootstrapClassLoader的加载路径
        Class launcherClass = Class.forName("sun.misc.Launcher");
        Method method_getClassPath = launcherClass.getDeclareMethod("getBootstrapClassPath",null);
        if(method_getClassPath != null){
            method_getClassPath.setAccessible(true);
            Object mObj = method_getClassPath.invoke(null.null);
            if(mObj!=null){
                Method methodGetURLs = mObj.getClass().getDeclareMethod("getURLs",null);
                if(methodGetURLs!=null){
                    methodGetURLs.setAccessible(true);
                    URL[] murlBoot = (URL[])methodGetURLs.invoke(mObj,null);
                    //输出jre/lib 下的jar包
                    print(murlBoot);
                }
            }
        }
    }

    //打印url数组
    public static void print(URL[] murls){
        for(URL url:murls){
            System.out.println(url);
        }
    }
}

三、Java中父类委托加载机制原理

接下来看这段代码:

public class Main {
    public static void main(String[] args) {
        Class clazz = Main.class;
        ClassLoader loader = clazz.getClassLoader();
        System.out.println("loader's name:"+loader.toString());

        Class listClazz = ArrayList.class;
        ClassLoader listLoader = listClazz.getClassLoader();
        //空指针异常
        System.out.println("listloader's Name:"+listLoader.toString());
    }
}

一个输出将会输出appClassLoader,因为加载的是classPath路径下的类,而第二个输出将会报出空指针异常,因为ArrayList是存在于jre/lib/rt.jar中,而这个jar中的代码是BootstrapClassLoader加载的,但是因为它是纯C++的类加载器,没有java类,所以报空指针异常.
源码分析:
一个Class的加载是通过类加载的loadClass方法加载进来的

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

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

      从上面代码可以清晰的看出,当当前的ClassLoader是AppClassLoader的时候,会去检测class有没有被当前classloader加载过,如果没有再去调用父类的loadClass方法,在parent.loadClass()方法中,同样先去检测当前classLoader有没有被加载过,没有被加载过,这个时候再走到parent.loadClass(name,false)这行代码,这个时候因为当前的类加载器是ExtClassLoader,它的parent为空,所以会走到findBootstralClassOrNull(name)方法,去看bootstrapClassLoader有没有加载过,如果都没有加载过,则报出空指针异常.这种机制叫做:父委托加载机制.
简单总结:
      AppClassLoader–(委托父加载器)–>ExtClassLoader–(委托父加载器)—>由于父加载器是空,就委托BootstrapClassLoader去加载.
      由于BootstrapClassLoader是顶级的类加载器,它不会委托任何加载器去加载.
      如果BootstrapClassLoader加载成功,会将加载的类返回给ExtClassLoader,ExtClassLoader继续返回给AppClassLoader.
      如果BootstrapClassLoader加载失败,这时就需要ExtClassLoader到jre/lib/ext目录下去加载,如果加载成功就直接返回给AppClassLoader,如果没有加载成功,就需要appClassLoader到classPath目录下去加载.
      整个过程类似于一个递归过程.
这里写图片描述

四、为什么要引入父委托机制呢?

      当工程中创建了一个和jdk中一模一样的类的时候,这个类是不会被加载的,比如工程中创建一个类名和包名都是和jdk中一样的ArrayList,这时自己创建的ArrayList是不会被创建的,仍然加载的是jdk中的类,这样做为了保证软件系统的安全性,假如自己在代码写了一个ArrayList类,里面注入了一段恶意代码,如果被加载进来了,就相当于加载了一段恶意代码,别人就很有可能利用这个漏洞来攻击你的代码.

五、Android中常用的类加载器

      PathClassLoader:加载data/app目录下的apk文件,从这个目录可以看出,PathClassLoader主要用来加载已经安装了的apk
      DexClassLoader:加载路径需要在创建DexClassLoader时传入,也就是说可以加载任何路径下的apk/dex/jar

类加载器在Android中的继承关系图:
这里写图片描述

六、实现分包原理及实现

如何分包以及整个过程可以查看这两篇博客:
http://blog.csdn.net/mynameishuangshuai/article/details/52703029
http://blog.csdn.net/mynameishuangshuai/article/details/52716877

七、gradle分包原理以及另一种动态加载分dex方法

一、谷歌官方提供的分包
      因为Android系统在启动应用时只加载了主dex(Classes.dex),其他的 dex 需要我们在应用启动后进行动态加载安装。 Google 官方方案是如何加载的呢,Google官方支持Multidex 的 jar 包是 android-support-multidex.jar,该 jar 包从 build tools 21.1 开始支持.
      dex包的加载主要是依赖于Android中的BaseDexClassLoader,BaseDexClassLoader中有一个属性DexPathList,DexPathList中有一个dexElements数组,里面存放的就是dex数组,而使用谷歌官方提供的分包后,是将PathClassLoader加载的dex与DexClassLoader的dex合并到一个新的elements数组,再重新赋值到dexElements.(PathClassLoader与DexClassLoader是BaseDexClassLoader)
二、手动动态加载dex

 /**
     * 动态加载分dex
     */
    private void install() {
        //创建存放dex的目录
        File dexDir = new File(this.getFilesDir(), "dex");
        if (dexDir != null && !dexDir.exists()) {
            dexDir.mkdirs();
        }

        //创建存放odex的目录
        File odexDir = new File(this.getFilesDir(), "odex");
        if (odexDir != null && !odexDir.exists()) {
            odexDir.mkdirs();
        }

        File dexFile = new File(dexDir, "libs.apk");
        //copy assets目录下的libs.apk到dexDir目录下
        int len = -1;
        byte[] buf = new byte[2048];
        InputStream isStream = null;
        FileOutputStream fos = null;
        try {
            isStream = this.getAssets().open("libs.apk");
            if (isStream != null) {
                fos = new FileOutputStream(dexFile);
                while ((len = isStream.read(buf)) != -1) {
                    fos.write(buf, 0, len);
                }
            }
            if (isStream != null) {
                isStream.close();
            }
            if (fos != null) {
                fos.close();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }

        ClassLoader loader = getClassLoader();
        String nativeSoPath = null;
        ApplicationInfo info = getApplicationInfo();
        if (Build.VERSION.SDK_INT > 8) {
            //Android2.2
            nativeSoPath = info.nativeLibraryDir;
        } else {
            nativeSoPath = "/data/data/" + info.packageName + "/lib/";
        }
        DexClassLoader dexClassLoader = new DexClassLoader(dexFile.getAbsolutePath(), odexDir.getAbsolutePath(),
                nativeSoPath, loader.getParent());

        try {
            Field filedParent = ClassLoader.class.getDeclaredField("parent");
            if (filedParent != null) {
                filedParent.setAccessible(true);
                filedParent.set(loader, dexClassLoader);
            }
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }

上面采用的是https://github.com/mmin18/Dex65536进行的分包
原理是在asset目录下生成一个lib.apk,里面存放的是分dex,而它加载的原理就是在PahtClassLoader加载一个类的时候原先是委托给BootClassLoader加载,而现在是交由DexClassLoader去加载,DexClassLoader再委托给BootClassLoader去加载,实现了三个之间的关联.

八、推荐两篇关于分包的文章

Android拆分与加载Dex的多种方案对比
https://mp.weixin.qq.com/s?__biz=MzAwNDY1ODY2OQ==&mid=207151651&idx=1&sn=9eab282711f4eb2b4daf2fbae5a5ca9a&3rd=MzA3MDU4NTYzMw==&scene=6#rd
美团Android DEX自动拆包及动态加载简介
https://tech.meituan.com/mt-android-auto-split-dex.html

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值