做安卓开发,尽管每个应用上线之前都是经过反复测试验证的,但是还是不免有漏网之bug。出现这种情况我们可以再出一个更新包,让用户下载升级,这是一种处理方法,但是这样的流量成本多了,而且用户体验相当不好,人家明明刚升级,结果没过一会儿又要升级了?所以热修复方法来了

所谓热修复,就是在我们应用上线后出现小bug需要及时修复时,不用再发新的安装包,只需要发布补丁包,在客户不知不觉之间修复掉bug。

热修复工具,主要分为两大流派:

  • 1.以阿里为代表的Native层替换方法表中的方法实现热修复[AndFix ,Sophix等]

  • 2.以腾讯美团为代表的在JAVA层实现热修复[Tinker,Robust等]。

前者不需要重启APP,直接在虚拟机的方法区实现方法替换;后者要实现热修复必须要重启APP。下面记录一种Java层的Android热修复方法(也就是需要重启APP才能实现热修复):

热修复工具类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
package com.york.rexiufu.rexiufu;

import android.content.Context;
import android.os.Environment;
import android.support.annotation.NonNull;
import com.york.rexiufu.utils.LogUtil;

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;

public class FixDexUtil {

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";
public static final String DEX_DIR = "odex";
private static final String OPTIMIZE_DEX_DIR = "optimize_dex";
private static HashSet<File> loadedDex = new HashSet<>();

static {
loadedDex.clear();
}

/**
* 加载补丁,使用默认目录:data/data/包名/files/odex
*
* @param context
*/
public static void loadFixedDex(Context context) {
loadFixedDex(context, null);
}

/**
* 加载补丁
*
* @param context 上下文
* @param patchFilesDir 补丁所在目录
*/
public static void loadFixedDex(Context context, File patchFilesDir) {
// dex合并之前的dex
LogUtil.e("开始修复。。。。。。");
doDexInject(context, loadedDex);
}

/**
*验证是否需要热修复
*/
public static boolean isGoingToFix(@NonNull Context context) {
boolean canFix = false;
File externalStorageDirectory = Environment.getExternalStorageDirectory();

// 遍历所有的修复dex , 因为可能是多个dex修复包
File fileDir = externalStorageDirectory != null ?
new File(externalStorageDirectory, "YorkFix") :
new File(context.getFilesDir(), DEX_DIR);// data/data/包名/files/odex(这个可以任意位置)

File[] listFiles = fileDir.listFiles();
if (listFiles != null) {
for (File file : listFiles) {
if (file.getName().startsWith("my") &&
(file.getName().endsWith(DEX_SUFFIX)
|| file.getName().endsWith(APK_SUFFIX)
|| file.getName().endsWith(JAR_SUFFIX)
|| file.getName().endsWith(ZIP_SUFFIX))) {

loadedDex.add(file);// 存入集合
//有目标dex文件, 需要修复
canFix = true;
}
}
}
return canFix;
}

private static void doDexInject(Context appContext, HashSet<File> loadedDex) {
String optimizeDir = appContext.getFilesDir().getAbsolutePath() +
File.separator + OPTIMIZE_DEX_DIR;
// data/data/包名/files/optimize_dex(这个必须是自己程序下的目录)

File fopt = new File(optimizeDir);
if (!fopt.exists()) {
fopt.mkdirs();
}
try {
// 1.加载应用程序dex的Loader
PathClassLoader pathLoader = (PathClassLoader) appContext.getClassLoader();
for (File dex : loadedDex) {
// 2.加载指定的修复的dex文件的Loader
DexClassLoader dexLoader = new DexClassLoader(
dex.getAbsolutePath(),// 修复好的dex(补丁)所在目录
fopt.getAbsolutePath(),// 存放dex的解压目录(用于jar、zip、apk格式的补丁)
null,// 加载dex时需要的库
pathLoader// 父类加载器
);
// 3.开始合并
// 合并的目标是Element[],重新赋值它的值即可

/**
* BaseDexClassLoader中有 变量: DexPathList pathList
* DexPathList中有 变量 Element[] dexElements
* 依次反射即可
*/

//3.1 准备好pathList的引用
Object dexPathList = getPathList(dexLoader);
Object pathPathList = getPathList(pathLoader);
//3.2 从pathList中反射出element集合
Object leftDexElements = getDexElements(dexPathList);
Object rightDexElements = getDexElements(pathPathList);
//3.3 合并两个dex数组
Object dexElements = combineArray(leftDexElements, rightDexElements);

// 重写给PathList里面的Element[] dexElements;赋值
Object pathList = getPathList(pathLoader);// 一定要重新获取,不要用pathPathList,会报错
setField(pathList, pathList.getClass(), "dexElements", dexElements);
}
LogUtil.e("修复完成");//此处可以把所有的dex文件都删除掉
} catch (Exception e) {
e.printStackTrace();
}
}

/**
* 反射给对象中的属性重新赋值
*/
private static void setField(Object obj, Class<?> cl, String field, Object value) throws NoSuchFieldException, IllegalAccessException {
Field declaredField = cl.getDeclaredField(field);
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 arrayLhs, Object arrayRhs) {
Class<?> clazz = arrayLhs.getClass().getComponentType();
int i = Array.getLength(arrayLhs);// 得到左数组长度(补丁数组)
int j = Array.getLength(arrayRhs);// 得到原dex数组长度
int k = i + j;// 得到总数组长度(补丁数组+原dex数组)
Object result = Array.newInstance(clazz, k);// 创建一个类型为clazz,长度为k的新数组
System.arraycopy(arrayLhs, 0, result, 0, i);
System.arraycopy(arrayRhs, 0, result, i, j);
return result;
}
}


