热修复/热更新
- 一.Android热修复
- 二.热修复框架
- 三.类加载器
- 0.BootClassLoader
- 1.PathClassLoader
- 2.DexClassLoader
- 四.实现思路
- 五.代码
- 1.FixManager
- 2.App
- 3.更加标准的代码
- 五.制作补丁包
- 1.写段有bug的工具类,并写个点击按钮调用
- 2.运行项目到模拟器上
- 3.修复ToastUtils工具类,本地测试没问题,build项目,并将ToastUtils.class文件拷贝出来
- 4.创建dex/com/bawei/myfix文件夹,里面只有修复好的ToastUtils.class文件
- 5.生成dex补丁文件
- 6.将补丁文件放在对应的SD卡目录下进行修复,注意读写SD卡权限
一.Android热修复
热修复,就是对线上版本的静默更新。当APP发布上线之后,如果出现了严重的bug,通常需要重新发版来修复,但是重新走发布流程可能时间比较长,重新安装APP用户体验也不友好,所以出现了热修复,热修复就是通过发布一个插件,使APP运行的时候加载插件里面的代码,从而解决缺陷,并且对用户来说是无感的(有时候可能需要重启一下APP)。
热修复的实现方案,一种是类加载方案,即dex插桩,这种思路在插件化中也会用到;还有一种是底层替换方案,即修改替换ArtMethod。采用类加载方案的主要是以腾讯系为主,包括微信的Tinker、qq空间的QZone、美团的Robust、饿了么的Amigo;采用底层替换方案的主要是阿里系的AndFix等。
热修复包括3部分:开发端、服务端和用户端。在开发端,通过Gradle插件生成补丁包,并上传到云端,客户端通过判断是否需要下载新的补丁包,并执行热修复。
(1)无需重新发版。
(2)快速修复线上bug,修复成功率高,降低损失。
(3)用户无感知修复,无需下载最新的应用,代价小。
二.热修复框架
我们主要使用tinker
三.类加载器
0.BootClassLoader
系统类加载器,当系统启动的时候加载常用类
1.PathClassLoader
加载应用中的类,只能加载已经安装到 Android 系统中的 APK 文件。因此不符合插件化的需求,不作考虑。
2.DexClassLoader
支持加载外部的 APK、Jar 或者 dex 文件,正好符合文件化的需求,所有的插件化方案都是使用 DexClassloader 来加载插件 APK 中的 .class文件的
类加载器的时序图
四.实现思路
五.代码
1.FixManager
public class FixManager {
private Context mContext;
private FixManager(Context context){
mContext = context;
}
private static FixManager manager;
public static FixManager getInstance(Context context){
if(manager == null){
synchronized (FixManager.class){
if(manager == null){
manager = new FixManager(context);
}
}
}
return manager;
}
public void loadFixClass() throws NoSuchFieldException, IllegalAccessException {
//1.反射机制获得补丁包的dexElements:DexClassLoader
//1.0 准备补丁包的路径
String patchPath = mContext.getExternalFilesDir(null).getAbsolutePath()+"/output.dex";//补丁包的SD卡路径,
String cachePatchPath = mContext.getDir("patch",Context.MODE_PRIVATE).getAbsolutePath();//补丁包的缓存路径
//1.1 将补丁到的dex文件加载到虚拟机内存中
DexClassLoader dexClassLoader = new DexClassLoader(patchPath,cachePatchPath,null,mContext.getClassLoader());
Class<?> superclass = dexClassLoader.getClass().getSuperclass();//获得BaseDexClassLoader class对象
Field pathListField = superclass.getDeclaredField("pathList");//获得BaseDexClassLoader类成员属性
pathListField.setAccessible(true);//属性的私有的需要暴力访问
Object pathListObject = pathListField.get(dexClassLoader);//获得dexClassLoader对象的pathList属性值
Field dexElementsField = pathListObject.getClass().getDeclaredField("dexElements");//获得PathList类的成员属性
dexElementsField.setAccessible(true);//属性的私有的需要暴力访问
Object dexElementsObject = dexElementsField.get(pathListObject);//获得pathListObject对象dexElements属性值
//2.反射机制获得宿主app的 dexElements
ClassLoader pathClassLoader = mContext.getClassLoader();//获得PathClassLoader加载器对象
Object myPathListField = pathListField.get(pathClassLoader);//获得PathClassLoader对象的pathList属性值
Object myDexElementsObject = dexElementsField.get(myPathListField);//获得myPathListField对象dexElements属性值
//3.将2个数组合并newDexElements:补丁包在前面,宿主在后面
int fixLength = Array.getLength(dexElementsObject);//补丁包的长度
int myLength = Array.getLength(myDexElementsObject);//宿主数组长度
int newDexElementsLength = fixLength + myLength;//新数组的长度
//新的数组 参数一:数组中元素的类型 参数二:长度
Object newDexElements = Array.newInstance(dexElementsObject.getClass().getComponentType(), newDexElementsLength);
for(int i = 0;i<newDexElementsLength;i++){
if(i<fixLength){//放补丁包
Object o = Array.get(dexElementsObject, i);
Array.set(newDexElements,i,o);
}else{
Object o = Array.get(myDexElementsObject, i - fixLength);
Array.set(newDexElements,i,o);
}
}
//4.反射机制将newDexElements新数组给宿主app放回去
dexElementsField.set(myPathListField,newDexElements);
}
}
2.App
public class App extends Application {
@Override
public void onCreate() {
super.onCreate();
try {
FixManager.getInstance(this).loadFixClass();
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
3.更加标准的代码
package com.bawei.myfix;
import android.content.Context;
import java.io.File;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.util.HashSet;
import dalvik.system.DexClassLoader;
import dalvik.system.PathClassLoader;
/**
* @Author : yaotianxue
* @Time : On 2023/6/8 18:26
* @Description : FixDexUtils
*/
public class FixDexUtils {
private static final String DEX_SUFFIX = ".dex";
private static final String APK_SUFFIX = ".apk";
private static final String JAR_SUFFIX = ".jar";
private static final String ZIP_SUFFIX = ".zip";
private static final HashSet<File> loadedDex = new HashSet<File>();
//加载补丁,使用默认目录:data/data/包
public static void loadFixedDex(Context context) {
loadFixedDex(context, null);
}
//加载补丁包
public static void loadFixedDex(Context context, File patchFilesDir) {
if (context == null) {
return;
}
// 遍历所有的修复dex
File fileDir = patchFilesDir != null ? patchFilesDir : new File(context.getExternalCacheDir().getAbsolutePath()); // data/data/包名/cache(这个可以任意位置)
File[] listFiles = fileDir.listFiles();
for (File file : listFiles) {
if (file.getName().startsWith("classes") &&(file.getName().endsWith(DEX_SUFFIX) || file.getName().endsWith(APK_SUFFIX) || file.getName().endsWith(JAR_SUFFIX) || file.getName().endsWith(ZIP_SUFFIX))) {
loadedDex.add(file);// 存入集合
}
}
// dex合并之前的dex
doDexInject(context);
}
private static void doDexInject(Context appContext) {
String optimizeDir = appContext.getFilesDir().getAbsolutePath();// data/data/包名/files (这个必须是自己程序下的目录)
File fopt = new File(optimizeDir);
if (!fopt.exists()) {
fopt.mkdirs();
}
try {
// 1.加载应用程序的dex
PathClassLoader pathLoader = (PathClassLoader) appContext.getClassLoader();
for (File dex : FixDexUtils.loadedDex) {
// 2.加载指定的修复的dex文件
DexClassLoader dexLoader = new DexClassLoader(dex.getAbsolutePath(), fopt.getAbsolutePath(), null, pathLoader);
// 3.合并
Object dexPathList = getPathList(dexLoader);
Object pathPathList = getPathList(pathLoader);
Object leftDexElements = getDexElements(dexPathList);
Object rightDexElements = getDexElements(pathPathList);
// 合并完成
Object dexElements = combineArray(leftDexElements, rightDexElements);
// 重写给PathList里面的Element[] dexElements;赋值
Object pathList = getPathList(pathLoader);// 一定要重新获取,不要用pathPathList,会报错
setField(pathList, pathList.getClass(), dexElements);
}
} catch (Exception e) {
e.printStackTrace();
}
}
//反射给对象中的属性重新赋值
private static void setField(Object obj, Class<?> cl, Object value) throws NoSuchFieldException, IllegalAccessException {
Field declaredField = cl.getDeclaredField( "dexElements");
declaredField.setAccessible(true);
declaredField.set(obj, value);
}
// 反射得到对象中的属性值
private static Object getField(Object obj, Class<?> cl, String field) throws NoSuchFieldException, IllegalAccessException {
Field localField = cl.getDeclaredField(field);
localField.setAccessible(true);
return localField.get(obj);
}
//反射得到类加载器中的pathList对象
private static Object getPathList(Object baseDexClassLoader) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
return getField(baseDexClassLoader, Class.forName("dalvik.system.BaseDexClassLoader"), "pathList");
}
//反射得到pathList中的dexElements
private static Object getDexElements(Object pathList) throws NoSuchFieldException, IllegalAccessException {
return getField(pathList, pathList.getClass(), "dexElements");
}
//数组合并
private static Object combineArray(Object left, Object right) {
Class<?> componentType = left.getClass().getComponentType();
int i = Array.getLength(left);// 得到左数组长度(补丁数组)
int j = Array.getLength(right);// 得到原dex数组长度
int k = i + j;// 得到总数组长度(补丁数组+原dex数组)
Object result = Array.newInstance( componentType, k);// 创建一个类型为componentType,长度为k的新数组
System.arraycopy(left, 0, result, 0, i);
System.arraycopy(right, 0, result, i, j);
return result;
}
}
五.制作补丁包
1.写段有bug的工具类,并写个点击按钮调用
public class ToastUtils {
public static void toast(){
int a = 1;
int b = 0;
int c = a/b;
}
}
2.运行项目到模拟器上
3.修复ToastUtils工具类,本地测试没问题,build项目,并将ToastUtils.class文件拷贝出来
4.创建dex/com/bawei/myfix文件夹,里面只有修复好的ToastUtils.class文件
5.生成dex补丁文件
(1)Android SDK提供了dx.bat工具将class文件转成dex文件,目录如下:
(2)将第4步骤创建的dex文件夹放在sdk目录下,如上图所示
(3)cmd到SDK的路径,如上图所示
(4)执行命令:将dex文件夹里面的内容打成output.dex
.\dx --dex --output = .\output.dex .\dex