Android: MultiDex原理和优化

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就是在这个环节做了手脚:

发布了363 篇原创文章 · 获赞 75 · 访问量 16万+

猜你喜欢

转载自blog.csdn.net/qq_39969226/article/details/105167392