1.前言
查阅整合了一下网上的资料,快速实现一个自定义的无需重启的在JAVA层的热修复。
2.热修复的简单介绍
所谓热修复就是在一些小bug出现后不需要发布新的安装包,直接发布补丁解决问题。JAVA虚拟机JVM在运行时,加载的是.classes的字节码文件。而Android的虚拟机Dalvik/ART虚拟机加载的是dex文件,他们遵循相同的工作原理,都是通过ClassLoader类加载器。android在ClassLoader的基础上又定义类PathClassLoader和DexClassloader,两者都继承自BaseDexClassLoader。
- PathClassLoader主要用于加载系统类和应用类。
- DexClassLoader主要用来加载jar、apk、dex文件。加载jar、apk也是最终抽取里面的Dex文件进行加载
3.一些前提准备工作
因为是在JAVA层实现热修复,基本思路就是自己打出dex文件放置于服务器端,app启动后在首页与服务器交互获得dex文件存入手机内,插入下载的dex文件实现修复。
1>生成dex文件的方法
1.在project目录下进入build\intermediates\javac\debug\compileDebugJavaWithJavac\classes\com下找到class文件。
2.找到Android SDK目录下的build-tools文件目录。
3.打开cmd 进入tools目录 运行命令:
dx --dex --output 需要生成dex的目录和名称 class文件所在的目录
*** 如果报了does not match path 错误。其实路径目录都对。但是编译器就是通不过。这时候只要在--dex 后面加上--no-strict 就可以了。
如下:dx --dex --no-strict --output
2>热修复核心工具类
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 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
doDexInject(context, loadedDex);
}
/**
*@author bthvi
*@time 2018/6/25 0025 15:51
*@desc 验证是否需要热修复
*/
public static boolean isGoingToFix(@NonNull Context context) {
boolean canFix = false;
File externalStorageDirectory = Environment.getExternalStorageDirectory();
// 遍历所有的修复dex , 因为可能是多个dex修复包
File fileDir = externalStorageDirectory != null ?
new File(externalStorageDirectory,"007"):
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("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文件, 需要修复
canFix = true;
}
}
}
return canFix;
}
private static void doDexInject(Context appContext, HashSet 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);
}
Toast.makeText(appContext, "修复完成", Toast.LENGTH_SHORT).show();
} 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;
}
}
4.准备完成,开始正式进行热修复。
1.检查权限,既然涉及文件下载,读写权限必须要给到。方法如下。
//进入启动页检测权限
private boolean checkAndRequestPermission() {
List listPermissionNeeded = new ArrayList<>();
for (String perm : appPermission) {
if (ContextCompat.checkSelfPermission(this, perm) != PackageManager.PERMISSION_GRANTED) {
listPermissionNeeded.add(perm);
}
}
if (!listPermissionNeeded.isEmpty()) {
ActivityCompat.requestPermissions(this, listPermissionNeeded.toArray(new String[listPermissionNeeded.size()]), PERMISSION_REQUEST_CODE);
return false;
}
return true;
}
//给予权限
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
if (requestCode == PERMISSION_REQUEST_CODE) {
HashMap permissionResults = new HashMap<>();
List permissionNeeded = new ArrayList<>();
int deniedCount = 0;
for (int i = 0; i < grantResults.length; i++) {
if (grantResults[i] == PackageManager.PERMISSION_DENIED) {
permissionResults.put(permissions[i], grantResults[i]);
permissionNeeded.add(permissions[i]);
deniedCount++;
}
}
if (deniedCount == 0) {
//在此处下载热修复所需的dex文件
} else {
if (!permissionNeeded.isEmpty()) {
ActivityCompat.requestPermissions(this, permissionNeeded.toArray(new String[permissionNeeded.size()]), PERMISSION_REQUEST_CODE);
}
}
}
}
private static final String savePath = "storage/emulated/0/007/"; //保存到SD卡的路径
public void writeToDisk(final String apkUrl) {
new Thread(new Runnable() {
@Override
public void run() {
try {
URL url = new URL(apkUrl);
HttpURLConnection conn = (HttpURLConnection) url
.openConnection();
conn.setConnectTimeout(10*1000);
conn.setReadTimeout(10*1000);
conn.connect();
InputStream is = conn.getInputStream();
File file = new File(savePath);
if (!file.exists()) {
file.mkdirs();
}
String apkFile = savePath + "classes.dex";
File ApkFile = new File(apkFile);
FileOutputStream fos = new FileOutputStream(ApkFile);
if (is != null) {
byte[] buf = new byte[1024];
int ch;
while ((ch = is.read(buf)) != -1) {
fos.write(buf, 0, ch);//将获取到的流写入文件中
}
}
fos.flush();
fos.close();
byte buf[] = new byte[64];
while (is.read(buf) != -1) {
int numread = is.read(buf);
fos.write(buf, 0, numread);
}
// TODO:在此处通知dex下载成功
fos.close();
is.close();
} catch (Exception e) {
mHandler.sendEmptyMessage(DOWNLOAD_FAILED);
e.printStackTrace();
}
}
}).start();
}
待到用户成功下载后准备工作结束,此时可以调用热修复工具类进行热修复。
private void init() {
File externalStorageDirectory = Environment.getExternalStorageDirectory();
// 遍历所有的修复dex , 因为可能是多个dex修复包
File fileDir = externalStorageDirectory != null ?
new File(externalStorageDirectory, "007") :
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(),mHandler);
}else {
Log.i("loge", "do not have dex");
//失败处理
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
startActivity(new Intent(StartActivity.this, Splash.class));
finish();
}
},500);
}
}
至此大功告成,修复成功。
****注意事项
1.修复内容中如有四大组件需要提前在清单文件注册