一、什么是热修复?
在我们应用上线后出现bug需要及时修复时,不用再发新的安装包,只需要发布补丁包,在客户无感知下修复掉bug。
实现效果:
Demo源码:
https://gitee.com/sziitjim/hotfix
二、怎么进行热修复?
1.开发端:生成补丁包,开发者把Bug修复后,将修复后的代码打包成.jar或者.dex文件,上传到服务端。
2.服务端:补丁包管理,制作好的补丁包放到服务器进行管理;
3.用户端:执行热修复,APP启动后,通过api查询如果服务端有补丁包,则下载并执行补丁包,进行bug修复,补丁包要在调用bug代码前执行才能修复达到bug效果;
三、制作补丁包流程:
1、把Bug修复掉后,先生成类的class文件。
1.1首先我在代码里面制造了一个bug,程序运行后抛出异常,
1.2修复bug,成类的class文件;
2、执行命令:dx --dex --output=patch.jar com/hotfix/Utils.class ,生成补丁包.jar或者.dex文件,我这里生成.jar文件;
命令目录地址不要输错:
注意:如果提示错误:'dx' 不是内部或外部命令,也不是可运行的程序 或批处理文件。
要配置dex环境变量:
如果提示错误:-Djava.ext.dirs=D:\Android\Sdk\build-tools\30.0.2\lib is not supported. Use -classpath instead.
Error: Could not create the Java Virtual Machine.
Error: A fatal exception has occurred. Program will exit.
则修改:D:\Android\Sdk\build-tools\30.0.2目录下的dx.bat 最后一行
把 call "%java_exe%" %javaOpts% -Djava.ext.dirs="%frameworkdir%" -jar "%jarpath%" %params%
改成 call "%java_exe%" %javaOpts% --class-path="%frameworkdir%" -jar "%jarpath%" %params%
四、使用补丁包:
1.做测试我这边没有使用服务器管理,直接将生成的补丁包复制到手机SDcard目录下,由APP端读取SDcard目录下补丁包patch.jar使用;
2.使用类替换的方式实现热修复,所以不能立即生效,需要重启APP才能生效;
2.1类加载:
由于补丁包的类是我们自己写的,所以是使用当前程序的PathClassLoader去获取补丁包的类数据。
2.2使用类加载器 ClassLoader获取补丁包的Utils类,代替APP原来的Utils,到达修复bug的效果;
核心思想:当程序要使用Utils类的时候,会通过类加载器 ClassLoader去Element数组里面的各个dex文件中查找Utils类,如果查找到了,就不会再查找后面的dex文件,所以我们要想办法,把补丁的dex文件放在Element数组的最前面,ClassLoader找到修复后的Utils类后就会直接返回,达到修复的效果。
热修复流程:
1、获取当前程序的PathClassLoader对象;
2、反射获得PathClassLoader父类BaseDexClassLoader的pathList对象;
3、反射获取pathList的dexElements对象 (oldElement)
4、把补丁包patch.jar转化为Element数组:patchElement(反射执行makePathElements)
5、合并patchElement(放在前面)+oldElement = newElement (Array.newInstance)
6、反射把oldElement赋值成newElement
Android6.0及以下系统代码实现:
// 1、获取程序的PathClassLoader对象
ClassLoader classLoader = application.getClassLoader();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
try {
ClassLoaderInjector.inject(application, classLoader, patchFile);
} catch (Throwable throwable) {
}
return;
}
try {
// 2、反射获得PathClassLoader父类BaseDexClassLoader的pathList对象
Field pathListField = ShareReflectUtil.findField(classLoader, "pathList");
Object pathList = pathListField.get(classLoader);
// 3、反射获取pathList的dexElements对象 (oldElement)
Field dexElementsField = ShareReflectUtil.findField(pathList, "dexElements");
Object[] oldElements = (Object[]) dexElementsField.get(pathList);
// 4、把补丁包变成Element数组:patchElements(反射执行makePathElements)
Object[] patchElements = null;
ArrayList<IOException> ioExceptions = new ArrayList<>();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
Method makePathElementsMethod = ShareReflectUtil.findMethod(pathList, "makePathElements",
List.class, File.class, List.class);
patchElements = (Object[]) makePathElementsMethod.invoke(pathList, patchFile,
application.getCacheDir(), ioExceptions);
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
Method makePathElements = ShareReflectUtil.findMethod(pathList, "makeDexElements",
ArrayList.class, File.class, ArrayList.class);
patchElements = (Object[])
makePathElements.invoke(pathList, patchFile, application.getCacheDir(), ioExceptions);
}
// 5、合并patchElement+oldElement = newElement (Array.newInstance)
Object[] newElements = (Object[]) Array.newInstance(oldElements.getClass().getComponentType(),
oldElements.length + patchElements.length);
Log.i("FixUtil", "installPatch patchElements " + patchElements.length + ",oldElements " + oldElements.length);
// copy patchElements
System.arraycopy(patchElements, 0, newElements, 0, patchElements.length);
// copy oldElements
System.arraycopy(oldElements, 0, newElements, patchElements.length, oldElements.length);
// 6、反射把oldElement赋值成newElement
dexElementsField.set(pathList, newElements);
} catch (Exception e) {
Log.i("FixUtil", "err:" + e.getMessage());
}
Android6.0以上系统代码实现:
public class ClassLoaderInjector {
public static void inject(Application app, ClassLoader oldClassLoader, List<File> patchs) throws Throwable {
Log.i("ClassLoaderInjector", "inject");
//创建我们自己的加载器
ClassLoader newClassLoader
= createNewClassLoader(app, oldClassLoader, patchs);
doInject(app, newClassLoader);
Log.i("ClassLoaderInjector", "inject end");
}
private static ClassLoader createNewClassLoader(Context context, ClassLoader oldClassLoader, List<File> patchs) throws Throwable {
/**
* 1、先把补丁包的dex拼起来
*/
// 获得原始的dexPath用于构造classloader
StringBuilder dexPathBuilder = new StringBuilder();
String packageName = context.getPackageName();
boolean isFirstItem = true;
for (File patch : patchs) {
//添加:分隔符 /xx/a.dex:/xx/b.dex
if (isFirstItem) {
isFirstItem = false;
} else {
dexPathBuilder.append(File.pathSeparator);
}
dexPathBuilder.append(patch.getAbsolutePath());
}
/**
* 2、把apk中的dex拼起来
*/
//得到原本的pathList
Field pathListField = ShareReflectUtil.findField(oldClassLoader, "pathList");
Object oldPathList = pathListField.get(oldClassLoader);
//dexElements
Field dexElementsField = ShareReflectUtil.findField(oldPathList, "dexElements");
Object[] oldDexElements = (Object[]) dexElementsField.get(oldPathList);
//从Element上得到 dexFile
Field dexFileField = ShareReflectUtil.findField(oldDexElements[0], "dexFile");
for (Object oldDexElement : oldDexElements) {
String dexPath = null;
DexFile dexFile = (DexFile) dexFileField.get(oldDexElement);
if (dexFile != null) {
dexPath = dexFile.getName();
}
if (dexPath == null || dexPath.isEmpty()) {
continue;
}
if (!dexPath.contains("/" + packageName)) {
continue;
}
if (isFirstItem) {
isFirstItem = false;
} else {
dexPathBuilder.append(File.pathSeparator);
}
dexPathBuilder.append(dexPath);
}
String combinedDexPath = dexPathBuilder.toString();
/**
* 3、获取apk中的so加载路径
*/
// app的native库(so) 文件目录 用于构造classloader
Field nativeLibraryDirectoriesField = ShareReflectUtil.findField(oldPathList, "nativeLibraryDirectories");
List<File> oldNativeLibraryDirectories = (List<File>) nativeLibraryDirectoriesField.get(oldPathList);
StringBuilder libraryPathBuilder = new StringBuilder();
isFirstItem = true;
for (File libDir : oldNativeLibraryDirectories) {
if (libDir == null) {
continue;
}
if (isFirstItem) {
isFirstItem = false;
} else {
libraryPathBuilder.append(File.pathSeparator);
}
libraryPathBuilder.append(libDir.getAbsolutePath());
}
String combinedLibraryPath = libraryPathBuilder.toString();
//创建自己的类加载器
ClassLoader result = new EnjoyClassLoader(combinedDexPath, combinedLibraryPath, ClassLoader.getSystemClassLoader());
return result;
}
private static void doInject(Application app, ClassLoader classLoader) throws Throwable {
Thread.currentThread().setContextClassLoader(classLoader);
Context baseContext = (Context) ShareReflectUtil.findField(app, "mBase").get(app);
if (Build.VERSION.SDK_INT >= 26) {
ShareReflectUtil.findField(baseContext, "mClassLoader").set(baseContext, classLoader);
}
Object basePackageInfo = ShareReflectUtil.findField(baseContext, "mPackageInfo").get(baseContext);
ShareReflectUtil.findField(basePackageInfo, "mClassLoader").set(basePackageInfo, classLoader);
if (Build.VERSION.SDK_INT < 27) {
Resources res = app.getResources();
try {
ShareReflectUtil.findField(res, "mClassLoader").set(res, classLoader);
final Object drawableInflater = ShareReflectUtil.findField(res, "mDrawableInflater").get(res);
if (drawableInflater != null) {
ShareReflectUtil.findField(drawableInflater, "mClassLoader").set(drawableInflater, classLoader);
}
} catch (Throwable ignored) {
// Ignored.
}
}
}
}
注意:Android不同系统版本中ClassLoader涉及的源码都可能存在差异,目前demo只适配到了Android9,也就是说安装在Android9.0以上的手机上,该热修复未必能生效,每个版本的适配可以参考:腾讯热修复框架tinker的NewClassLoaderInjector.java类
五、如何打包混淆后的补丁包:
由于正式版本的apk我们是经过混淆打包的,所以我们要修复的是混淆后的代码,这边我们可以通过mapping文件, mapping文件存储的是混淆前和混淆后的代码文件对照表。
找到混淆后,Utils 类名 == a;test 方法名 == a;这样我们在编写修复代码时,要改成a类名和a方法再打包成补丁包。
public class a {
public static void a() {
throw new IllegalStateException("this is a bug...");
//Log.i("Utils", "Fix bug.");
}
}
Demo源码: