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的dexpathlistField 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,重新复制给dexPathListdexElementsField.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就是在这个环节做了手脚:

Android: MultiDex原理和优化相关推荐

  1. Android布局原理与优化

    Android布局原理与优化 目录: 绘制原理 CPU与GPU Android 图形系统的整体架构 RenderThread 硬件加速和软件绘制 invalidate软件绘制流程 invalidate ...

  2. android MultiDex multidex原理原理下遇见的N个深坑(二)

    android MultiDex 原理下遇见的N个深坑(二) 这是在一个论坛看到的问题,其实你不知道MultiDex到底有多坑. 不了解的可以先看上篇文章:android MultiDex multi ...

  3. Android MultiDex分析

    前言 我先占个坑吧,暂时不想分析了. MultiDex在install的时候,过久会导致ANR-- 这个问题经常见于低端机上. 原理 类加载机制系列3--MultiDex原理解析 Android Mu ...

  4. Android分包MultiDex原理详解

    MultiDex的产生背景 当Android系统安装一个应用的时候,有一步是对Dex进行优化,这个过程有一个专门的工具来处理,叫DexOpt.DexOpt的执行过程是在第一次加载Dex文件的时候执行的 ...

  5. 一文详解 Android multidex 使用方式及实现原理

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

  6. 字节跳动Android三面视频解析:framework+MVP架构+HashMap原理+性能优化+Flutter+源码分析等

    前言 对于字节跳动的二面三面而言,Framework+MVP架构+HashMap原理+性能优化+Flutter+源码分析等问题都成高频问点!然而很多的朋友在面试时却答不上或者答不全!今天在这分享下这些 ...

  7. MultiDex原理分析

    MultiDex原理分析 一.MultiDex是什么,解决了什么问题 MultiDex 顾名思义就是对分包的Dex文件进行读取加载到ClassLoader的库 android 早期的版本中,Dex文件 ...

  8. Android:Socket客户端开发,Android 的Socket客户端优化,Android非UI线程修改控件程序崩溃的问题

    一.Android:Socket客户端开发 创建一个工程 我们要做的是按下按键之后,去往服务器 (服务器) 或者我们自己写的服务器 ,给他发送一些预定好的东西 然后打开操作界面 然后修改一下 你要发送 ...

  9. android blockcanary 原理,blockCanary原理

    blockCanary 对于android里面的性能优化,最主要的问题就是UI线程的阻塞导致的,对于如何准确的计算UI的绘制所耗费的时间,是非常有必要的,blockCanary是基于这个需求出现的,同 ...

最新文章

  1. AnyProxy代理
  2. J-LINK segger 驱动,MDK5.15版本,用于解决**JLink Warning: Mis-aligned memory write: Address: 0x20000000......
  3. 【活动】畅想云端加油站,赢iPad
  4. LockSupport的park和unpark
  5. 天池 在线编程 最频繁出现的子串(字符串哈希)
  6. 数据源管理 | PostgreSQL环境整合,JSON类型应用
  7. 通达oa考勤可以代打吗_可完全免费使用的OA办公系统
  8. mysql一些常用操作_MySQL常用操作
  9. java回忆录—输入输出流详细讲解(入门经典)
  10. python使用-Python 应该怎么去练习和使用?
  11. cron一点半到两点半之间每分钟_分辨率,定位精度,重复定位精度三者之间有什么关系?...
  12. leetcode 两个排序的中位数 python
  13. Quartz.NET开源作业调度框架系列(五):AdoJobStore保存job到数据库
  14. linux安装snmp显示乱码_Linux安装X Window服务——远程显示GUI
  15. mysql批量插入大量数据
  16. 读书笔记(平凡的世界)
  17. java实现第六届蓝桥杯分机号
  18. LMS、kalman、RLS的Matlab仿真
  19. WordCount 官方源码解读及工程代码
  20. python简单爬虫

热门文章

  1. Linux常用命令——pmap命令
  2. 中国烟酰胺单核苷酸(NMN)行业研究与投资预测报告(2022版)
  3. CAPL创建与语法简介
  4. 计算机打印途中卡住,联想打印机经典故障处理方法
  5. 011-Java代理模式
  6. NDK篇 - JNI NDK 初探
  7. 【高级篇 / HA】(6.0) ❀ 03. 通过命令切换 HA 主备 ❀ FortiGate 防火墙
  8. Pandas 数据预处理
  9. 论文MICO for MRI bias field estimation and tissue segmentation品讲
  10. eclipse打开时报错: