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