Android HotPatch在线热补丁方案

本教程采用阿里dexposed开源库实现。
https://github.com/alibaba/dexposed

主APP实现:

主程序Application onCreate方法中初始化dexposed

1
DexposedBridge.canDexposed(context);

Patch apk下载及修复:

  1. 为保证修复patch的及时性,使用push推送patch,客户端收到消息后立即完成patch的下载及修复;
  2. 客户端版本管理模块在程序入口Activity中检测是否有需要修复的patch;
  3. 下载完patch apk到程序私有目录,即/data/data/packageName/files目录,同时可在xml中保存patch apk本地存储路径、方便下载启动app时加载补丁patch。
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
public class HotPatchManager {
public static boolean canDexposed = false;
private static final String SP_KEY_HOT_PATCH = "hot_patch_path";
/**
* init hotPatch library.
*
* @param Context
*/
public static void init(Context context) {
// aop init.
canDexposed = DexposedBridge.canDexposed(context);
if (canDexposed) {
List<String> list = getHotPatchPaths(context);
if (list != null && list.size() > 0) {
for (String path : list) {
runPatchApk(context, path);
}
}
} else {
if (LogUtils.DEBUG) {
LogUtils.d("==========your device not support dexposed aop.==========");
}
}
}
/**
* /data/data/package/files
*
* @param context
* @param apkPath
*/
public static void runPatchApk(Context context, String apkPath) {
if (Build.VERSION.SDK_INT >= 21 || !canDexposed) {
LogUtils.d("This device doesn't support dexposed.");
return;
}
if (!pathIsValid(context, apkPath)) {
return;
}
try {
PatchResult result = PatchMain.load(context, apkPath, null);
if (result.isSuccess()) {
LogUtils.d("hotPath load apk success.");
} else {
LogUtils.e("hotPath load apk error.", result.getErrorInfo());
result.getThrowbale().printStackTrace();
}
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* download hotPatch and auto mege.
*
* @param context
*/
public static void downloadHotPatch(final Context context, String downloadUrl) {
if (TextUtils.isEmpty(downloadUrl)) {
LogUtils.d("downloadUrl is null.");
return;
}
DownloadInfo downloadInfo = new DownloadInfo();
downloadInfo.setDownloadUrl(downloadUrl);
String fileName = downloadUrl.substring(downloadUrl.lastIndexOf("/") + 1);
String fileSavePath = new File(context.getFilesDir(), fileName).getAbsolutePath();
downloadInfo.setFileSavePath(fileSavePath);
downloadInfo.setDaoCallback(
new Task.Callback() {
@Override
public void onSuccess(DownloadInfo downloadInfo) {
LogUtils.d("runPatchApk begin.", downloadInfo.getFileSavePath());
runPatchApk(context, downloadInfo.getFileSavePath());
appendHotPatchPath(context, downloadInfo.getFileSavePath());
LogUtils.d("runPatchApk end.", downloadInfo.getFileSavePath());
}
@Override
public void onStart(DownloadInfo downloadInfo) {
}
@Override
public void onFailure(DownloadInfo downloadInfo) {
}
@Override
public boolean onLoading(long total, long current) {
return true;
}
@Override
public void onCancelled(DownloadInfo downloadInfo) {
}
}
);
DownloadManager dm = DownloadService.getDownloadManager(context, DownloadService.ACTION);
dm.addDownloadTask(downloadInfo);
}
public static void clearHotPatchFiles(Context context) {
List<String> list = getHotPatchPaths(context);
if (list != null && list.size() > 0) {
for (String path : list) {
FileUtils.delFile(path);
}
}
}
public static boolean pathIsValid(Context context, String apkPath) {
if (TextUtils.isEmpty(apkPath)) {
LogUtils.d("apkPath is null.");
return false;
}
String parentDir = String.format("/data/data/%s/files", context.getPackageName());
File apkFile = new File(apkPath);
if (!parentDir.equals(apkFile.getParent())) {
LogUtils.d("apkPath is error.", apkPath);
return false;
}
if (!apkFile.exists()){
LogUtils.d("apkPath is not exist.", apkPath);
return false;
}
return true;
}
public static List<String> getHotPatchPaths(Context context) {
List<String> list = null;
SP sp = SP.getInstance(context);
String paths = sp.getString(SP_KEY_HOT_PATCH, null);
if (!TextUtils.isEmpty(paths)) {
if (paths.indexOf(",") != -1) {
String[] pathArr = paths.split(",");
if (pathArr != null && pathArr.length > 0) {
list = Arrays.asList(paths);
}
} else {
list = new ArrayList<String>();
list.add(paths);
}
}
return list;
}
public static void appendHotPatchPath(Context context, String apkPath) {
if (!pathIsValid(context, apkPath)) {
return;
}
SP sp = SP.getInstance(context);
String paths = sp.getString(SP_KEY_HOT_PATCH, null);
if (!TextUtils.isEmpty(paths)) {
String allPath = new StringBuilder(apkPath).append(",").append(apkPath).toString();
sp.commit(SP_KEY_HOT_PATCH, allPath);
} else {
sp.commit(SP_KEY_HOT_PATCH, apkPath);
}
}
public static void clearHotPatchPaths(Context context) {
SP sp = SP.getInstance(context);
sp.commit(SP_KEY_HOT_PATCH, "");
}
}

Patch Apk部分:

dexpose支持方法粒度的patch,可以实现整个方法的替换或方法前、后执行修复代码。
以下实例为方法替换实例,其它只需实现相应的回调接口即可。

方法替换实例:

  1. 新建Android工程,引入patchloader.jar、dexposedbridge.jar;
  2. 创建Patch修复类实现IPatch接口;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class HotPatch implements IPatch {
@Override
public void handlePatch(final PatchParam arg0) throws Throwable {
Class<?> cls = null;
try {
cls= arg0.context.getClassLoader()
.loadClass("com.zaozuo.app.MainActivity");
} catch (ClassNotFoundException e) {
e.printStackTrace();
return;
}
DexposedBridge.findAndHookMethod(cls, "bindData",
new XC_MethodReplacement() {
@Override
protected Object replaceHookedMethod(MethodHookParam param) throws Throwable {
Activity mainActivity = (Activity) param.thisObject;
Toast.makeText(mainActivity, "test show hotPatch.",Toast.LENGTH_LONG).show();
return null;
}
});
}
}
  1. 打包patch apk,上传到服务器并通知客户端下载。

Patch Apk安全性:

  1. 打包apk必须使用主app签名文件签名;
  2. 主app对加载的patch apk做签名和无篡改校验:
0%