YorkFix是手机上我们存放.dex文件的目录。

使用方法

1.先创建一个测试类,写入测试代码

1
2
3
4
5
6
7
8
9
10
11
package com.york.rexiufu.rexiufu;

import android.content.Context;
import android.widget.Toast;

public class BugClass {
public BugClass(Context context){
Toast.makeText(context,"假装这里有bug!",Toast.LENGTH_SHORT).show();
}
}

2.在MianActivity进行测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.york.rexiufu.activitys;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;

import com.york.rexiufu.R;
import com.york.rexiufu.rexiufu.BugClass;

public class MainActivity extends AppCompatActivity {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

}

public void check(View view) {
new BugClass(MainActivity.this);
}
}

3.运行APP

4.开始修复,修改测试类BugClass

1
2
3
4
5
6
7
8
9
10
11
package com.york.rexiufu.rexiufu;

import android.content.Context;
import android.widget.Toast;

public class BugClass {
public BugClass(Context context){
Toast.makeText(context,"bug 已经修复了!",Toast.LENGTH_SHORT).show();
}
}

5.新增一个 WelcomeActivity,在WelcomeActivity中进行热修复

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
package com.york.rexiufu.activitys;

import android.Manifest;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.widget.Toast;

import com.york.rexiufu.R;
import com.york.rexiufu.rexiufu.FixDexUtil;
import com.york.rexiufu.utils.PermissionUtil;

import java.io.File;

public class WelcomeActivity extends Activity {
private String[] permissions = {Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE};
private PermissionUtil.IPermissionsResult mIPermissionsResult = new PermissionUtil.IPermissionsResult() {
@Override
public void passPermissons() {

}

@Override
public void forbitPermissons() {

}
};
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
PermissionUtil.getInstance().onRequestPermissionsResult(this, requestCode, permissions, grantResults);
}


@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_welcome);
PermissionUtil.getInstance().chekPermissions(this, permissions, mIPermissionsResult);
init();
}

private void init() {
File externalStorageDirectory = Environment.getExternalStorageDirectory();
// 遍历所有的修复dex , 因为可能是多个dex修复包
File fileDir = externalStorageDirectory != null ? new File(externalStorageDirectory,"YorkFix"): new File(getFilesDir(), FixDexUtil.DEX_DIR);// data/user/0/包名/files/odex(这个可以任意位置)
if (!fileDir.exists()){
fileDir.mkdirs();
}
if (FixDexUtil.isGoingToFix(this)) {
FixDexUtil.loadFixedDex(this, Environment.getExternalStorageDirectory());
}
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
startActivity(new Intent(WelcomeActivity.this, MainActivity.class));
finish();
}
},3000);

}
}

6.在AS中找到生成的 BugClass.class 。拷贝它的目录

D:\york_android\RexiufuDemo\app\build\intermediates\javac\debug\compileDebugJavaWithJavac\classes
\com\york\rexiufu\rexiufu\BugClass.class

这里可以把从 com 整个包拷贝出来。

7.使用命令,用SDK中 build tool 里的dx工具,生成 .dex 后缀的修复包。

命令如下:

dx –dex –output=[要生成的.dex文件名称].dex [刚才拷贝的修复bug的类及包名的目录]

演示:

1
dx --dex --output=my.dex com\york\rexiufu\rexiufu\BugClass.class

这样就会的到一个 my.dex 文件

8.拷贝.dex文件到手机目录

9.重新启动APP

这时就可以看到原来的类被替换修复了,修复成功!

补充: 在实际操作中,我们需要用网络把我们的补丁文件.dex下载到用户手机的指定文件夹中,用户在不知不觉中APP就完成了对bug的修复。所以关键我们还是要早发现bug,解决bug,这样才能给到用户有更好的体验。