multidex原理_android分包multidex原理详解 - CSDN
精华内容
参与话题
  • Android: MultiDex原理和优化

    千次阅读 2020-03-29 20:34:58
    MultiDex: Google提供的第三方库,...5.0下的版本都还占有市场率,且MultiDex内部的运行时原理和国内的热修复、插件化技术方案原理都一致。 Class文件和Dex文件: MultiDex = Multi + Dex(多Dex) Dex (Dalvik-...

    MultiDex:

    Google提供的第三方库,android5.0以前不支持加载多个dex,所以google提供了MultiDex库支持在运行时加载和使用多个Dex.

    5.0下的版本都还占有市场率,且MultiDex内部的运行时原理和国内的热修复、插件化技术方案原理都一致。

    Class文件和Dex文件:

    MultiDex = Multi + Dex(多Dex)

    Dex (Dalvik-executable)

    *.java/.kt----被源代码编译器编译,生成*.class才能被JVM加载和执行。

    手机的硬件有限,所以google开发了专门用在android平台上的虚拟机为android上的程序提供运行环境。

    其中根据系统版本的不同,android平台上的虚拟机分为:

    • Dalvik VM
    • ART VM

    上面的2个与JVM不同的是都不支持直接加载执行class文件,而是需要在源代码被编译为class文件后将多个class文件进一步翻译、重构、解释、压缩等步骤生成一个或多个dex文件,才能在运行的时候被android虚拟机加载、执行。

    class文件记录了对应类文件的所有信息:包括类的常量池、字段信息、方法信息

    所有的class文件被收集起来后会被编译成一个dex文件,这个dex文件会包含前面所有class文件的常量池的信息。

    dex文件针对class文件进行了去冗余操作,使得生成的最终文件体积更小,速度更快。 

    方法数超限问题和解决: 

    apk本质就是压缩包,所以可以将后缀.apk修改为.zip

    解压后:

    原生编译流程默认只会生成一个dex文件。

    当项目代码量很多很多的时候,直到报错:

    Conversion to Dalvik format failed: Unable to execute dex: method ID not in [0,0xffff]: 65536

    即常说的:方法数超过65536个

    一个dex文件是多个class文件的集合:

    即一个dex文件可以包含多个类的多个方法,所有这些方法都会分配索引,在运行的时候虚拟机会根据方法索引去引用对应的方法。其中索引的取值范围是0到65535,所以方法个数限制为65536.

    这些方法包括:

    • 开发者自己编写的方法
    • 第三方库里的方法

    问题解决思路:

    • 尽可能让方法数不要超过这个限制。
    • 应尽量去除混淆,去除不必要的代码。
    • 分散为多个dex(怎么生成多个dex、多出来的dex怎么被加载和运行)=====》MultiDex

    MultiDex就是Google推出的Dex文件支持库,支持在应用程序中使用多个Dex.


    MultiDex的使用:

    • Android5.0+的用法
    • Android5.0-的用法
    • 编译后的apk包结构分析

    1.Android5.0+的用法:

    2.Android5.0-的用法:

     

    并且无自定义的application时候:

    有自定义的application时需要继承MultiDexApplication。

    如果原来的代码继承的不是原生的Application,那么就需要在attachBaseContext()中加上

    MultoDex.install(this)

    使用multidex前后生成的apk在包结构上发生的变化:(这里演示android5.0后)


    MultiDex原理:

    1.编译期原理:

    apk编译过程中,*.class文件通过dx命令行工具来生成classes.dex文件的,

    dx工具负责将class文件转化为虚拟机需要的dex文件。

    jar包就可以生成一个dex文件:

    dx --dex --output=<target.dex> origin.jar

    --multi-dex参数: 

    --multi-dex:
       allows to generate several dex files if needed.

     所以--multi-dex在编译期就是在dx运行过程中,使用--multi-dex参数控制拆分生成多个dex文件,最后一起打包到apk中就得到了可运行的安装包。

    2. 运行期原理:分析入口与整体流程

    该AAR包含了运行期安装的逻辑。

    分析的入口点:

    •  判断虚拟机是否支持MultiDex
    • 解压获取待安装的Dex文件列表
    • 把Dex安装到ClassLoader中

    3. 虚拟机判断

     Dalvik和ART虚拟机的区别:

    Android4.4及其以下版本采用Dalvik虚拟机,Dalvik的JIT(即时编译)对应java.vm.version < 2.0.0

    APK -> INSTALL -> *.DEX-> 启动-> JIT->原生指令->运行

    其中JIT: 运行时动态的将执行频率很高的dex字节码翻译为本地机器码再执行,是发生在应用程序的运行过程中,每一次重新运行都需要重新做这个工作。

    • 启动慢(无缓存)
    • 运行慢比较耗电

    Android4.4后:ART的AOT(提前编译)对应java.vm.version>=2.0.0

    APK -> INSTALL(AOT) -> 原生指令->启动->执行

    其中AOT:安装应用的时候会使用自带的工具把安装包中的所有dex文件进行预编译,将字节码预先编译成机器码,生成一个可以在本地机器上运行的OAT文件并存储在本地,后续不需要编译。

    • 启动速度更快
    • 运行块,耗电少

    所以上面的源码中判断的是虚拟机是Dalvik还是ART

    4.Dex解压:

    List<? extends File> load(...){
      List files;
      if(!isModified(..)){//若apk未修改
         files = loadExistingExtractions(...);//加载之前解压的dex
      }else{
        files = performExtractions(...);//解压dex到指定的目录
        putStoredApkInfo(...);//保存已经解压的apk信息
      }
      return files;
    }

    解压后,原来存在apk里class2.dex文件会被解压到应用内置目录data/data等待被使用。

    5.Dex安装:

    在虚拟机中,编译期生成的.class文件都需要的通过类加载器加载到内存中,才能被运行。android应用程序启动后,系统默认会帮我们创建一个PathClassLoader进行类的加载工作,其有个成员变量pathList: DexPathList其内部包含一个Element数组:dexElements: Element[],数组中的每个元素都会对应一个dex文件,默认情况下系统会加载数组的第一个dex文件(class.dex)。

    在运行的时候,当需要加载某个类时,pathClassLoader会通过pathList的element数组从前往后遍历所有元素,去看哪个dex文件中有对应的类,有的话就直接返回,这样就完成了类的加载。

    void install(){
      //反射获取到pathclassloader的dexpathlist
      Field pathListsField = Multidex.findField(loader,"pathList");
      Object dexPathList = pathListsField.get(loader);
    //生成dex文件对应的element数组
       expandFieldArray(dexPathList,"dexElements",makeDexElements(...))
    }

    6.整体流程:

    javac编译所有的源代码文件生成class文件,然后通过dx工具生成多个dex文件。

    运行期会判断虚拟机版本。

    如果是art虚拟机,则说明已经在系统层面支持了多dex文件的处理,所有的dex的文件在应用安装的时候被提前合并成1个oat文件,运行的也是这个oat文件,不再需要应用程序自己处理了。

    如果是dalvik虚拟机,则说明系统层面并不支持多dex文件的处理,需要自己运用dx安装,需要把2级dex文件解压到应用的特定目录中,得到1个2级dex列表,然后2级dex列表会注入到classloader的操作。


    代码热修复:

    • 代码热修复介绍
    • 代码热修复原理
    • 代码热修复demo

    1.代码热修复:

    已发布apk有bug的时候:

    方案1:重新发布apk

    修改后的x.java-》编译打包-》新的APK-》上架应用市场-》用户手动下载安装apk-》重新启动应用程序-》完成修复

    • 重新上架发布
    • 用户有感知

    方案2:热修复方案

    修改后的x.java-》编译补丁包-》新的补丁包-》远程下发-》后台静默下载安装补丁包-》重新启动应用程序-》完成修复

    • 无需重新发布应用
    • 用户无感知

    2.代码热修复原理:

    • 生成代码补丁包

    ToBeFixed.java->javac->.class->dx->patch.dex

    • 运行时注入代码补丁包

    PathClassLoader->Pathlist->dexElements

    热修复的目的是让补丁包中的类优先被系统加载到,达到修复的目的。

    所以可以将patch.dex插入到dexElements数组的最前面,即注入补丁。

    3.代码实现:

    以一个小demo为例,点击按钮后textview将设置显示待修复类的内容:

    然后生成补丁包:

    删除待除待修复类的源码:

    这个文件就类似于修复BUG后i的源代码。

    生成对应的补丁包:

    使用dx命令生成dex文件:

    查看当前工程使用的build tools版本:

    这就已经生成好补丁包了。

    接下来是运行时注入补丁包:

    package com.yinlei.multidexdemo;
    
    import android.content.Context;
    import android.os.Environment;
    
    import java.io.File;
    import java.io.IOException;
    import java.lang.reflect.Array;
    import java.lang.reflect.Field;
    import java.lang.reflect.Method;
    import java.util.ArrayList;
    import java.util.List;
    
    /**
     * 运行时注入补丁包
     */
    public class HotFixManager {
        public static final String FIXED_DEX_SDCARD_PATH = Environment.getExternalStorageDirectory().getPath() + "/fixed.dex";
    
        /**
         * 注入补丁包
         * @param context
         */
        public static void installFixedDex(Context context){
            try{
                //获取收集目录的补丁包
                File fixedDexFile = new File(FIXED_DEX_SDCARD_PATH);
                //文件不存在,说明不需要热修复
                if (!fixedDexFile.exists()){
                    return;
                }
                // 获取PathCLassLoader的pathList字段
                Field pathListField = ReflectUtils.findField(context.getClassLoader(),"pathList");
                Object dexPathList = pathListField.get(context.getClassLoader());
                
                // 获取DexPathList中的makeDexElements方法
                Method makeDexElements = ReflectUtils.findMethod(dexPathList,"makeDexElements",
                        List.class,File.class,List.class,ClassLoader.class);
                // 把待加载的补丁文件添加到列表中
                ArrayList<File> filesToBeInstalled = new ArrayList<>();
                filesToBeInstalled.add(fixedDexFile);
                
                // 准备makeDexElements()的其他参数
                File optimizedDirecotry = new File(context.getFilesDir(),"fixed_dex");
                ArrayList<IOException> suppressedException = new ArrayList<>();
                
                //调用makeDexElements(),然后得到新的elements数组
                Object[] extraElements = (Object[]) makeDexElements.invoke(dexPathList,filesToBeInstalled,optimizedDirecotry,suppressedException,context.getClassLoader());
                
                //获取原始的elements数组
                Field dexElementsField = ReflectUtils.findField(dexPathList,"dexElements");
                Object[] originElements = (Object[]) dexElementsField.get(dexPathList);
                
                //数组的合并
                Object[] combinedElements = (Object[]) Array.newInstance(originElements.getClass().getComponentType(),originElements.length+extraElements.length);
                //在新的elements数组中先放入补丁包中的数组,再放原来的数组,以确保优先加载我们补丁包中的类
                System.arraycopy(extraElements,0,combinedElements, 0, extraElements.length);//深拷贝
                System.arraycopy(originElements,0,combinedElements,extraElements.length,originElements.length);
                
                // 用新的combinedElements,重新复制给dexPathList
                dexElementsField.set(dexPathList, combinedElements);
            }catch (Exception e){
                throw new RuntimeException(e);
            }
        }
    }
    

    最后就是启动注入逻辑和权限申请:

    现在是未被修复的样子.

    先杀死应用,执行:

     推送后再打开应用:

    现在就是修复后的版本了。

    TODO:

    • 不同系统版本API兼容性
    • 未实现资源热修复,只实现了代码的热修复

    MultiDex的优化:

    1.MultiDex引起的启动ANR:

    启动过程中,Multidex会从原始的APK找到2级dex文件,然后解压存放到应用的/data目录下,然后将解压后的dex注入到PathClassLoader中,首次注入后会调用dexopt将dex文件优化为.odex,应用程序实际加载类的时候都是通过.odex文件加载。

    此过程存在2个可能耗时的操作:

    • 文件的解压
    • dexopt程序的执行

    这些过程一般是在主线程执行,超过5s发生点击事件等无响应就会发生ANR.

    2. MultiDex启动优化方案:

    ANR问题出现的原因是耗时的IO过程在主进程的主线程中执行了。耗时的操作只会发生在应用安装的首次启动过程中,因此解决思路是不在主进程的

    attachBaseContext()中去执行耗时的MultiDex.install()

    改为在新的进程中去进行这些耗时的操作。

    如果启动了新的远程,那么原来的主进程就成为了后台进程,把它挂起也不会导致ANR问题。

    具体思路:

    点击APP应用图标进入应用-》主进程被拉起-》Application.attachBaseContext-》是否已经Dex初始化。

    如果已经进行了首次Dex安装操作,就调用multidex.install()并进行application后续的初始化流程。(dex解压、安装,application初始化)

    如果没dex首次初始化,就进入一个循环:挂起主进程,并不断检测temp.file是否存在,存在就跳出循环。

    主进程挂起后同时会启动一个新的进程(dex加载进程),dex加载进程被拉起后,吊起dexActivity来显示应用程序的启动画面,并创建一个子线程,调用multidex.install()来进行dex的解压安装,然后创建temp.file,最后把dexActivity给finish().这时dex加载进程的执行就结束了。

    然后看主进程,这时的中间文件temp.file已经被dex加载进程创建了,那么主进程在循环检测的过程中就会停止循环,继续执行主进程的初始化逻辑完成整体流程。

     

    总结:关键点是dex安装,上面的小demo就是在这个环节做了手脚:

    展开全文
  • Multidex机制,以及实现原理,产生的问题和解决方案; 1、产生原因 当Android系统安装一个应用的时候,有一步是对Dex进行优化,这个过程有一个专门的工具来处理,叫DexOpt。DexOpt的执行过程是在第一次加载Dex文件...
    Multidex机制,以及实现原理,产生的问题和解决方案;
            1、产生原因
                当Android系统安装一个应用的时候,有一步是对Dex进行优化,这个过程有一个专门的工具来处理,叫DexOpt。DexOpt的执行过程是在第一次加载Dex文件的时候执行的。这个过程会生成一个ODEX文件,即Optimised Dex。执行ODex的效率会比直接执行Dex文件的效率要高很多。

                但是在早期的Android系统中,DexOpt有一个问题,DexOpt会把每一个类的方法id检索起来,存在一个链表结构里面。但是这个链表的长度是用一个short类型来保存的,导致了方法id的数目不能够超过65536个。当一个项目足够大的时候,显然这个方法数的上限是不够的。尽管在新版本的Android系统中,DexOpt修复了这个问题,但是我们仍然需要对低版本的Android系统做兼容。

                为了解决方法数超限的问题,需要将该dex文件拆成两个或多个,为此谷歌官方推出了multidex兼容包,配合AndroidStudio实现了一个APK包含多个dex的功能
                    
            2、实现原理    
                我们以APK中有两个dex文件为例,第二个dex文件为classes2.dex。

                兼容包在Applicaion实例化之后,会检查系统版本是否支持 multidex,classes2.dex是否需要安装。
                如果需要安装则会从APK中解压出classes2.dex并将其拷贝到应用的沙盒目录下。
                通过反射将classes2.dex注入到当前的classloader中。

                流程:  
                    APP启动
                    一系列的校验和初始化
                    提取Dex文件
                    强制提取或者修改过
                        是
                            强制重新提取dex
                            原始apk解压得到Dex,写成zip文件
                        否
                            提取缓存Dex
                    动态装载得到的zip文件
                    运行APP

            3.65535问题:
                我们知道Android中的可执行文件都存储在dex文件中,其中包含已编译的代码来运行你的应用程序。
                Dalvik虚拟机对可执行dex文件的规格是有方法限制的,即一个单一的dex文件的方法总数最多为65536。

                其中包括:
                    引用的Android Framework方法
                    library的方法
                    我们自己书写代码的方法。

                为了突破这个方法数的限制,我们就提出了一个方案——生成多个dex文件。这个多个dex文件的方案,我们又称为multidex方案配置。

                当你确定使用multidex的分包策略的时候,请你先确定自己的代码中都是优秀的。
                你还需要做以下几步:
                    去掉一些未使用的import和library
                    使用ProGuard去掉一些未使用的代码

            4.版本支持:
                5.0以前:
                    Android5.0版本的平台之前,Android使用的是Dalvik Runtime执行的程序代码。默认情况下,限制应用到一个单一的classes.dex。
                    Dalvik字节码文件每APK。为了绕过这个限制,你可以使用multidex支持库,成为你的应用程序的主要部分和DEX文件进行管理,获得额外的dex文件,它们包含的代码。

                5.0之后:
                    Android 5.0和更高的Runtime 如art,本身就支持从应用的APK文件加载多个DEX文件。art支持预编译的应用程序在安装时扫描类(..)。Dex文件编译成一个单一的Android设备上执行.oat文件。

            5.如何使用
                Gradle配置使用Multidex (21.1以后)
                    修改gradle配置(应用层级),支持multiindex;
                    修改mainefest文件,让其支持multidexapplication;

            6.局限性
                如果二DEX文件太大,安装分割dex文件是一个复杂的过程,可能会导致应用程序无响应(ANR)的错误。在这种情况下,你应该尽量的减小dex文件的大小和删除无用的逻辑,而不是完全依赖于multidex。

                在Android 4.0设备(API Level 14)之前,由于Dalvik linearalloc bug(问题22586),multidex很可能是无法运行的。如果希望运行在Level 14之前的Android系统版本,请先确保完整的测试和使用。

                应用程序使用了multiedex配置的,会造成使用比较大的内存。当然,可能还会引起dalvik虚拟机的崩溃(issue 78035)。
                对于应用程序比较复杂的,存在较多的library的项目。multidex可能会造成不同依赖项目间的dex文件函数相互调用,找不到方法。

            7.优化建议
    展开全文
  • Android multidex 使用 与 实现原理

    千次阅读 2018-10-16 15:55:52
    Android multidex 使用 与 实现原理 在Android中一个Dex文件最多存储65536个方法,也就是一个short类型的范围。但随着应用方法数量的不断增加,当Dex文件突破65536方法数量时,打包时就会抛出异常。 为解决该问题,...

    Android multidex 使用 与 实现原理

    在Android中一个Dex文件最多存储65536个方法,也就是一个short类型的范围。但随着应用方法数量的不断增加,当Dex文件突破65536方法数量时,打包时就会抛出异常。

    为解决该问题,Android5.0时Google推出了官方解决方案:MultiDex。

    • 打包时,把一个应用分成多个dex,例:classes.dex、classes2.dex、classes3.dex…,加载的时候把这些dex都追加到DexPathList对应的数组中,这样就解决了方法数的限制。
    • Andorid 5.0之后,ART虚拟机天然支持MultiDex。
    • Andorid 5.0之前,系统只加载一个主dex,其它的dex采用MultiDex手段来加载。

    一、使用

    如何使用,最好参照google官方文档,写的很详细:

    配置方法数超过 64K 的应用

    这里做一下简要说明:

    1、minSdkVersion 为 21 或更高值

    如果是android 5.0以上的设备,只需要设置为multiDexEnabled true

    android {
        defaultConfig {
            ...
            minSdkVersion 21 
            targetSdkVersion 26
            multiDexEnabled true
        }
        ...
    }
    

    2、minSdkVersion 为 20 或更低值

    如果需要适配android 5.0以下的设备,需使用 Dalvik 可执行文件分包支持库

    android {
        defaultConfig {
            ...
            minSdkVersion 15 
            targetSdkVersion 26
            multiDexEnabled true
        }
        ...
    }
    
    dependencies {
      compile 'com.android.support:multidex:1.0.3'
    }
    

    Java代码方面,继承MultiDexApplication 或者 在Application中添加MultiDex.install(this);

    // 继承 MultiDexApplication
    public class MyApplication extends MultiDexApplication { ... }
    
    
    // 或者 在Application中添加 MultiDex.install(this);
    public class MyApplication extends Application {
      @Override
      protected void attachBaseContext(Context base) {
         super.attachBaseContext(base);
         MultiDex.install(this);
      }
    }
    
    

    二、android 5.0 以下 MultiDex 原理

    注:
    源码基于的版本 com.android.support:multidex:1.0.3

    通过 Dalvik可执行文件分包支持库配置方法数超过64K的应用 我们了解到:

    • android 5.0 以下Dalvik虚拟机 只能加载一个主class.dex
    • android.support.multidex.MultiDex.install(this)是对android 5.0 以下Dalvik虚拟机 的兼容;

    这里我们分两部分介绍,一部分是dex文件的加载;一部分是dex文件的抽取。

    2.1、Dex文件的加载

    下面通过跟踪 MultiDex.install(this); 源码,了解其实现原理。

    MultiDex.install(this);

    跟踪 MultiDex.install(this); 源码

    public static void install(Context context) {
        // 如果系统版本大于android 5.0 则天然支持MultiDex
        if (IS_VM_MULTIDEX_CAPABLE) {
            Log.i("MultiDex", "VM has multidex support, MultiDex support library is disabled.");
        } 
        // 系统版本低于android 1.6 抛出异常
        else if (VERSION.SDK_INT < 4) {
            throw new RuntimeException("MultiDex installation failed. SDK " + VERSION.SDK_INT + " is unsupported. Min SDK version is " + 4 + ".");
        } 
        // android 1.6 < android < android 5.0
        else {
            try {
    	        // 获取当前应用信息 应用信息不存在,则返回
                ApplicationInfo applicationInfo = getApplicationInfo(context);
                if (applicationInfo == null) {
                    Log.i("MultiDex", "No ApplicationInfo available, i.e. running on a test Context: MultiDex support library is disabled.");
                    return;
                }
                // MultiDex
                // sourceDir: /data/app/com.xiaxl.demo-2/base.apk
                // dataDir:   /data/user/0/com.xiaxl.demo
                doInstallation(context, new File(applicationInfo.sourceDir), new File(applicationInfo.dataDir), "secondary-dexes", "", true);
            } catch (Exception var2) {
                Log.e("MultiDex", "MultiDex installation failure", var2);
                throw new RuntimeException("MultiDex installation failed (" + var2.getMessage() + ").");
            }
    
            Log.i("MultiDex", "install done");
        }
    }
    

    上边代码中,对1.6 < android < android 5.0 进行判断处理,低于1.6版本抛出异常;高于5.0版本,天然支持MultiDex,所以忽略

    • 如果系统版本大于android 5.0 ART虚拟机 天然支持MultiDex
    • 系统版本低于android 1.6 抛出异常
    • doInstallation MultiDex 处理

    跟踪 MultiDex.doInstallation

    跟踪 MultiDex.doInstallation,查看MultiDex的实现原理

    // 相关入口参数
    // sourceDir: /data/app/com.xiaxl.demo-2/base.apk
    // dataDir:   /data/user/0/com.xiaxl.demo
    // secondaryFolderName: "secondary-dexes"
    // prefsKeyPrefix: ""
    // reinstallOnPatchRecoverableException: true
    private static void doInstallation(Context mainContext, File sourceApk, File dataDir, String secondaryFolderName, String prefsKeyPrefix, boolean reinstallOnPatchRecoverableException) throws IOException, IllegalArgumentException, IllegalAccessException, NoSuchFieldException, InvocationTargetException, NoSuchMethodException, SecurityException, ClassNotFoundException, InstantiationException {
    	// 已安装Apk
        Set var6 = installedApk;
        // 同步
        synchronized(installedApk) {
    	    // 如果 /data/app/com.xiaxl.demo-2/base.apk 未安装
            if (!installedApk.contains(sourceApk)) {
    	        // 添加到 installedApk 这个集合中
                installedApk.add(sourceApk);
                // Android 系统版本大约5.0("java.vm.version"的版本号错误),天然支持MultiDex
                if (VERSION.SDK_INT > 20) {
                    Log.w("MultiDex", "MultiDex is not guaranteed to work in SDK version " + VERSION.SDK_INT + ": SDK version higher than " + 20 + " should be backed by " + "runtime with built-in multidex capabilty but it's not the " + "case here: java.vm.version=\"" + System.getProperty("java.vm.version") + "\"");
                }
                // 根据context 获取 ClassLoader
                ClassLoader loader;
                try {
    	            // 获取ClassLoader,实际上是PathClassLoader
                    loader = mainContext.getClassLoader();
                } catch (RuntimeException var25) {
                    Log.w("MultiDex", "Failure while trying to obtain Context class loader. Must be running in test mode. Skip patching.", var25);
                    return;
                }
                // ClassLoader 获取失败
                if (loader == null) {
                    Log.e("MultiDex", "Context class loader is null. Must be running in test mode. Skip patching.");
                }
                //  
    			else {
    				// 清除老的缓存的Dex目录,来源的缓存目录是"/data/user/0/${packageName}/files/secondary-dexes"
    				// 清空 /data/user/0/com.xiaxl.demo/files/secondary-dexes
                    try {
                        clearOldDexDir(mainContext);
                    } catch (Throwable var24) {
                        Log.w("MultiDex", "Something went wrong when trying to clear old MultiDex extraction, continuing without cleaning.", var24);
                    }
                    
                    // 新建一个存放dex的目录,路径是"/data/user/0/${packageName}/code_cache/secondary-dexes",用来存放优化后的dex文件
                    // 创建 /data/user/0/com.xiaxl.demo/code_cache/secondary-dexes 目录
                    File dexDir = getDexDir(mainContext, dataDir, secondaryFolderName);
                    
                    // 使用MultiDexExtractor这个工具类把APK中的dex抽取到dexDir目录中,返回的files集合有可能为空,表示没有secondaryDex
    	            // 不强制重新加载,也就是说如果已经抽取过了,可以直接从缓存目录中拿来使用,这么做速度比较快
                    // sourceApk: /data/app/com.xiaxl.demo-2/base.apk
                    // dexDir: /data/user/0/com.xiaxl.demo/code_cache/secondary-dexes
                    MultiDexExtractor extractor = new MultiDexExtractor(sourceApk, dexDir);
                    IOException closeException = null;
    
                    try {
    		            // prefsKeyPrefix: ""
    		            // 返回dex文件列表
                        List files = extractor.load(mainContext, prefsKeyPrefix, false);
                        try {
    	                    // 安装secondaryDex
    	                    // /data/user/0/com.xiaxl.demo/code_cache/secondary-dexes
                            installSecondaryDexes(loader, dexDir, files);
                        } catch (IOException var26) {
                            if (!reinstallOnPatchRecoverableException) {
                                throw var26;
                            }
    
                            Log.w("MultiDex", "Failed to install extracted secondary dex files, retrying with forced extraction", var26);
                            files = extractor.load(mainContext, prefsKeyPrefix, true);
                            installSecondaryDexes(loader, dexDir, files);
                        }
                    } finally {
                        try {
                            extractor.close();
                        } catch (IOException var23) {
                            closeException = var23;
                        }
    
                    }
    
                    if (closeException != null) {
                        throw closeException;
                    }
                }
            }
        }
    }
    

    忽略dex文件抽取逻辑和校验逻辑,以上代码中主要做了以下三件事:

    • 清空缓存目录"/data/user/0/${packageName}/files/secondary-dexes"
    • 使用MultiDexExtractor这个工具把APK中的dex抽取到"/data/user/0/${packageName}/code_cache/secondary-dexes"目录
    • 加载"/data/user/0/${packageName}/code_cache/secondary-dexes"目录下的dex

    下边查看MultiDex.installSecondaryDexes方法,了解MultiDex的具体实现

    MultiDex.V4.install(loader, files);

    // dexDir:  /data/user/0/com.xiaxl.demo/code_cache/secondary-dexes
    private static void installSecondaryDexes(ClassLoader loader, File dexDir, List<? extends File> files) throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, InvocationTargetException, NoSuchMethodException, IOException, SecurityException, ClassNotFoundException, InstantiationException {
        if (!files.isEmpty()) {
            if (VERSION.SDK_INT >= 19) {
                MultiDex.V19.install(loader, files, dexDir);
            } else if (VERSION.SDK_INT >= 14) {
                MultiDex.V14.install(loader, files);
            } else {
                MultiDex.V4.install(loader, files);
            }
        }
    }
    

    不同版本的Android系统,类加载机制有一些不同,所以分为了V19、V14和V4三种情况下的安装。

    这里我们看下一V19的源码

    private static final class V19 {
        private V19() {
        }
        // additionalClassPathEntries: dex列表
        // optimizedDirectory: /data/user/0/com.xiaxl.demo/code_cache/secondary-dexes
        static void install(ClassLoader loader, List<? extends File> additionalClassPathEntries, File optimizedDirectory) throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, InvocationTargetException, NoSuchMethodException, IOException {
    	    // 传递的loader是PathClassLoader,findFidld()方法找到父类BaseClassLoader中pathList属性
    	    // 获取BaseDexClassLoader中pathList属性
    	    // this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
            Field pathListField = MultiDex.findField(loader, "pathList");
            Object dexPathList = pathListField.get(loader);
            // 将dex文件添加到DexPathList中的dexElements 数组的末尾
            ArrayList<IOException> suppressedExceptions = new ArrayList();
            MultiDex.expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList, new ArrayList(additionalClassPathEntries), optimizedDirectory, suppressedExceptions));
            // 后面就是添加一些IO异常信息,因为调用DexPathList的makeDexElements会有一些IO操作,相应的可能就会有一些异常情况
            if (suppressedExceptions.size() > 0) {
                Iterator var6 = suppressedExceptions.iterator();
    
                while(var6.hasNext()) {
                    IOException e = (IOException)var6.next();
                    Log.w("MultiDex", "Exception in makeDexElement", e);
                }
    
                Field suppressedExceptionsField = MultiDex.findField(dexPathList, "dexElementsSuppressedExceptions");
                IOException[] dexElementsSuppressedExceptions = (IOException[])((IOException[])suppressedExceptionsField.get(dexPathList));
                if (dexElementsSuppressedExceptions == null) {
                    dexElementsSuppressedExceptions = (IOException[])suppressedExceptions.toArray(new IOException[suppressedExceptions.size()]);
                } else {
                    IOException[] combined = new IOException[suppressedExceptions.size() + dexElementsSuppressedExceptions.length];
                    suppressedExceptions.toArray(combined);
                    System.arraycopy(dexElementsSuppressedExceptions, 0, combined, suppressedExceptions.size(), dexElementsSuppressedExceptions.length);
                    dexElementsSuppressedExceptions = combined;
                }
    
                suppressedExceptionsField.set(dexPathList, dexElementsSuppressedExceptions);
                IOException exception = new IOException("I/O exception during makeDexElement");
                exception.initCause((Throwable)suppressedExceptions.get(0));
                throw exception;
            }
        }
    	
    	// 通过反射的方式调用DexPathList#makeDexElements()方法
    	// dexPathList: DexPathList
    	// files: dex文件列表
        private static Object[] makeDexElements(Object dexPathList, ArrayList<File> files, File optimizedDirectory, ArrayList<IOException> suppressedExceptions) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
    	    // 通过DexPathList的makeDexElements方法加载 “dex文件”
            Method makeDexElements = MultiDex.findMethod(dexPathList, "makeDexElements", ArrayList.class, File.class, ArrayList.class);
            return (Object[])((Object[])makeDexElements.invoke(dexPathList, files, optimizedDirectory, suppressedExceptions));
        }
    }
    

    通过V19的install()方法,关于MultiDex如何加载Dex文件的问题已经清晰:

    • 将APK文件中除主dex文件之外的dex文件追加到PathClassLoader(也就是BaseClassLoader)DexPathListde Element[]数组中。这样在加载一个类的时候就会遍历所有的dex文件,保证了打包的类都能够正常加载。

    Dex的加载到此完成,下边查看Dex的抽取逻辑~~~~~

    2.2、Dex文件的抽取

    前边说过:
    MultiDexExtractor这个工具类的作用是把APK中的dex文件抽取到/data/user/0/com.xiaxl.demo/code_cache/secondary-dexes目录中

    MultiDexExtractor 构造方法

    // sourceApk:  /data/app/com.xiaxl.demo-2/base.apk
    // dexDir:  /data/user/0/com.xiaxl.demo/code_cache/secondary-dexes
    MultiDexExtracto(File sourceApk, File dexDir) throws IOException {
        this.sourceApk = sourceApk;
        this.dexDir = dexDir;
        // 循环冗余校验码(CRC)
        this.sourceCrc = getZipCrc(sourceApk);
        // 创建 /data/user/0/com.xiaxl.demo/code_cache/secondary-dexes/MultiDex.lock
        File lockFile = new File(dexDir, "MultiDex.lock");
        // 对文件内容的访问,既可以读文件也可以写文件,可以访问文件的任意位置适用于由大小已知的记录组成的文件
        // 对/data/user/0/com.xiaxl.demo/code_cache/secondary-dexes/MultiDex.lock 进行读写
        this.lockRaf = new RandomAccessFile(lockFile, "rw");
    
        try {
    	    // 返回文件通道
            this.lockChannel = this.lockRaf.getChannel();
    
            try {
                Log.i("MultiDex", "Blocking on lock " + lockFile.getPath());
                this.cacheLock = this.lockChannel.lock();
            } catch (RuntimeException | Error | IOException var5) {
                closeQuietly(this.lockChannel);
                throw var5;
            }
    
            Log.i("MultiDex", lockFile.getPath() + " locked");
        } catch (RuntimeException | Error | IOException var6) {
            closeQuietly(this.lockRaf);
            throw var6;
        }
    }
    

    MultiDexExtractor.load

    APK中的dex文件的抽取

    // 返回dex文件列表
    // prefsKeyPrefix: ""
    // forceReload: false
    List<? extends File> load(Context context, String prefsKeyPrefix, boolean forceReload) throws IOException {
        // MultiDexExtractor 不可用
        if (!this.cacheLock.isValid()) {
            throw new IllegalStateException("MultiDexExtractor was closed");
        } else {
            List files;
            // forceReload ==false;
            // isModified == true;
            // 如果不需要重新加载并且文件没有被修改过
    	    // isModified()方法是根据SharedPreference中存放的APK文件上一次修改的时间戳和currentCrc来判断是否修改过文件
            if (!forceReload && !isModified(context, this.sourceApk, this.sourceCrc, prefsKeyPrefix)) {
                try {
    	            // 从缓存目录中加载已经抽取过的文件,并返回dex文件列表
                    files = this.loadExistingExtractions(context, prefsKeyPrefix);
                } catch (IOException var6) {
                    Log.w("MultiDex", "Failed to reload existing extracted secondary dex files, falling back to fresh extraction", var6);
                    files = this.performExtractions();
                    putStoredApkInfo(context, prefsKeyPrefix, getTimeStamp(this.sourceApk), this.sourceCrc, files);
                }
            } else {
                if (forceReload) {
                    Log.i("MultiDex", "Forced extraction must be performed.");
                } else {
                    Log.i("MultiDex", "Detected that extraction must be performed.");
                }
                // 如果强制加载或者APK文件已经修改过就重新抽取dex文件
                files = this.performExtractions();
                putStoredApkInfo(context, prefsKeyPrefix, getTimeStamp(this.sourceApk), this.sourceCrc, files);
            }
    
            Log.i("MultiDex", "load found " + files.size() + " secondary dex files");
            return files;
        }
    }
    

    MultiDexExtractor.performExtractions()

    private List<MultiDexExtractor.ExtractedDex> performExtractions() throws IOException {
    	//  抽取出的dex文件名前缀是"base.apk.classes"
        String extractedFilePrefix = this.sourceApk.getName() + ".classes";
        this.clearDexDir();
        // 返回的dex列表
        List<MultiDexExtractor.ExtractedDex> files = new ArrayList();
        // apk压缩包
        ZipFile apk = new ZipFile(this.sourceApk);
    
        try {
            int secondaryNumber = 2;
    
            for(ZipEntry dexFile = apk.getEntry("classes" + secondaryNumber + ".dex"); dexFile != null; dexFile = apk.getEntry("classes" + secondaryNumber + ".dex")) {
    	        // base.apk.classes2.zip
                String fileName = extractedFilePrefix + secondaryNumber + ".zip";
                // 创建文件/data/app/com.xiaxl.demo-2/base.apk.classes2.zip
                MultiDexExtractor.ExtractedDex extractedFile = new MultiDexExtractor.ExtractedDex(this.dexDir, fileName);
                // 添加到文件列表
                files.add(extractedFile);
                Log.i("MultiDex", "Extraction is needed for file " + extractedFile);
                int numAttempts = 0;
                boolean isExtractionSuccessful = false;
                // 抽取dex
                while(numAttempts < 3 && !isExtractionSuccessful) {
                    ++numAttempts;
                    extract(apk, dexFile, extractedFile, extractedFilePrefix);
    
                    try {
                        extractedFile.crc = getZipCrc(extractedFile);
                        isExtractionSuccessful = true;
                    } catch (IOException var18) {
                        isExtractionSuccessful = false;
                        Log.w("MultiDex", "Failed to read crc from " + extractedFile.getAbsolutePath(), var18);
                    }
    
                    Log.i("MultiDex", "Extraction " + (isExtractionSuccessful ? "succeeded" : "failed") + " '" + extractedFile.getAbsolutePath() + "': length " + extractedFile.length() + " - crc: " + extractedFile.crc);
                    if (!isExtractionSuccessful) {
                        extractedFile.delete();
                        if (extractedFile.exists()) {
                            Log.w("MultiDex", "Failed to delete corrupted secondary dex '" + extractedFile.getPath() + "'");
                        }
                    }
                }
    
                if (!isExtractionSuccessful) {
                    throw new IOException("Could not create zip file " + extractedFile.getAbsolutePath() + " for secondary dex (" + secondaryNumber + ")");
                }
    
                ++secondaryNumber;
            }
        } finally {
            try {
                apk.close();
            } catch (IOException var17) {
                Log.w("MultiDex", "Failed to close resource", var17);
            }
    
        }
    
        return files;
    }
    

    2.3、其他相关代码

    clearOldDexDir(Context context)

    private static void clearOldDexDir(Context context) throws Exception {
    	// /data/user/0/com.xiaxl.demo/files/secondary-dexes
        File dexDir = new File(context.getFilesDir(), "secondary-dexes");
        if (dexDir.isDirectory()) {
            Log.i("MultiDex", "Clearing old secondary dex dir (" + dexDir.getPath() + ").");
            // 获取文件列表
            File[] files = dexDir.listFiles();
            // 文件为空
            if (files == null) {
                Log.w("MultiDex", "Failed to list secondary dex dir content (" + dexDir.getPath() + ").");
                return;
            }
            // 文件不为空
            File[] var3 = files;
            int var4 = files.length;
            // 循环清空 /data/user/0/com.xiaxl.demo/files/secondary-dexes 下全部文件
            for(int var5 = 0; var5 < var4; ++var5) {
                File oldFile = var3[var5];
                Log.i("MultiDex", "Trying to delete old file " + oldFile.getPath() + " of size " + oldFile.length());
                if (!oldFile.delete()) {
                    Log.w("MultiDex", "Failed to delete old file " + oldFile.getPath());
                } else {
                    Log.i("MultiDex", "Deleted old file " + oldFile.getPath());
                }
            }
            // 删除 /data/user/0/com.xiaxl.demo/files/secondary-dexes 文件夹
            if (!dexDir.delete()) {
                Log.w("MultiDex", "Failed to delete secondary dex dir " + dexDir.getPath());
            } else {
                Log.i("MultiDex", "Deleted old secondary dex dir " + dexDir.getPath());
            }
        }
    }
    
    private static File getDexDir(Context context, File dataDir, String secondaryFolderName) throws IOException {
    	// 创建 /data/user/0/com.xiaxl.demo/code_cache 目录
        File cache = new File(dataDir, "code_cache");
        try {
            mkdirChecked(cache);
        } catch (IOException var5) {
            cache = new File(context.getFilesDir(), "code_cache");
            mkdirChecked(cache);
        }
        // 创建 /data/user/0/com.xiaxl.demo/code_cache/secondary-dexes 目录
        File dexDir = new File(cache, secondaryFolderName);
        mkdirChecked(dexDir);
        return dexDir;
    }
    

    三、总结

    到这里,MultiDex安装多个dex的原理已经清楚了。

    • 通过一定的方式把dex文件抽取出来;
    • 把这些dex文件追加到DexPathList的Element[]数组的后面
      这个过程要尽可能的早,所以一般是在Application的attachBaseContext()方法中。

    另外,hotfix热修复技术,就是通过一定的方式把修复后的dex插入到DexPathList的Element[]数组前面实现修复后的class抢先加载。

    参考:

    Android源代码

    类加载机制系列3——MultiDex原理解析

    展开全文
  • 动态加载技术(插件化)系列已经坑了有一段时间了,不过UP主我并没有放弃治疗哈,相信在不就的未来就可以看到...他们的核心原理之一都是dex文件的加载。 MultiDex是Google为了解决“65535方法数超标”以及“INSTALL_FA...

    动态加载技术(插件化)系列已经坑了有一段时间了,不过UP主我并没有放弃治疗哈,相信在不就的未来就可以看到“系统Api Hook模式”和插件化框架Frontia的更新了。今天要讲的是动态加载技术的亲戚 —— MultiDex。他们的核心原理之一都是dex文件的加载。

    MultiDex是Google为了解决“65535方法数超标”以及“INSTALL_FAILED_DEXOPT”问题而开发的一个Support库,具体如何使用MultiDex现在市面已经有一大堆教程(可以参考给 App 启用 MultiDex 功能),这里不再赘述。这篇日志主要是配合源码分析MultiDex的工作原理,以及提供一些MultiDex优化的方案。

    Dex的工作机制

    等等,这个章节讲的不是MultiDex吗,怎么变成Dex了?没错哈,没有Dex,哪来的MultiDex。在Android中,对Dex文件操作对应的类叫做DexFile。在CLASSLOADER 的工作机制中,我们说到:

    对于 Java 程序来说,编写程序就是编写类,运行程序也就是运行类(编译得到的class文件),其中起到关键作用的就是类加载器 ClassLoader。

    Android程序的每一个Class都是由ClassLoader#loadClass方法加载进内存的,更准确来说,一个ClassLoader实例会有一个或者多个DexFile实例,调用了ClassLoader#loadClass之后,ClassLoader会通过类名,在自己的DexFile数组里面查找有没有那个DexFile对象里面存在这个类,如果都没有就抛ClassNotFound异常。ClassLoader通过调用DexFile的一个叫defineClass的Native方法去加载指定的类,这点与JVM略有不同,后者是直接调用ClassLoader#defineCLass方法,反正最后实际加载类的方法都叫defineClass就没错了。

    创建DexFile对象

    首先来看看造DexFile对象的构方法。

    
    
    1. public final class DexFile { 
    2.     private int mCookie; 
    3.     private final String mFileName; 
    4.     ... 
    5.  
    6.     public DexFile(File file) throws IOException { 
    7.         this(file.getPath()); 
    8.     } 
    9.     
    10.     public DexFile(String fileName) throws IOException { 
    11.         mCookie = openDexFile(fileName, null, 0); 
    12.         mFileName = fileName; 
    13.         guard.open("close"); 
    14.     } 
    15.     
    16.     private DexFile(String sourceName, String outputName, int flags) throws IOException { 
    17.         mCookie = openDexFile(sourceName, outputName, flags); 
    18.         mFileName = sourceName; 
    19.         guard.open("close"); 
    20.     } 
    21.  
    22.     static public DexFile loadDex(String sourcePathName, String outputPathName, 
    23.         int flags) throws IOException { 
    24.         return new DexFile(sourcePathName, outputPathName, flags); 
    25.     } 
    26.    
    27.     public Class loadClass(String name, ClassLoader loader) { 
    28.         String slashName = name.replace('.''/'); 
    29.         return loadClassBinaryName(slashName, loader); 
    30.     } 
    31.    
    32.     public Class loadClassBinaryName(String name, ClassLoader loader) { 
    33.         return defineClass(name, loader, mCookie); 
    34.     } 
    35.     private native static Class defineClass(String name, ClassLoader loader, int cookie); 
    36.  
    37.     native private static int openDexFile(String sourceName, String outputName, 
    38.         int flags) throws IOException; 
    39.  
    40.     native private static int openDexFile(byte[] fileContents) 
    41.     ... 
    42.  

    通过以前分析过的源码,我们知道ClassLoader主要是通过DexFile.loadDex这个静态方法来创建它需要的DexFile实例的,这里创建DexFile的时候,保存了Dex文件的文件路径mFileName,同时调用了openDexFile的Native方法打开Dex文件并返回了一个mCookie的整型变量(我不知道这个干啥用的,我猜它是一个C++用的资源句柄,用于Native层访问具体的Dex文件)。在Native层的openDexFile方法里,主要做了检查当前创建来的Dex文件是否是有效的Dex文件,还是是一个带有Dex文件的压缩包,还是一个无效的Dex文件。

    加载Dex文件里的类

    加载类的时候,ClassLoader又是通过DexFile#loadClass这个方法来完成的,这个方法里调用了defineClass这个Native方法,看来DexFile才是加载Class的具体API,加载Dex文件和加载具体Class都是通过Native方法完成,ClassLoader有点名不副实啊。

    MultiDex的工作机制

    当一个Dex文件太肥的时候(方法数目太多、文件太大),在打包Apk文件的时候就会出问题,就算打包的时候不出问题,在Android 5.0以下设备上安装或运行Apk也会出问题(具体原因可以参考给 App 启用 MultiDex 功能)。既然一个Dex文件不行的话,那就把这个硕大的Dex文件拆分成若干个小的Dex文件,刚好一个ClassLoader可以有多个DexFile,这就是MultiDex的基本设计思路。

    工作流程

    MultiDex的工作流程具体分为两个部分,一个部分是打包构建Apk的时候,将Dex文件拆分成若干个小的Dex文件,这个Android Studio已经帮我们做了(设置 “multiDexEnabled true”),另一部分就是在启动Apk的时候,同时加载多个Dex文件(具体是加载Dex文件优化后的Odex文件,不过文件名还是.dex),这一部分工作从Android 5.0开始系统已经帮我们做了,但是在Android 5.0以前还是需要通过MultiDex Support库来支持(MultiDex.install(Context))。

    所以我们需要关心的是第二部分,这个过程的简单示意流程图如下。

    (图中红色部分为耗时比较大的地方)

    源码分析

    现在官方已经部署的MultiDex Support版本是com.android.support:multidex:1.0.1,但是现在仓库的master分支已经有了许多新的提交(其中最明显的区别是加入了FileLock来控制多进程同步问题),所以这里分析的源码都是最新的master分支上的。

    MultiDex Support的入口是MultiDex.install(Context),先从这里入手吧。(这次我把具体的分析都写在代码的注释了,这样看是不是更简洁明了些?)

    
    
    1. public static void install(Context context) { 
    2.         Log.i(TAG, "install"); 
    3.          
    4.         // 1. 判读是否需要执行MultiDex。 
    5.         if (IS_VM_MULTIDEX_CAPABLE) { 
    6.             Log.i(TAG, "VM has multidex support, MultiDex support library is disabled."); 
    7.             return
    8.         } 
    9.         if (Build.VERSION.SDK_INT < MIN_SDK_VERSION) { 
    10.             throw new RuntimeException("Multi dex installation failed. SDK " + Build.VERSION.SDK_INT 
    11.                     + " is unsupported. Min SDK version is " + MIN_SDK_VERSION + "."); 
    12.         } 
    13.         try { 
    14.             ApplicationInfo applicationInfo = getApplicationInfo(context); 
    15.             if (applicationInfo == null) { 
    16.                 // Looks like running on a test Context, so just return without patching. 
    17.                 return
    18.             } 
    19.              
    20.             // 2. 如果这个方法已经调用过一次,就不能再调用了。 
    21.             synchronized (installedApk) { 
    22.                 String apkPath = applicationInfo.sourceDir; 
    23.                 if (installedApk.contains(apkPath)) { 
    24.                     return
    25.                 } 
    26.                 installedApk.add(apkPath); 
    27.                  
    28.                 // 3. 如果当前Android版本已经自身支持了MultiDex,依然可以执行MultiDex操作, 
    29.                 // 但是会有警告。 
    30.                 if (Build.VERSION.SDK_INT > MAX_SUPPORTED_SDK_VERSION) { 
    31.                     Log.w(TAG, "MultiDex is not guaranteed to work in SDK version " 
    32.                             + Build.VERSION.SDK_INT + ": SDK version higher than " 
    33.                             + MAX_SUPPORTED_SDK_VERSION + " should be backed by " 
    34.                             + "runtime with built-in multidex capabilty but it's not the " 
    35.                             + "case here: java.vm.version=\"" 
    36.                             + System.getProperty("java.vm.version") + "\""); 
    37.                 } 
    38.          
    39.                 // 4. 获取当前的ClassLoader实例,后面要做的工作,就是把其他dex文件加载后, 
    40.                 // 把其DexFile对象添加到这个ClassLoader实例里就完事了。 
    41.                 ClassLoader loader; 
    42.                 try { 
    43.                     loader = context.getClassLoader(); 
    44.                 } catch (RuntimeException e) { 
    45.                     Log.w(TAG, "Failure while trying to obtain Context class loader. " + 
    46.                             "Must be running in test mode. Skip patching.", e); 
    47.                     return
    48.                 } 
    49.                 if (loader == null) { 
    50.                     Log.e(TAG, 
    51.                             "Context class loader is null. Must be running in test mode. " 
    52.                             + "Skip patching."); 
    53.                     return
    54.                 } 
    55.                 try { 
    56.                   // 5. 清除旧的dex文件,注意这里不是清除上次加载的dex文件缓存。 
    57.                   // 获取dex缓存目录是,会优先获取/data/data/<package>/code-cache作为缓存目录。 
    58.                   // 如果获取失败,则使用/data/data/<package>/files/code-cache目录。 
    59.                   // 这里清除的是第二个目录。 
    60.                   clearOldDexDir(context); 
    61.                 } catch (Throwable t) { 
    62.                   Log.w(TAG, "Something went wrong when trying to clear old MultiDex extraction, " 
    63.                       + "continuing without cleaning.", t); 
    64.                 } 
    65.                  
    66.                 // 6. 获取缓存目录(/data/data/<package>/code-cache)。 
    67.                 File dexDir = getDexDir(context, applicationInfo); 
    68.                  
    69.                 // 7. 加载缓存文件(如果有)。 
    70.                 List<File> files = MultiDexExtractor.load(context, applicationInfo, dexDir, false); 
    71.                  
    72.                 // 8. 检查缓存的dex是否安全 
    73.                 if (checkValidZipFiles(files)) { 
    74.                     // 9. 安装缓存的dex 
    75.                     installSecondaryDexes(loader, dexDir, files); 
    76.                 } else { 
    77.                     // 9. 从apk压缩包里面提取dex文件 
    78.                     Log.w(TAG, "Files were not valid zip files.  Forcing a reload."); 
    79.                     files = MultiDexExtractor.load(context, applicationInfo, dexDir, true); 
    80.                     if (checkValidZipFiles(files)) { 
    81.                         // 10. 安装提取的dex 
    82.                         installSecondaryDexes(loader, dexDir, files); 
    83.                     } else { 
    84.                         throw new RuntimeException("Zip files were not valid."); 
    85.                     } 
    86.                 } 
    87.             } 
    88.         } catch (Exception e) { 
    89.             Log.e(TAG, "Multidex installation failure", e); 
    90.             throw new RuntimeException("Multi dex installation failed (" + e.getMessage() + ")."); 
    91.         } 
    92.         Log.i(TAG, "install done"); 
    93.     }  

    具体代码的分析已经在上面代码的注释里给出了,从这里我们也可以看出,整个MultiDex.install(Context)的过程中,关键的步骤就是MultiDexExtractor#load方法和MultiDex#installSecondaryDexes方法。

    (这部分是题外话)其中有个MultiDex#clearOldDexDir(Context)方法,这个方法的作用是删除/data/data/<package>/files/code-cache,一开始我以为这个方法是删除上一次执行MultiDex后的缓存文件,不过这明显不对,不可能每次MultiDex都重新解压dex文件一边,这样每次启动会很耗时,只有第一次冷启动的时候才需要解压dex文件。后来我又想是不是以前旧版的MultiDex曾经把缓存文件放在这个目录里,现在新版本只是清除以前旧版的遗留文件?但是我找遍了整个MultiDex Repo的提交也没有见过类似的旧版本代码。后面我仔细看MultiDex#getDexDir这个方法才发现,原来MultiDex在获取dex缓存目录是,会优先获取/data/data/<package>/code-cache作为缓存目录,如果获取失败,则使用/data/data/<package>/files/code-cache目录,而后者的缓存文件会在每次App重新启动的时候被清除。感觉MultiDex获取缓存目录的逻辑不是很严谨,而获取缓存目录失败也是MultiDex工作工程中少数有重试机制的地方,看来MultiDex真的是一个临时的兼容方案,Google也许并不打算认真处理这些历史的黑锅。

    接下来再看看MultiDexExtractor#load这个方法。

    
    
    1. static List<File> load(Context context, ApplicationInfo applicationInfo, File dexDir, 
    2.             boolean forceReload) throws IOException { 
    3.         Log.i(TAG, "MultiDexExtractor.load(" + applicationInfo.sourceDir + ", " + forceReload + ")"); 
    4.         final File sourceApk = new File(applicationInfo.sourceDir); 
    5.          
    6.         // 1. 获取当前Apk文件的crc值。 
    7.         long currentCrc = getZipCrc(sourceApk); 
    8.         // Validity check and extraction must be done only while the lock file has been taken. 
    9.         File lockFile = new File(dexDir, LOCK_FILENAME); 
    10.         RandomAccessFile lockRaf = new RandomAccessFile(lockFile, "rw"); 
    11.         FileChannel lockChannel = null
    12.         FileLock cacheLock = null
    13.         List<File> files; 
    14.         IOException releaseLockException = null
    15.         try { 
    16.             lockChannel = lockRaf.getChannel(); 
    17.             Log.i(TAG, "Blocking on lock " + lockFile.getPath()); 
    18.              
    19.             // 2. 加上文件锁,防止多进程冲突。 
    20.             cacheLock = lockChannel.lock(); 
    21.             Log.i(TAG, lockFile.getPath() + " locked"); 
    22.              
    23.             // 3. 先判断是否强制重新解压,这里第一次会优先使用已解压过的dex文件,如果加载失败就强制重新解压。 
    24.             // 此外,通过crc和文件修改时间,判断如果Apk文件已经被修改(覆盖安装),就会跳过缓存重新解压dex文件。 
    25.             if (!forceReload && !isModified(context, sourceApk, currentCrc)) { 
    26.                 try { 
    27.                  
    28.                     // 4. 加载缓存的dex文件 
    29.                     files = loadExistingExtractions(context, sourceApk, dexDir); 
    30.                 } catch (IOException ioe) { 
    31.                     Log.w(TAG, "Failed to reload existing extracted secondary dex files," 
    32.                             + " falling back to fresh extraction", ioe); 
    33.                      
    34.                     // 5. 加载失败的话重新解压,并保存解压出来的dex文件的信息。 
    35.                     files = performExtractions(sourceApk, dexDir); 
    36.                     putStoredApkInfo(context, 
    37.                             getTimeStamp(sourceApk), currentCrc, files.size() + 1); 
    38.                 } 
    39.             } else { 
    40.                 // 4. 重新解压,并保存解压出来的dex文件的信息。 
    41.                 Log.i(TAG, "Detected that extraction must be performed."); 
    42.                 files = performExtractions(sourceApk, dexDir); 
    43.                 putStoredApkInfo(context, getTimeStamp(sourceApk), currentCrc, files.size() + 1); 
    44.             } 
    45.         } finally { 
    46.             if (cacheLock != null) { 
    47.                 try { 
    48.                     cacheLock.release(); 
    49.                 } catch (IOException e) { 
    50.                     Log.e(TAG, "Failed to release lock on " + lockFile.getPath()); 
    51.                     // Exception while releasing the lock is bad, we want to report it, but not at 
    52.                     // the price of overriding any already pending exception. 
    53.                     releaseLockException = e; 
    54.                 } 
    55.             } 
    56.             if (lockChannel != null) { 
    57.                 closeQuietly(lockChannel); 
    58.             } 
    59.             closeQuietly(lockRaf); 
    60.         } 
    61.         if (releaseLockException != null) { 
    62.             throw releaseLockException; 
    63.         } 
    64.         Log.i(TAG, "load found " + files.size() + " secondary dex files"); 
    65.         return files; 
    66.     }  

    这个过程主要是获取可以安装的dex文件列表,可以是上次解压出来的缓存文件,也可以是重新从Apk包里面提取出来的。需要注意的时,如果是重新解压,这里会有明显的耗时,而且解压出来的dex文件,会被压缩成.zip压缩包,压缩的过程也会有明显的耗时(这里压缩dex文件可能是问了节省空间)。

    如果dex文件是重新解压出来的,则会保存dex文件的信息,包括解压的apk文件的crc值、修改时间以及dex文件的数目,以便下一次启动直接使用已经解压过的dex缓存文件,而不是每一次都重新解压。

    需要特别提到的是,里面的FileLock是最新的master分支里面新加进去的功能,现在最新的1.0.1版本里面是没有的。

    无论是通过使用缓存的dex文件,还是重新从apk中解压dex文件,获取dex文件列表后,下一步就是安装(或者说加载)这些dex文件了。最后的工作在MultiDex#installSecondaryDexes这个方法里面。

    
    
    1. private static void installSecondaryDexes(ClassLoader loader, File dexDir, List<File> files) 
    2.             throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, 
    3.             InvocationTargetException, NoSuchMethodException, IOException { 
    4.         if (!files.isEmpty()) { 
    5.             if (Build.VERSION.SDK_INT >= 19) { 
    6.                 V19.install(loader, files, dexDir); 
    7.             } else if (Build.VERSION.SDK_INT >= 14) { 
    8.                 V14.install(loader, files, dexDir); 
    9.             } else { 
    10.                 V4.install(loader, files); 
    11.             } 
    12.         } 
    13.     }  

    因为在不同的SDK版本上,ClassLoader(更准确来说是DexClassLoader)加载dex文件的方式有所不同,所以这里做了V4/V14/V19的兼容(Magic Code)。

    Build.VERSION.SDK_INT < 14

    
    
    1. /** 
    2.      * Installer for platform versions 4 to 13. 
    3.      */ 
    4.     private static final class V4 { 
    5.         private static void install(ClassLoader loader, List<File> additionalClassPathEntries) 
    6.                         throws IllegalArgumentException, IllegalAccessException, 
    7.                         NoSuchFieldException, IOException { 
    8.              
    9.             int extraSize = additionalClassPathEntries.size(); 
    10.             Field pathField = findField(loader, "path"); 
    11.             StringBuilder path = new StringBuilder((String) pathField.get(loader)); 
    12.             String[] extraPaths = new String[extraSize]; 
    13.             File[] extraFiles = new File[extraSize]; 
    14.             ZipFile[] extraZips = new ZipFile[extraSize]; 
    15.             DexFile[] extraDexs = new DexFile[extraSize]; 
    16.             for (ListIterator<File> iterator = additionalClassPathEntries.listIterator(); 
    17.                     iterator.hasNext();) { 
    18.                 File additionalEntry = iterator.next(); 
    19.                 String entryPath = additionalEntry.getAbsolutePath(); 
    20.                 path.append(':').append(entryPath); 
    21.                 int index = iterator.previousIndex(); 
    22.                 extraPaths[index] = entryPath; 
    23.                 extraFiles[index] = additionalEntry; 
    24.                 extraZips[index] = new ZipFile(additionalEntry); 
    25.                 extraDexs[index] = DexFile.loadDex(entryPath, entryPath + ".dex", 0); 
    26.             } 
    27.              
    28.             // 这个版本是最简单的。 
    29.             // 只需要创建DexFile对象后,使用反射的方法分别扩展ClassLoader实例的以下字段即可。 
    30.             pathField.set(loader, path.toString()); 
    31.             expandFieldArray(loader, "mPaths", extraPaths); 
    32.             expandFieldArray(loader, "mFiles", extraFiles); 
    33.             expandFieldArray(loader, "mZips", extraZips); 
    34.             expandFieldArray(loader, "mDexs", extraDexs); 
    35.         } 
    36.     }  

    14 <= Build.VERSION.SDK_INT < 19

    
    
    1. /** 
    2.      * Installer for platform versions 14, 15, 16, 17 and 18. 
    3.      */ 
    4.     private static final class V14 { 
    5.         private static void install(ClassLoader loader, List<File> additionalClassPathEntries, 
    6.                 File optimizedDirectory) 
    7.                         throws IllegalArgumentException, IllegalAccessException, 
    8.                         NoSuchFieldException, InvocationTargetException, NoSuchMethodException { 
    9.              
    10.             // 扩展ClassLoader实例的"pathList"字段。 
    11.             Field pathListField = findField(loader, "pathList"); 
    12.             Object dexPathList = pathListField.get(loader); 
    13.             expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList, 
    14.                     new ArrayList<File>(additionalClassPathEntries), optimizedDirectory)); 
    15.         } 
    16.        
    17.         private static Object[] makeDexElements( 
    18.                 Object dexPathList, ArrayList<File> files, File optimizedDirectory) 
    19.                         throws IllegalAccessException, InvocationTargetException, 
    20.                         NoSuchMethodException { 
    21.             Method makeDexElements = 
    22.                     findMethod(dexPathList, "makeDexElements", ArrayList.class, File.class); 
    23.             return (Object[]) makeDexElements.invoke(dexPathList, files, optimizedDirectory); 
    24.         } 
    25.     }  

    从API14开始,DexClassLoader会使用一个DexpDexPathList类来封装DexFile数组。

    
    
    1. final class DexPathList { 
    2.     private static final String DEX_SUFFIX = ".dex"
    3.     private static final String JAR_SUFFIX = ".jar"
    4.     private static final String ZIP_SUFFIX = ".zip"
    5.     private static final String APK_SUFFIX = ".apk"
    6.    
    7.     private static Element[] makeDexElements(ArrayList<File> files, 
    8.             File optimizedDirectory) { 
    9.         ArrayList<Element> elements = new ArrayList<Element>(); 
    10.         for (File file : files) { 
    11.             ZipFile zip = null
    12.             DexFile dex = null
    13.             String name = file.getName(); 
    14.             if (name.endsWith(DEX_SUFFIX)) { 
    15.                 // Raw dex file (not inside a zip/jar). 
    16.                 try { 
    17.                     dex = loadDexFile(file, optimizedDirectory); 
    18.                 } catch (IOException ex) { 
    19.                     System.logE("Unable to load dex file: " + file, ex); 
    20.                 } 
    21.             } else if (name.endsWith(APK_SUFFIX) || name.endsWith(JAR_SUFFIX) 
    22.                     || name.endsWith(ZIP_SUFFIX)) { 
    23.                 try { 
    24.                     zip = new ZipFile(file); 
    25.                 } catch (IOException ex) { 
    26.                     System.logE("Unable to open zip file: " + file, ex); 
    27.                 } 
    28.                 try { 
    29.                     dex = loadDexFile(file, optimizedDirectory); 
    30.                 } catch (IOException ignored) { 
    31.       
    32.                 } 
    33.             } else { 
    34.                 System.logW("Unknown file type for: " + file); 
    35.             } 
    36.             if ((zip != null) || (dex != null)) { 
    37.                 elements.add(new Element(file, zip, dex)); 
    38.             } 
    39.         } 
    40.         return elements.toArray(new Element[elements.size()]); 
    41.     } 
    42.     
    43.     private static DexFile loadDexFile(File file, File optimizedDirectory) 
    44.             throws IOException { 
    45.         if (optimizedDirectory == null) { 
    46.             return new DexFile(file); 
    47.         } else { 
    48.             String optimizedPath = optimizedPathFor(file, optimizedDirectory); 
    49.             return DexFile.loadDex(file.getPath(), optimizedPath, 0); 
    50.         } 
    51.     } 
    52.  

    通过调用DexPathList#makeDexElements方法,可以加载我们上面解压得到的dex文件,从代码也可以看出,DexPathList#makeDexElements其实也是通过调用DexFile#loadDex来加载dex文件并创建DexFile对象的。V14中,通过反射调用DexPathList#makeDexElements方法加载我们需要的dex文件,在把加载得到的数组扩展到ClassLoader实例的"pathList"字段,从而完成dex文件的安装。

    从DexPathList的代码中我们也可以看出,ClassLoader是支持直接加载.dex/.zip/.jar/.apk的dex文件包的(我记得以前在哪篇日志中好像提到过类似的问题…)。

    19 <= Build.VERSION.SDK_INT

    
    
    1. /** 
    2.     * Installer for platform versions 19. 
    3.     */ 
    4.    private static final class V19 { 
    5.        private static void install(ClassLoader loader, List<File> additionalClassPathEntries, 
    6.                File optimizedDirectory) 
    7.                        throws IllegalArgumentException, IllegalAccessException, 
    8.                        NoSuchFieldException, InvocationTargetException, NoSuchMethodException { 
    9.           
    10.            Field pathListField = findField(loader, "pathList"); 
    11.            Object dexPathList = pathListField.get(loader); 
    12.            ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>(); 
    13.            expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList, 
    14.                    new ArrayList<File>(additionalClassPathEntries), optimizedDirectory, 
    15.                    suppressedExceptions)); 
    16.                     
    17.            if (suppressedExceptions.size() > 0) { 
    18.                for (IOException e : suppressedExceptions) { 
    19.                    Log.w(TAG, "Exception in makeDexElement", e); 
    20.                } 
    21.                Field suppressedExceptionsField = 
    22.                        findField(dexPathList, "dexElementsSuppressedExceptions"); 
    23.                IOException[] dexElementsSuppressedExceptions = 
    24.                        (IOException[]) suppressedExceptionsField.get(dexPathList); 
    25.                if (dexElementsSuppressedExceptions == null) { 
    26.                    dexElementsSuppressedExceptions = 
    27.                            suppressedExceptions.toArray( 
    28.                                    new IOException[suppressedExceptions.size()]); 
    29.                } else { 
    30.                    IOException[] combined = 
    31.                            new IOException[suppressedExceptions.size() + 
    32.                                            dexElementsSuppressedExceptions.length]; 
    33.                    suppressedExceptions.toArray(combined); 
    34.                    System.arraycopy(dexElementsSuppressedExceptions, 0, combined, 
    35.                            suppressedExceptions.size(), dexElementsSuppressedExceptions.length); 
    36.                    dexElementsSuppressedExceptions = combined; 
    37.                } 
    38.                suppressedExceptionsField.set(dexPathList, dexElementsSuppressedExceptions); 
    39.            } 
    40.        } 
    41.     
    42.        private static Object[] makeDexElements( 
    43.                Object dexPathList, ArrayList<File> files, File optimizedDirectory, 
    44.                ArrayList<IOException> suppressedExceptions) 
    45.                        throws IllegalAccessException, InvocationTargetException, 
    46.                        NoSuchMethodException { 
    47.            Method makeDexElements = 
    48.                    findMethod(dexPathList, "makeDexElements", ArrayList.class, File.class, 
    49.                            ArrayList.class); 
    50.            return (Object[]) makeDexElements.invoke(dexPathList, files, optimizedDirectory, 
    51.                    suppressedExceptions); 
    52.        } 
    53.    }  

    V19与V14差别不大,只不过DexPathList#makeDexElements方法多了一个ArrayList<IOException>参数,如果在执行DexPathList#makeDexElements方法的过程中出现异常,后面使用反射的方式把这些异常记录进DexPathList的dexElementsSuppressedExceptions字段里面。

    无论是V4/V14还是V19,在创建DexFile对象的时候,都需要通过DexFile的Native方法openDexFile来打开dex文件,其具体细节暂不讨论(涉及到dex的文件结构,很烦,有兴趣请阅读dalvik_system_DexFile.cpp),这个过程的主要目的是给当前的dex文件做Optimize优化处理并生成相同文件名的odex文件,App实际加载类的时候,都是通过odex文件进行的。因为每个设备对odex格式的要求都不一样,所以这个优化的操作只能放在安装Apk的时候处理,主dex的优化我们已经在安装apk的时候搞定了,其余的dex就是在MultiDex#installSecondaryDexes里面优化的,而后者也是MultiDex过程中,另外一个耗时比较多的操作。(在MultiDex中,提取出来的dex文件被压缩成.zip文件,又优化后的odex文件则被保存为.dex文件。)

    到这里,MultiDex的工作流程就结束了。怎么样,是不是觉得和以前谈到动态加载技术(插件化)的时候说的很像?没错,谁叫它们的核心都是dex文件呢。Java老师第一节课就说“类就是编程”,搞定类你就能搞定整个世界啊!

    优化方案

    MultiDex有个比较蛋疼的问题,就是会产生明显的卡顿现象,通过上面的分析,我们知道具体的卡顿产生在解压dex文件以及优化dex两个步骤。不过好在,在Application#attachBaseContext(Context)中,UI线程的阻塞是不会引发ANR的,只不过这段长时间的卡顿(白屏)还是会影响用户体验。

    目前,优化方案能想到的有两种。

    PreMultiDex方案

    大致思路是,在安装一个新的apk的时候,先在Worker线程里做好MultiDex的解压和Optimize工作,安装apk并启动后,直接使用之前Optimize产生的odex文件,这样就可以避免第一次启动时候的Optimize工作。

    安装dex的时候,核心是创建DexFile对象并使用其Native方法对dex文件进行opt处理,同时生产一个与dex文件(.zip)同名的已经opt过的dex文件(.dex)。如果安装dex的时候,这个opt过的dex文件已经存在,则跳过这个过程,这会节省许多耗时。所以优化的思路就是,下载Apk完成的时候,预先解压dex文件,并预先触发安装dex文件以生产opt过的dex文件。这样覆盖安装Apk并启动的时候,如果MultiDex能命中解压好的dex和odex文件,则能避开耗时最大的两个操作。

    不过这个方案的缺点也是明显的,第一次安装的apk没有作用,而且事先需要使用内置的apk更新功能把新版本的apk文件下载下来后,才能做PreMultiDex工作。

    异步MultiDex方案

    这种方案也是目前比较流行的Dex手动分包方案,启动App的时候,先显示一个简单的Splash闪屏界面,然后启动Worker线程执行MultiDex#install(Context)工作,就可以避免UI线程阻塞。不过要确保启动以及启动MultiDex#install(Context)所需要的类都在主dex里面(手动分包),而且需要处理好进程同步问题。




    作者:Kaede
    来源:51CTO
    展开全文
  • android MultiDex multiDex原理(一)

    万次阅读 2016-04-15 15:25:19
    Android分包MultiDex原理详解 MultiDex的产生背景 当Android系统安装一个应用的时候,有一步是对Dex进行优化,这个过程有一个专门的工具来处理,叫DexOpt。DexOpt的执行过程是在第一次加载Dex文件的时候执行的。这个...
  • MultiDex与热修复实现原理

    千次阅读 2016-06-15 14:12:29
    一、Android的ClassLoader体系 由上图可以看出,在叶子节点上,我们能使用到的是DexClassLoader和PathClassLoader,他们有如下使用场景: ...PathClassLoader是Android应用中的默认加载器,PathClassLoader只能...
  • 自动化打包工具 multidex原理 这是在一个论坛看到的问题,其实你不知道MultiDex到底有多坑。 解决和遇到的其它问题,请见下一篇文章:android MultiDex 原理下超出方法数的限制问题(三) 遭遇MultiDex 愉快...
  • Android Multidex原理及实现

    千次阅读 2017-08-28 22:09:59
    Android Multidex原理及实现一、什么是分包,分包能解决什么问题? 正常apk打包后的文件目录是含有AndroidManifest.xml、R、resource.arcs(资源的索引)、assets、lib、classes.dex这几个模块,而分包后又是怎么样的...
  • Android分包MultiDex原理详解

    万次阅读 2015-09-09 10:32:26
    MultiDex的产生背景当Android系统安装一个应用的时候,有一步是对Dex进行优化,这个过程有一个专门的工具来处理,叫DexOpt。DexOpt的执行过程是在第一次加载Dex文件的时候执行的。这个过程会生成一个ODEX文件,即...
  • Android Support Multidex原理分析

    千次阅读 2016-10-24 15:15:34
    Android Support Multidex原理分析 前言 源码分析 总结
  • Android分包MultiDex原理详解?插件化?自动化?multiDex?是不是觉得已经懵逼了?请先看这篇文章的内容,在下篇文章中将会详解具体的过程… 随着应用不断迭代,业务线的扩展,应用越来越大(比如集成了各种第三方sdk...
1 2 3 4 5 ... 20
收藏数 1,813
精华内容 725
关键字:

multidex原理