参考:学习视频
课程内容
1. 背景知识
1.1 Class文件与Dex文件
-
根据系统版本的不同,虚拟机分为:Dalvik VM和ART VM。
-
dex文件和dvm或art关系 --> .exe文件和windows系统的关系
-
5.0及以上开始支持多dex,5.0以下还是单dex
-
java文件 经javac —> .class文件 经dx.jar—> .dex文件
左侧的图描述了许多个class文件,一个class文件的作用是记录对应类文件的所有信息,这些信息包含类的常量池、字段信息、方法信息等。
dex是.class文件的集合,由很多个class文件压缩而来,去除很多class文件的冗余信息,是便于移动设备执行。
总结:dex文件格式是专门为Android上虚拟机设计的一种压缩格式,可以简单理解为dex文件是很多class文件处理后的产物。
1.2 方法数超限问题与解决思路
一个apk安装包包含以下一些文件:
原生编译流程默认只会生成一个Dex文件
这些方法包括:开发人员自己编写代码中的方法以及工程中引用第三方库里面所有的代码方法。
解决思路:MultiDex
Google推出的Dex文件支持库,支持在应用程序中使用多个Dex
2. 基本用法
2.1 Android 5.0 及更高版本
2.2 Android 5.0 之前版本
上面是无自定义Application时的使用
上面是有自定义Application时的使用
自定义的Application很可以继承自其他的Application子类,这时候不能破坏原有的继承体系,为了MultiDex的接入,需要在attachBaseContext()方法中手动调用MultiDex.install()方法,如下所示
2.3 产物分析
未使用MuitlDex和使用MuitlDex的区别:
箭头左边为未使用MuitlDex的产物,箭头右边为使用MuitlDex的产物。
3. 原理解析
3.1 编译期原理
3.2 运行期原理(1)-- 代码分析入口及整体流程介绍
3.2 运行期原理(2)-- 第一步:虚拟机判断
从源码中可以看到,首先会取名为"java.vm.version"这个参数,如果这个参数是2.0或者更高版本的话,它会直接跳过install()流程,直接返回改为输出上面的log日志。
Dalvik与ART虚拟机的区别:
Android应用程序都是运行在Android虚拟机上的,在不同的系统版本中有着不同的Android虚拟机实现。 在Android
4.4版本以下采用的是Dalvik虚拟机,Dalvik虚拟机对应的 “java.vm.version” 版本是 2.0.0以下,Dalvik依靠JIT(即时编译)来运作,在运行的时候动态的将执行频率很高的dex字节码翻译成本地的机器码在执行,但是将dex字节码翻译成本地的机器码是发生在应用程序运行过程中,并且应用程序每次重新运行的时候都需要做这个翻译工作,从而使启动速度慢,运行效率比较低、同时也比较耗电。
针对上面的问题,google进行了优化,在Android4.4的时候发布了ART虚拟机,ART采用的是AOT(提前编译)机制。系统在安装应用的时候会采用自带的Dex的AOT工具,把安装包装的所有dex文件进行一次预编译,将字节码预先编译成机器码,生成一个可以在本地机器上能够运行的oat文件,并存储在本地,后续就不需要再编译,运行的效率得到了很大提升。
如果 “java.vm.version” >= 2,说明当前系统采用的是ART虚拟机,已经在系统层面上支持多dex文件的处理;当前应用系统所有的dex文件已经在应用安装过程中被提前合并成一个oat文件,运行时也是运行该oat文件,不需要应用程序自己做处理。
3.3 运行期原理(3)-- 第二步:Dex解压
APK安装包中的class*.dex文件解压到data/data/目录下
3.4 运行期原理(4)-- 第三步:Dex安装
Android的类加载机制:
在虚拟机中编译器生成的class文件都需要类加载器加载到内存中才能被运行。
Android应用程序启动之后,系统默认会创建一个PathClassLoader来进行类的加载工作,PathClassLoader中有个重要成员变量pathList,pathList内部包含一个Element数组(dexElements),数组中的每一个元素都会对应一个DexFile文件,在默认情况下,系统只会加载apk中的第一个DexFile文件,也就是classes.dex文件,所有说Element数组中只会存在一个元素,它对应的就是classes.dex文件;在运行的时候,当需要加载某个类时,PathClassLoader会通过pathList的Element数组从前往后遍历所有元素,看哪个dex文件中有对应的类,如果有找到对应的类就直接返回,这样就完成了类的加载,这就是Android中的类加载器。
Dex安装流程就对应是install()方法,首先通过反射获取PathClassLoader的DexPathList成员变量,然后会向"pathList"成员变量内部的Element数组尾部添加之前解压得到的dex文件列表。
首先通过反射获取到PathClassLoader的pathList字段,然后针对这个pathList字段再次通过反射方式获取到pathList内部的Element数组,接着为前期解压得到的二级dex文件生成对应的新Element数组,最后把新的Element数组中的所有元素追加到PathClassLoader的pathList内部的Element数组尾部,至此安装流程已完成,这样就可以通过刚才的安装把应用程序所需要用到的所有类都注入到PathClassLoader中,后续当需要加载某个类的时候,会在Element数组中从前往后依次遍历所有的元素,依次查找需要的类,确保应用的正常运行。
3.5 运行期原理(5)-- 小结
1)在编译期会通过javac编译所有的源代码文件,即将.java文件编译成.class文件,接着会采用dx工具生成多个dex文件;
2)接着进入运行期,首先会判断当前手机系统的虚拟机版本,如果是ART虚拟机,则说明已经在系统层面支持多dex文件的处理,当前应用的所有dex文件已经在应用安装的时候提前合并成一个oat文件,运行的时候也就是该oat文件,不再需要应用程序自行处理。如果当前使用的是Dalvik虚拟机,那么系统层面不支持MultiDex,需要应用自行MultiDex的安装,这就需要把apk中的二级dex文件解压到应用的特定目录中,得到一个二级dex列表,然后会进行二级dex文件列表注入到ClassLoader的操作。
4. 进阶实践
4.1 代码热修复简介
现在如果发布的apk有bug的时候通常会有如下两种方式进行解决:
1)常规方案—重新发布APK
2)热修复方案
4.2 代码热修复原理
- 生成代码补丁包
生成代码补丁的流程基本遵从Android原生代码编译的流程,首先需要准备好需要动态更新的源代码,比如上图中的ToBeFixed.java,使用javac程序对代码进行编译生成对应的class文件,接着手动调用dx程序,将class文件编译生成为patch.dex文件,此时代码补丁已生成,也就是patch.dex。
- 运行时注入代码补丁包
当应用程序执行到某处需要加载某个类的时候,类加载器会从前往后遍历dexElements数组的所有dex文件直到找到某个类为止,热修复的目标就是让补丁包的类优先可以被系统加载到以达到被修复的目的,因此可以利用这个机制,可以将补丁包插入到dexElements数组的最前面,这个过程称为注入补丁,注入之后就可以达到热修复的效果。
4.3 动手实现代码热修复
- 建立基本工程
原始的项目layout文件配置名为activity_main.xml:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.android.performance.lsn.multidexdemo.MainActivity">
<Button
android:id="@+id/btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="CLICK ME"
android:textSize="32sp"
android:layout_centerInParent="true"/>
<TextView
android:id="@+id/tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/btn"
android:textSize="32sp"
android:layout_centerHorizontal="true"/>
</RelativeLayout>
待修复的类:ToBeFixed.java
package com.android.performance.lsn.multidexdemo;
/**
* 待修复的类
*/
public class ToBeFixed {
/*
* 返回一字符串
*/
public static String get() {
return "Hello, Initial Version";
}
}
主文件:
package com.android.performance.lsn.multidexdemo;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
final TextView tv = (TextView) findViewById(R.id.tv);
final Button btn = (Button) findViewById(R.id.btn);
btn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
String context = ToBeFixed.get();
tv.setText(context);
}
});
}
}
- 生成补丁包
在项目工程根目录下生成一个Directory文件,文件名为to-be-fixed,并且将项目工程中的原有源码对应的包全部拷贝到该to-be-fixed文件中,删除除了ToBeFixed.java之外的源代码,代码结构如下图所示
并修改ToBeFixed.java相关内容如下:
public class ToBeFixed {
public static String get() {
return "Hello, Fixed Version";
}
}
在AS终端Terminal中,执行“cd to-be-fixed”命令,进入到to-be-fixed目录下,之后执行javac multidexdemo/ToBeFixed.java,会在multidexdemo目录下生成ToBeFixed.class文件
之后根据build.gradle配置中的buildToolsVersion的版本,找到对应版本的dx.bat,通过命令生成对应的dex文件
比如把dx.bat文件放到to-be-fixed文件中的multidexdemo目录下,执行下面编译目录:
命令:**/dx.bat --dex --output=./fixed.dex **/ToBeFixed.class
反射工具类
package com.android.performance.lsn.multidexdemo;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
/**
* 反射工具类,用来获取某个字段或者方法
*/
public class ReflectUtil {
/*
* 获取某个字段
*
* @param instance 对象实例
* @param name 字段名称
* @return 对应的字段
*/
public static Field findField(Object instance, String name) throws NoSuchFieldException {
Class clazz = instance.getClass();
while (clazz != null) {
try {
Field field = clazz.getField(name);
if (!field.isAccessible()) {
field.setAccessible(true);
}
return field;
} catch (NoSuchFieldException e) {
clazz = clazz.getSuperclass();
}
}
throw new NoSuchFieldException("No such field: " + name);
}
/*
* 获取某个方法
*
* @param instance 对象实例
* @param name 方法名称
* @param parameterTypes 方法参数类型列表
* @return 对应的方法
*/
public static Method findMethod(Object instance,
String name,
Class<?>... parameterTypes) throws NoSuchMethodException {
Class clazz = instance.getClass();
while (clazz != null) {
try {
Method method = clazz.getDeclaredMethod(name, parameterTypes);
if (!method.isAccessible()) {
method.setAccessible(true);
}
return method;
} catch (NoSuchMethodException e) {
clazz = clazz.getSuperclass();
}
}
throw new NoSuchMethodException("No such method: " + name);
}
}
- 运行时注入补丁包
负责运行时注入补丁包–HotFixManager .java
package com.android.performance.lsn.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.sql.Ref;
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 = ReflectUtil.findField(context.getClassLoader(), "pathList");
Object dexPathList = pathListField.get(context.getClassLoader());
// 获取 DexPathList 中的 makeDexElements 方法(版本不同,方法名称可能不同,方法参数也可能不同)
Method makeDexElements = ReflectUtil.findMethod(
dexPathList,
"makeDexElements",
List.class,
File.class,
List.class,
ClassLoader.class
);
// 把待加载的补丁文件,添加到中
ArrayList<File> filesToBeInstalled = new ArrayList<>();
filesToBeInstalled.add(fixedDexFile);
// 准备 makeDexElements 方法调用所需参数
File optimizedDirectory = new File(context.getFilesDir(), "fixed_dex");
ArrayList<IOException> suppressedException = new ArrayList<>();
// 调用 makeDexElements 方法,得到待修复Dex文件,对应的Elements数组(新)
Object[] extraElements = (Object[]) makeDexElements.invoke(
dexPathList,
filesToBeInstalled,
suppressedException,
context.getClassLoader()
);
// 获取原始的Elements[]
Field dexElementsField = ReflectUtil.findField(dexPathList, "dexElements");
Object[] originElements = (Object[]) dexElementsField.get(dexPathList);
// 创建一个新的Elements[]
Object[] combinedElements = (Object[]) Array.newInstance(
originElements.getClass().getComponentType(),
originElements.length + extraElements.length
);
// 在新的Element数组中,先放 extraElements, 在放 originalElements
// 这是为了确保类查找的时候,先从extraElements 中查找,达到修复的效果
System.arraycopy(extraElements, 0, combinedElements,
0, extraElements.length);
System.arraycopy(originElements, 0, combinedElements,
extraElements.length, originElements.length);
// 将新的 combinedElements, 重新复制给 dexPathList
// 即: 替换原来的 dexElements 数组
dexElementsField.set(dexPathList, combinedElements);
} catch (Exception ex) {
throw new RuntimeException(ex);
}
}
}
因为需要进行类的修复,因此需要确保类被加载之前就已经完成补丁包的注入,完成类的替换
package com.android.performance.lsn.multidexdemo;
import android.app.Application;
import android.content.Context;
public class App extends Application{
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
HotFixManager.installFixedDex(this);
}
}
在编写完App这个类的时候需要在AndroidManifest.xml中进行注册
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.performance.lsn.multidexdemo">
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<application
android:name=".App"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
由于在HotFixManager.java类中需要获取文件的读取权限,因此需要在AndroidManifest.xml和MainActivity.java中申请读写权限。
添加了动态文件读取权限的MainActivity.java
package com.android.performance.lsn.multidexdemo;
import android.Manifest;
import android.content.pm.PackageManager;
import android.os.Build;
import android.support.v4.app.ActivityCompat;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.LOLLIPOP) { // Build.VERSION_CODES.LOLLIPOP = 21
if (ActivityCompat.checkSelfPermission(
this, Manifest.permission.READ_EXTERNAL_STORAGE)
!= PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this,
new String[]{
Manifest.permission.READ_EXTERNAL_STORAGE
}, 1);
}
}
final TextView tv = (TextView) findViewById(R.id.tv);
final Button btn = (Button) findViewById(R.id.btn);
btn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
String context = ToBeFixed.get();
tv.setText(context);
}
});
}
}
5. 高级优化
- MultiDex引起的ANR问题
上图中有两个过程比较耗时: 1)解压过程。 2)dexopt程序的执行。
如果生成的二级dex文件较多,就可能导致解压过程和dexopt的过程耗时过长,一般情况下,这些过程是在主线程中执行的,如果执行的时间超过5s,用户在此期间发生了点击事件没有相应就可能发生ANR问题。
- MultiDex启动优化方案