原理
先说一下Java的主动引用,主要场景有:
1、遇到new,getstatic,putstatic,invokestatic这4条字节码指令时,类的class如果没有被加载会先被ClassLoader加载到内存,然后才能创建对象,读取或设置静态字段,调用静态方法等。
2、反射,Class.forName()
3、子类初始化前会先初始化父类
4、包含main方法的类,虚拟机启动时会先初始化该类
5、JNI的方式:JNI.findClass()
每个class要使用时,也就是主动引用时会先通过ClassLoader把字节码文件信息加载到内存,Android也是类似,它默认使用dalvik.system.PathClassLoader类来加载class(dex文件)的,Android系统还提供了DexClassLoader类,开发者可以使用它来手动加载class(dex文件),PathClassLoader和DexClassLoader都继承了dalvik.system.BaseDexClassLoader类。
通过查看源码得知,BaseDexClassLoader类在构造方法里会实例化一个成员变量:DexPathList pathList,并将对应的dex文件目录传给pathList成员变量。事实上查找并解析class文件的操作都是在DexPathList类里面进行。DexPathList通过调用makePathElements()方法将dex、zip以及目录文件封装成Element实例,并存放到成员变量DexPathList.Element[] dexElements里。每次BaseDexClassLoader类加载一个class时实际上就是通过调用DexPathList.findClass()方法,遍历成员变量DexPathList.Element[] dexElements,如果找到匹配的class那么就直接返回结果,也就是最先被找到的class会被直接使用,不管后面的dex文件里是否还有相同的class。
举个例子:app.apk里有两个dex文件分别是1.dex和2.dex,这两个dex文件都有一个相同com.app.A的class文件,那么BaseDexClassLoader在加载com.app.A的class时,假设1.dex文件对应的Element实例是成员变量数组dexElements的第一个元素,2.dex对应的Element实例是第二个元素,那么通常情况下2.dex里的com.app.A的class永远都不会被加载到。
因此要实现热修复或更新,我们可以把需要修复或更新的类编写好并编译成.class文件后,使用Android sdk提供的dx工具把这些.class文件打包成.dex文件,然后通过使用DexClassLoader手动加载把.dex文件加载进来,上文已提到BaseDexClassLoader在构造时会实例化一个DexPathList类型的成员变量,而这个成变量初始化时也会把.dex文件封装成Element类型并存入DexPathList.Element[] dexElements数组里,然后我们可以通过反射把这个数组和系统默认使用的PathClassLoader里的dexElements数组进行合并,然后把合并后的新数组重新赋值给系统默认使用的PathClassLoader里的成员变量dexElements,这样就可以实现热修复/更新了。
注意:这里要先把我们手动加载的dex生成的Element数组放到合并后的新数组的最前面!
使用Android提供的dx工具打dex文件的命令:
dx --dex --output out.dex com/log/hotfixjava/Call.class
笔者电脑把dx所在的路径已经添加到系统环境变量,所以可以直接使用:dx命令
Mac电脑下该工具的默认路径:/Users/[用户名]/Library/Android/sdk/build-tools/28.0.2
注意事项
由于程序都是启动后,先去服务器下载补丁dex文件后,再插桩上去,这样会导致如果要修复或更新的类的class文件已经加载到内存了,那么就算重新插桩了dexElements数组,也不会加载到补丁dex文件里的class,因为ClassLoader加载类时都是会先从内存缓存里找,没有找到才会去dex文件里找。所以补丁打完后,需要重新启动app,并且每次启动都需要在Application初始化后重新打补丁。
由于懒得写服务器代码,这里直接把打好的补丁文件放到/assets目录下,模拟从服务器下载dex文件到手机里。
工程目录结构:
package com.log.hotfixjava;
import android.content.Context;
import android.text.TextUtils;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileFilter;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import dalvik.system.BaseDexClassLoader;
import dalvik.system.DexClassLoader;
import dalvik.system.PathClassLoader;
/**
* 热修复管理类
*/
public class DexManager {
public static final String TAG = "DexManager";
/**
* 补丁dex文件存储位置。官方建议放在app私有的目录下。
*
* @param context
* @return
*/
public static String getDexPath(Context context) {
return context.getDir("out", Context.MODE_PRIVATE).getAbsolutePath() + File.separator;
}
/**
* 补丁dex文件解压后的位置
*
* @param context
* @return
*/
public static String getOptimizedDirectory(Context context) {
return context.getDir("dex", Context.MODE_PRIVATE).getAbsolutePath();
}
/**
* 热修复辅助方法
*
* @param context
*/
public static void fix(Context context) {
String dexPath = getDexPath(context);
File dexFile = new File(dexPath);
if (!dexFile.exists()) {
dexFile.mkdir();
}
// 查找存放dex补丁目录下所有的.dex文件
File[] files = dexFile.listFiles(new FileFilter() {
@Override
public boolean accept(File pathname) {
return pathname.getName().lastIndexOf(".dex") != -1;
}
});
for (File file : files) {
fix(context, file.getAbsolutePath(), getOptimizedDirectory(context));
}
}
/**
* 热修复
*
* @param context
* @param dexPath
* @param optimizedDirectory
*/
private static void fix(Context context, String dexPath, String optimizedDirectory) {
if (TextUtils.isEmpty(dexPath) || TextUtils.isEmpty(optimizedDirectory)) {
throw new NullPointerException("The argument dexPath or optimizedDirectory is null");
}
try {
// PathClassLoader的父类BaseDexClassLoader有一个私有的成员变量:DexPathList pathList
// DexPathList类有一个私有的成员变量:Element[] dexElements,这个数组就是用来存放dex和resource文件实例的
PathClassLoader pathClassLoader = (PathClassLoader) context.getClassLoader();
ElementWrapper dexElementsOld = getElements(pathClassLoader);
// DexClassLoader的父类也是BaseDexClassLoader
DexClassLoader dexClassLoader = new DexClassLoader(dexPath, optimizedDirectory, null, context.getClassLoader());
// 获取补丁的dexElements数组实例
ElementWrapper dexElementsNew = getElements(dexClassLoader);
// 通过反射获取两个数组的值,并把补丁的dex文件加到数组的最前面
// 因为BaseDexClassLoader里重写了findClass()方法,实际调用的是DexPathlist的findClass()方法,
// 从源码可以知道,它查找class时会遍历dexElements数组,如果找到对应的class那么就返回,
// 所以要把补丁dex加到数组前面才能保证在加载class时使用的是补丁文件里的class,从而实现热修复、热更新等功能
// 通过java.lang.reflect.Array可以对一个Object实例(实际类型是数组)进行数组的操作
// 因为Element[] dexElements的类型是android系统内部的类,不能显式的强制转换,不然开发工具会报找不到类的错
int oldDexElementsLen = Array.getLength(dexElementsOld.dexElements);
int newDexElementsLen = Array.getLength(dexElementsNew.dexElements);
Object newElements = Array.newInstance(Array.get(dexElementsOld.dexElements, 0).getClass(), oldDexElementsLen + newDexElementsLen);
// 合并新旧两个dexElements数组
// 把补丁的dex文件加载数组的前面
for (int i = 0; i < oldDexElementsLen; i++) {
Array.set(newElements, i, Array.get(dexElementsNew.dexElements, i));
}
// 把旧的dex文件加载到数组的后面
for (int i = 0; i < newDexElementsLen; i++) {
Array.set(newElements, i + oldDexElementsLen, Array.get(dexElementsOld.dexElements, i));
}
// 把合并后的Element数组重新赋值给系统的DexPathList实例的成员变量:dexElements
dexElementsOld.dexElementsField.set(dexElementsOld.dexPathList, newElements);
} catch (Exception e) {
e.printStackTrace();
}
}
private static ElementWrapper getElements(BaseDexClassLoader baseDexClassLoader) {
ElementWrapper elementWrapper = null;
try {
Class<?> baseDexClassLoaderClass = baseDexClassLoader.getClass().getSuperclass();
Field dexPathListField = baseDexClassLoaderClass.getDeclaredField("pathList");
dexPathListField.setAccessible(true);
// DexPathList的实例
Object dexPathList = dexPathListField.get(baseDexClassLoader);
// 获得DexPathList的Class,从而通过反射获得它的成员变量dexElements
Class<?> dexPathListClass = dexPathListField.getType();
Field dexElementsField = dexPathListClass.getDeclaredField("dexElements");
dexElementsField.setAccessible(true);
elementWrapper = new ElementWrapper();
elementWrapper.dexPathList = dexPathList;
elementWrapper.dexElementsField = dexElementsField;
// 获取dexElements数组实例
elementWrapper.dexElements = dexElementsField.get(dexPathList);
} catch (Exception e) {
e.printStackTrace();
}
return elementWrapper;
}
private static class ElementWrapper {
Object dexPathList;
Object dexElements;
Field dexElementsField;
}
/**
* 模拟下载dex文件
*
* @param destPath
*/
public static void downloadFile(InputStream inputStream, String destPath) {
BufferedInputStream bis = null;
BufferedOutputStream bos = null;
try {
File destFile = new File(destPath);
if (destFile.exists()) {
destFile.delete();
}
bis = new BufferedInputStream(inputStream);
bos = new BufferedOutputStream(new FileOutputStream(destFile));
byte[] buf = new byte[1024];
int len;
while ((len = bis.read(buf)) != -1) {
bos.write(buf, 0, len);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (bis != null) {
bis.close();
}
if (bos != null) {
bos.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
需要修复的类:
package com.log.hotfixjava;
public class Call {
public int divide() {
// out.dex文件里把这里修复成:return 100 /10;
return 100 / 0 ;
}
}
Application类:
package com.log.hotfixjava;
import android.app.Application;
import android.content.Context;
public class App extends Application {
// 这个被onCreate方法更早调用
@Override
protected void attachBaseContext(Context base) {
/**
* 每次都需要在Application启动的时候进行打补丁,不然bug依然会存在
*/
DexManager.fix(base);
super.attachBaseContext(base);
}
}
测试的Activity:
package com.log.hotfixjava;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.view.View;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import java.io.IOException;
public class MainActivity extends AppCompatActivity {
private TextView tv_result;
private Handler handler = new Handler() {
@Override
public void handleMessage(@NonNull Message msg) {
Toast.makeText(MainActivity.this, "补丁下载完毕", Toast.LENGTH_SHORT).show();
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
tv_result = findViewById(R.id.tv_result);
}
public void onCall(View view) {
Call call = new Call();
tv_result.setText("" + call.divide());
}
public void onFix(View view) {
DexManager.fix(this);
}
public void onDownload(View view) {
new Thread() {
@Override
public void run() {
try {
String dexPath = DexManager.getDexPath(MainActivity.this) + "out.dex";
DexManager.downloadFile(getAssets().open("out.dex"), dexPath);
handler.sendEmptyMessage(1);
} catch (IOException e) {
e.printStackTrace();
}
}
}.start();
}
}
布局文件:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center_horizontal"
tools:context=".MainActivity">
<Button
android:id="@+id/btn_call"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="调用"
android:onClick="onCall" />
<Button
android:id="@+id/btn_fix"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="修复"
android:layout_toRightOf="@+id/btn_call"
android:onClick="onFix" />
<Button
android:id="@+id/btn_download"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="下载dex文件"
android:layout_toRightOf="@+id/btn_fix"
android:onClick="onDownload" />
<TextView
android:id="@+id/tv_result"
android:layout_below="@+id/btn_fix"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</RelativeLayout>
效果图: