首先!我们抛开网上的热修复框架不谈,我们来通过原理手动实现一个热修复工具,在撸码之前我们先通过一张图来了解热修复的流程.
Android热修复
聪明的和不聪明的都已经看出来,Android 在加载dex的时候会遍历一个Element集合,找到class,加载成二进制!(别拍砖!)
如果我们想要实现我们的热修复机制,我们只需要把我们的dex补丁插入到集合的最前面(或者插入到bug class 的前面,这里我就偷个懒嘛,反正老天爷是保佑我的嘛!),当遍历开始找到class的时候就直接return了啊,如果集合后面还有bug的dex或者class都不会被加载了啊!看到这里你是不是明白了!热修复就是这么简单!
首先我们需要以下几个队友的配合!
1.PathClassLoader:这个加载器只能加载已经安装的dex文件
2.DexClassLoader:这个加载器能够加载未安装的dex,但是这个dex文件一定要在使用者的App目录中.(原因自己想!)
3.反射工具Filed
4.Android build-tools工具dx(打包dex用的啊)
5.dalvik.system.BaseDexClassLoader 我是一个字符串,对,就是一个字符串.因为我们要反射这个类里面的信息.
我们先看一下BaseDexClassLoader里面的代码,不用担心就看一个方法
在上一张findClass方法的图
通过看源码你就知道,我上面所说的不是我自己吹牛逼的,也不是忽悠你的!!!
详细的流程:
1.通过PathClassLoader 来加载我们自身App的dex,因为我们要修改自己的bug,而不是隔壁老王的.
2.通过DexClassLoader来加载我们的补丁dex文件,这里面就是没有bug的dex.
3.来!我们先来反射两个classLoader的<DexPathList pathList;>,我们的目的就是拿到这个值.
4.接着我们来继续反射两个classloader中的pathList(注意:是两个!一个是我们自己应用的,另一个是我们补丁的,PathClassLoader和DexClassLoader都继承BaseDexClassLoader),DexPathList里面的<Element[] dexElements;>,没错还是拿到这个数组的值
5.合并两个反射到的Element 数组!这里是重中之重.我们需要把我们的补丁dex放在数组的最前面!
6.将合并的新的数组,通过Field重新设置到我们自身App的DexPathList->dexElements.没错!就是合并之后覆盖有bug那个loader的Element 数组!!
7.通过Android build-tools 中的dx命令打包一个没有bug的dex
注:假设你的App中有一个class A 出bug了,那么你就可以通过dx命令打包一个只有class A的dex文件.
有人说!楼主SB,8步还说全网最简单?呵呵呵呵呵!我只是把代码流程说的详细点而已!不服上代码!只有撸码才是真理!
/**
* Created by 暴走青年 on 2017/1/19.
*/
public class HotFixEngine {
public static final String DEX_OPT_DIR = "optimize_dex";//dex的优化路径
public static final String DEX_BASECLASSLOADER_CLASS_NAME = "dalvik.system.BaseDexClassLoader";
public static final String DEX_FILE_E = "dex";//扩展名
public static final String DEX_ELEMENTS_FIELD = "dexElements";//pathList中的dexElements字段
public static final String DEX_PATHLIST_FIELD = "pathList";//BaseClassLoader中的pathList字段
public static final String FIX_DEX_PATH = "fix_dex";//fixDex存储的路径
/**
* 获得pathList中的dexElements
*
* @param obj
* @return
* @throws NoSuchFieldException
* @throws IllegalAccessException
*/
public Object getDexElements(Object obj) throws NoSuchFieldException, IllegalAccessException {
return getField(obj, obj.getClass(), DEX_ELEMENTS_FIELD);
}
public interface LoadDexFileInterruptCallback {
boolean loadDexFile(File file);
}
/**
* fix
*
* @param context
*/
public void loadDex(Context context, File dexFile) {
if (context == null) {
return;
}
File fixDir = context.getDir(FIX_DEX_PATH, Context.MODE_PRIVATE);
//mrege and fix
mergeDex(context, fixDir,dexFile);
}
/**
* 获取指定classloader 中的pathList字段的值(DexPathList)
*
* @param classLoader
* @return
*/
public Object getDexPathListField(Object classLoader) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
return getField(classLoader, Class.forName(DEX_BASECLASSLOADER_CLASS_NAME), DEX_PATHLIST_FIELD);
}
/**
* 获取一个字段的值
*
* @return
*/
public Object getField(Object obj, Class<?> clz, String fieldName) throws NoSuchFieldException, IllegalAccessException {
Field field = clz.getDeclaredField(fieldName);
field.setAccessible(true);
return field.get(obj);
}
/**
* 为指定对象中的字段重新赋值
*
* @param obj
* @param claz
* @param filed
* @param value
*/
public void setFiledValue(Object obj, Class<?> claz, String filed, Object value) throws NoSuchFieldException, IllegalAccessException {
Field field = claz.getDeclaredField(filed);
field.setAccessible(true);
field.set(obj, value);
// field.setAccessible(false);
}
/**
* 合并dex
*
* @param context
* @param fixDexPath
*/
public void mergeDex(Context context, File fixDexPath, File dexFile) {
try {
//创建dex的optimize路径
File optimizeDir = new File(fixDexPath.getAbsolutePath(), DEX_OPT_DIR);
if (!optimizeDir.exists()) {
optimizeDir.mkdir();
}
//加载自身Apk的dex,通过PathClassLoader
PathClassLoader pathClassLoader = (PathClassLoader) context.getClassLoader();
//找到dex并通过DexClassLoader去加载
//dex文件路径,优化输出路径,null,父加载器
DexClassLoader dexClassLoader = new DexClassLoader(dexFile.getAbsolutePath(), optimizeDir.getAbsolutePath(), null, pathClassLoader);
//获取app自身的BaseDexClassLoader中的pathList字段
Object appDexPathList = getDexPathListField(pathClassLoader);
//获取补丁的BaseDexClassLoader中的pathList字段
Object fixDexPathList = getDexPathListField(dexClassLoader);
Object appDexElements = getDexElements(appDexPathList);
Object fixDexElements = getDexElements(fixDexPathList);
//合并两个elements的数据,将修复的dex插入到数组最前面
Object finalElements = combineArray(fixDexElements, appDexElements);
//给app 中的dex pathList 中的dexElements 重新赋值
setFiledValue(appDexPathList, appDexPathList.getClass(), DEX_ELEMENTS_FIELD, finalElements);
Toast.makeText(context, "修复成功!", Toast.LENGTH_SHORT).show();
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 两个数组合并
*
* @param arrayLhs
* @param arrayRhs
* @return
*/
private static Object combineArray(Object arrayLhs, Object arrayRhs) {
Class<?> localClass = arrayLhs.getClass().getComponentType();
int i = Array.getLength(arrayLhs);
int j = i + Array.getLength(arrayRhs);
Object result = Array.newInstance(localClass, j);
for (int k = 0; k < j; ++k) {
if (k < i) {
Array.set(result, k, Array.get(arrayLhs, k));
} else {
Array.set(result, k, Array.get(arrayRhs, k - i));
}
}
return result;
}
/**
* 复制SD卡中的补丁文件到dex目录
*/
public static void copyDexFileToAppAndFix(Context context, String dexFileName, boolean copyAndFix) {
File path = new File(Environment.getExternalStorageDirectory(), dexFileName);
if (!path.exists()) {
Toast.makeText(context, "没有找到补丁文件", Toast.LENGTH_SHORT).show();
return;
}
if (!path.getAbsolutePath().endsWith(DEX_FILE_E)){
Toast.makeText(context, "补丁文件格式不正确", Toast.LENGTH_SHORT).show();
return;
}
File dexFilePath = context.getDir(FIX_DEX_PATH, Context.MODE_PRIVATE);
File dexFile = new File(dexFilePath, dexFileName);
if (dexFile.exists()) {
dexFile.delete();
}
//copy
InputStream is = null;
FileOutputStream os = null;
try {
is = new FileInputStream(path);
os = new FileOutputStream(dexFile);
int len = 0;
byte[] buffer = new byte[1024];
while ((len = is.read(buffer)) != -1) {
os.write(buffer, 0, len);
}
if (dexFile.exists() && copyAndFix) {
//复制成功,进行修复
new HotFixEngine().loadDex(context, dexFile);
}
path.delete();//删除sdcard中的补丁文件,或者你可以直接下载到app的路径中
is.close();
os.close();
} catch (Exception e) {
e.printStackTrace();
} finally {
if (is != null) {
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (os != null) {
try {
os.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
如果你能看到这里!那么我告诉你!这里才是最重要的!!
代码已经撸完,并且你的App已经上线,那么我在告诉你怎么打包一个dex
dx --dex --output=在这里指定一个dex的输出路径 在这里指定一个class文件的完整路径,从报名开始的完整路径.(懵逼了吗?),你连dex文件打包命令都不会?你是一个假的Android程序员!不!你是一个假的程序员!
例子:
dx.bat --dex --output=D:\AndroidFix\app\src\main\java D:\AndroidFix\app\src\main\java
如果你爆了一个找不到命令的错误怎么办呢?那么请自行解决!!
打包完了!我们来测试一下!写一个带Bug的类!
public class TestClass {
public void showToast(String str,Application context){
Toast.makeText(context,"i am bug!"+1/0,Toast.LENGTH_SHORT).show();
}
}
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
/**
* 关于dex文件被恶意加载和替换的解决方案
* 1.可通过在服务器生成一个dex文件的MD5列表,在修复之前客户端
* 向服务发送验证请求,验证通过即可修复。
* 2.将dex文件打包为rar并且设置密码,在客户端通过ndk进行验证解密
* @param view
*/
public void onClick(View view){
switch (view.getId()){
case R.id.fix:
HotFixEngine.copyDexFileToAppAndFix(this,"classes_fix.dex",true);
break;
case R.id.bug:
new TestClass().showToast(null,getApplication());
break;
}
}
}
啥?你还不明白!??请在看一遍!