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的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原理和优化相关推荐
- Android布局原理与优化
Android布局原理与优化 目录: 绘制原理 CPU与GPU Android 图形系统的整体架构 RenderThread 硬件加速和软件绘制 invalidate软件绘制流程 invalidate ...
- android MultiDex multidex原理原理下遇见的N个深坑(二)
android MultiDex 原理下遇见的N个深坑(二) 这是在一个论坛看到的问题,其实你不知道MultiDex到底有多坑. 不了解的可以先看上篇文章:android MultiDex multi ...
- Android MultiDex分析
前言 我先占个坑吧,暂时不想分析了. MultiDex在install的时候,过久会导致ANR-- 这个问题经常见于低端机上. 原理 类加载机制系列3--MultiDex原理解析 Android Mu ...
- Android分包MultiDex原理详解
MultiDex的产生背景 当Android系统安装一个应用的时候,有一步是对Dex进行优化,这个过程有一个专门的工具来处理,叫DexOpt.DexOpt的执行过程是在第一次加载Dex文件的时候执行的 ...
- 一文详解 Android multidex 使用方式及实现原理
在Android中一个Dex文件最多存储65536个方法,也就是一个short类型的范围.但随着应用方法数量的不断增加,当Dex文件突破65536方法数量时,打包时就会抛出异常. 为解决该问题,And ...
- 字节跳动Android三面视频解析:framework+MVP架构+HashMap原理+性能优化+Flutter+源码分析等
前言 对于字节跳动的二面三面而言,Framework+MVP架构+HashMap原理+性能优化+Flutter+源码分析等问题都成高频问点!然而很多的朋友在面试时却答不上或者答不全!今天在这分享下这些 ...
- MultiDex原理分析
MultiDex原理分析 一.MultiDex是什么,解决了什么问题 MultiDex 顾名思义就是对分包的Dex文件进行读取加载到ClassLoader的库 android 早期的版本中,Dex文件 ...
- Android:Socket客户端开发,Android 的Socket客户端优化,Android非UI线程修改控件程序崩溃的问题
一.Android:Socket客户端开发 创建一个工程 我们要做的是按下按键之后,去往服务器 (服务器) 或者我们自己写的服务器 ,给他发送一些预定好的东西 然后打开操作界面 然后修改一下 你要发送 ...
- android blockcanary 原理,blockCanary原理
blockCanary 对于android里面的性能优化,最主要的问题就是UI线程的阻塞导致的,对于如何准确的计算UI的绘制所耗费的时间,是非常有必要的,blockCanary是基于这个需求出现的,同 ...
最新文章
- AnyProxy代理
- J-LINK segger 驱动,MDK5.15版本,用于解决**JLink Warning: Mis-aligned memory write: Address: 0x20000000......
- 【活动】畅想云端加油站,赢iPad
- LockSupport的park和unpark
- 天池 在线编程 最频繁出现的子串(字符串哈希)
- 数据源管理 | PostgreSQL环境整合,JSON类型应用
- 通达oa考勤可以代打吗_可完全免费使用的OA办公系统
- mysql一些常用操作_MySQL常用操作
- java回忆录—输入输出流详细讲解(入门经典)
- python使用-Python 应该怎么去练习和使用?
- cron一点半到两点半之间每分钟_分辨率,定位精度,重复定位精度三者之间有什么关系?...
- leetcode 两个排序的中位数 python
- Quartz.NET开源作业调度框架系列(五):AdoJobStore保存job到数据库
- linux安装snmp显示乱码_Linux安装X Window服务——远程显示GUI
- mysql批量插入大量数据
- 读书笔记(平凡的世界)
- java实现第六届蓝桥杯分机号
- LMS、kalman、RLS的Matlab仿真
- WordCount 官方源码解读及工程代码
- python简单爬虫