很早之前就想深入的研究和学习一下热修复,由于时间的原因一直拖着,现在才执笔弄起来。
Android而更新系列:
Android热更新一:JAVA的类加载机制
Android热更新二:理解Java反射
Android热更新三:Android类加载机制
Android热更新四:热修复机制
Android热更新五:四大热修复方案分析
Android热更新六:Qzone热更新原理
Android热更新七:Tinker热更新原理
Android热更新八:AndFix热更新原理
Android热更新九:Robust热更新原理
Android热更新十:自己写一个Android热修复
经过之前分析了各大热修复的实现原理,参考原理,我们来写一个属于自己的Android热修复吧。
一. 热修复简述。
所谓热修复,就是已经上线APP发现了Bug,不需要花大精力发布新版本,即可通过在线下载补丁并且修复Bug。
热修复的基本原理:
Android框架中存在一个数组,它的作用是维护全部的dex文件(我们写的类的二进制表述方式,用来给安卓虚拟机加载),安卓虚拟机会根据需要从该数组按照自上而下的顺序加载对应的类文件,即使数组中存多个同一个类对应的dex文件,虚拟机一旦找到了对应的dex文件就会停止查找,并加载。根据这个规则,我们只需要把Bug修复涉及到的类文件插入到数组的最前面去,就可以达到修复的目的。
说白了,热修复是利用Android Application的加载dex的规则,从中干预,从而达到修复的目的。
二. 根据原理,我们先来写一个热修复的核心类,
有了上面的原理分析,这个类也肯定不会太复杂,主要用到的是Java的反射以及ClassLoader(DexClassLoader以及PathClassLoader)。
package com.yb.demo.olfix.fixdex;
import java.io.File;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import android.content.Context;
import dalvik.system.DexClassLoader;
import dalvik.system.PathClassLoader;
/**
* 作者:created by yufenfen on 2019/3/21:12:13
* 邮箱: [email protected]
*/
public final class HotFix {
/**
* 修复指定的类
*
* @param context 上下文对象
* @param fixDexFilePath 修复的dex文件路径
*/
public static void fixDexFile(Context context, String fixDexFilePath) {
if (fixDexFilePath != null && new File(fixDexFilePath).exists()) {
try {
injectDexToClassLoader(context, fixDexFilePath);
} catch (Exception e) {
e.printStackTrace();
}
}
}
/**
* @param context
* @param fixDexFilePath 修复文件的路径
* @throws ClassNotFoundException
* @throws NoSuchFieldException
* @throws IllegalAccessException
*/
private static void injectDexToClassLoader(Context context, String fixDexFilePath)
throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
//读取 baseElements
PathClassLoader pathClassLoader = (PathClassLoader) context.getClassLoader();
Object basePathList = getPathList(pathClassLoader);
Object baseElements = getDexElements(basePathList);
//读取 fixElements
String baseDexAbsolutePath = context.getDir("dex", 0).getAbsolutePath();
DexClassLoader fixDexClassLoader = new DexClassLoader(
fixDexFilePath, baseDexAbsolutePath, fixDexFilePath, context.getClassLoader());
Object fixPathList = getPathList(fixDexClassLoader);
Object fixElements = getDexElements(fixPathList);
//合并两份Elements
Object newElements = combineArray(baseElements, fixElements);
//一定要重新获取,不要用basePathList,会报错
Object basePathList2 = getPathList(pathClassLoader);
//新的dexElements对象重新设置回去
setField(basePathList2, basePathList2.getClass(), "dexElements", newElements);
}
/**
* 通过反射先获取到pathList对象
*
* @param obj
* @return
* @throws ClassNotFoundException
* @throws NoSuchFieldException
* @throws IllegalAccessException
*/
private static Object getPathList(Object obj) throws ClassNotFoundException, NoSuchFieldException,
IllegalAccessException {
return getField(obj, Class.forName("dalvik.system.BaseDexClassLoader"), "pathList");
}
/**
* 从上面获取到的PathList对象中,进一步反射获得dexElements对象
*
* @param obj
* @return
* @throws NoSuchFieldException
* @throws IllegalAccessException
*/
private static Object getDexElements(Object obj) throws NoSuchFieldException, IllegalAccessException {
return getField(obj, obj.getClass(), "dexElements");
}
private static Object getField(Object obj, Class cls, String str)
throws NoSuchFieldException, IllegalAccessException {
Field declaredField = cls.getDeclaredField(str);
declaredField.setAccessible(true);//设置为可访问
return declaredField.get(obj);
}
private static void setField(Object obj, Class cls, String str, Object obj2)
throws NoSuchFieldException, IllegalAccessException {
Field declaredField = cls.getDeclaredField(str);
declaredField.setAccessible(true);//设置为可访问
declaredField.set(obj, obj2);
}
/**
* 合拼dexElements ,并确保 fixElements 在 baseElements 之前
*
* @param baseElements
* @param fixElements
* @return
*/
private static Object combineArray(Object baseElements, Object fixElements) {
Class componentType = fixElements.getClass().getComponentType();
int length = Array.getLength(fixElements);
int length2 = Array.getLength(baseElements) + length;
Object newInstance = Array.newInstance(componentType, length2);
for (int i = 0; i < length2; i++) {
if (i < length) {
Array.set(newInstance, i, Array.get(fixElements, i));
} else {
Array.set(newInstance, i, Array.get(baseElements, i - length));
}
}
return newInstance;
}
}
三. 写bug修bug
修复主体类写好了,那么,我们来写个baseAPP,,然后在baseAPP里写一个专门带有bug的类,既然要测试热修复,我们肯定要写一个带有bug的类。
package com.yb.demo.olfix;
import android.app.Activity;
import android.content.Context;
import android.util.Log;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
import java.net.URL;
/**
* 作者:created by yufenfen on 2019/3/27:08:26
* 邮箱: [email protected]
*/
public class FixMe {
private final String TAG = "FixMe";
private ImageView mBelle;
private TextView mNotGril;
private Context mContext;
private MyGlide myGlide;
//false: bug, true: fix
private boolean fix = false;
public FixMe(Activity context) {
mContext = context;
mBelle = (ImageView) context.findViewById(R.id.gril);
mNotGril = (TextView) context.findViewById(R.id.notgril);
myGlide = MyGlide.getInstance(mContext);
}
public void showWhat() {
if (fix) {
fixBug();
Log.d(TAG, "fix bug!");
} else {
mBelle.setVisibility(View.GONE);
mNotGril.setVisibility(View.VISIBLE);
Log.d(TAG, "this is a bug!");
}
}
private void fixBug() {
try {
mBelle.setVisibility(View.VISIBLE);
mNotGril.setVisibility(View.GONE);
URL url = new URL("https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1553252483041&di=3c51ed29d8b2efe3c98dac5168f19e6b&imgtype=0&src=http%3A%2F%2Fpic.feizl.com%2Fupload%2Fallimg%2F171016%2F522zpd0y2srfqa.jpg");
myGlide.loadImageAndAddToTarget(mBelle, url);
} catch (Exception e) {
e.printStackTrace();
}
}
}
然后打包安装到我们的设备上。
接下来,我们把bug修正,也就是在出bug的对应类修复bug,其实就是上面的类的变量赋值为true
//false: bug, true: fix
private boolean fix = true;
四. 打补丁包打补丁
修复好bug后,先不要着急编译运行,我们要先在AndroidStudio里面关闭掉Instant_Run。
由于Android Studio的instan run的原理也是热修复,所以安装的时候不会安装完整的安装包,只会安装新改变的代码。
重新编译,然后就可以打热修复补丁包了,我们这里了非常原始的打补丁包的方式,步骤如下:
1. 拷贝出新修改的类
点击Build->RebuildProject来重新构建,构建完成之后,可以在app/build/interintermediate/debug/包名/找到你刚刚修改的class文件,将他拷贝出来,要连同包名路径一起拷贝出来。
2. 将class文件打包成dex文件
我们前面知道热修复的原理是Dalvik/ART加载dex文件,所以接下来我们要将class文件打包成dex文件,首先我们找到AndroidSDK的build-tools 目录下,在控制台下进入该目录下的任意一个版本,执行dx命令,关于dx命令的使用帮助可以使用dx -- help,下面们通过 dx --dex [指定输出路径]/classes.dex [刚才拷贝的修复bug的类及包名的目录]这样我们就得到了.dex文件。
dx --dex --output=/Users/yufenfen/Desktop/outputdex/classes2.dex /Users/yufenfen/Desktop/outputdex
3. 将补丁包放到目的地
由于实现在线下载补丁文件(classes2.dex)还是比较麻烦一点,我们这里采取简单粗暴的方式,就是手动的把补丁放到以下目录:
Environment.getExternalStorageDirectory()
四. 调用热更新
放好补丁文件后,就可以在页面点击按钮“修复”后进行检查热修复,方式如下
private void checkFix(){
try {
String dexPath = Environment.getExternalStorageDirectory() + "/classes2.dex";
HotFix.fixDexFile(this, dexPath);
Toast.makeText(this, "修复成功", Toast.LENGTH_SHORT).show();
} catch (Exception e) {
Toast.makeText(this, "修复失败" + e.getMessage(), Toast.LENGTH_SHORT).show();
e.printStackTrace();
}
}
OK,造补丁包打补丁就是这么简单粗暴的搞定了,接下来就可以验证是否成功了。
先验证APP是在bug状态的,然后重新启动有bug的APP,点击修复,我们就可以看到美女了。
存在的问题,是如果先点击“来个妞”,再点修复,貌似没有效果,一定要先点修复,再点击“来个妞”才好,原因待确定。
初步估计是相关的类已经被加载,因为根据类加载机制,尽管修复bug了,也就是对相关的类进行了提位操作,而该类已经被加载内存,不会再重新加载,故无效。
有时间找找准确的原因,找到后会更新。