项目构成
先写一个要修复的类
public class TestCaculate {
public void caculate(Context context) {
int a = 10;
int b = 1;
Toast.makeText(context, "结果" + a / b, Toast.LENGTH_LONG).show();
}
}
这是我们要修复后的结果。我们在build-gradle(module:app)的配置文件最后加上
task makeJar(type: Jar) {
//指定生成的jar名称
baseName 'log'
//从哪里打包class文件
//from('build/intermediates/classes/debug/com/example/bugfix/')
from('build\\intermediates\\javac\\debug\\compileDebugJavaWithJavac\\classes\\com\\example\\bugfix')
//打包到jar后的目录结构
into('com/example/bugfix/')
//去掉不需要打包的目录和文件
exclude('text/', 'BuildConfig.class', 'R.class', 'BuildConfig.class')
exclude {
it.name.startsWith('R$')
}
}
makeJar.dependsOn(clearJar, build)
注意,要打包class的路径可能随as的版本变化而变化,我们可以在app\build\intermediates
路径下面搜索.class
文件以确定文件夹路径。例如此处我找到的存放class文件的路径是
****\BugFix\app\build\intermediates\javac\debug\compileDebugJavaWithJavac\classes\com\example\bugfix
布局文件我们设置两个按钮,一个用于修复,一个用于显示calc的结果。
然后mainActivity的逻辑如下
public class MainActivity extends AppCompatActivity {
public static final int REQUEST_CODE = 1;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button btn = findViewById(R.id.button_calc);
Button btn_fix = findViewById(R.id.button_fix);
btn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
TestCaculate testCaculate = new TestCaculate();
testCaculate.caculate(MainActivity.this);
}
});
btn_fix.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
fix();
}
});
}
private void fix() {
try {
String dexPath = Environment.getExternalStorageDirectory() + "/happyclass.dex";
HotFixUtil.patch(this, dexPath, "com.example.bugfix.TestCaculate");
Toast.makeText(this, "修复成功", Toast.LENGTH_LONG).show();
} catch (Exception e) {
Toast.makeText(this, "修复失败" + e.getMessage(), Toast.LENGTH_LONG).show();
e.printStackTrace();
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
switch (requestCode) {
case REQUEST_CODE: {
if (grantResults.length > 0) {
// permission was granted
Toast.makeText(this, "Thanks for grant the permission!" , Toast.LENGTH_LONG).show();
} else {
// permission denied
Toast.makeText(this, "Ops, you deny the permission!" , Toast.LENGTH_LONG).show();
}
return;
}
}
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
}
然后我们Build->Rebuild Project
HotFix核心代码
添加一个类HotFixUtil
package com.example.bugfix;
import android.annotation.TargetApi;
import android.content.Context;
import android.widget.Toast;
import java.io.File;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import dalvik.system.DexClassLoader;
import dalvik.system.PathClassLoader;
public final class HotFixUtil {
/**
* 修复指定的类
*
* @param context 上下文对象
* @param patchDexFile dex文件
* @param patchClassName 被修复类名
*/
public static void patch(Context context, String patchDexFile, String patchClassName) {
if (patchDexFile != null && new File(patchDexFile).exists()) {
try {
if (hasLexClassLoader()) {
injectInAliyunOs(context, patchDexFile, patchClassName);
} else if (hasDexClassLoader()) {
injectAboveEqualApiLevel14(context, patchDexFile, patchClassName);
} else {
injectBelowApiLevel14(context, patchDexFile, patchClassName);
}
} catch (Throwable th) {
}
}
}
private static boolean hasLexClassLoader() {
try {
Class.forName("dalvik.system.LexClassLoader");
return true;
} catch (ClassNotFoundException e) {
return false;
}
}
private static boolean hasDexClassLoader() {
try {
Class.forName("dalvik.system.BaseDexClassLoader");
return true;
} catch (ClassNotFoundException e) {
return false;
}
}
private static void injectInAliyunOs(Context context, String patchDexFile, String patchClassName)
throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException,
InstantiationException, NoSuchFieldException {
PathClassLoader obj = (PathClassLoader) context.getClassLoader();
String replaceAll = new File(patchDexFile).getName().replaceAll("\\.[a-zA-Z0-9]+", ".lex");
Class cls = Class.forName("dalvik.system.LexClassLoader");
Object newInstance =
cls.getConstructor(new Class[]{String.class, String.class, String.class, ClassLoader.class}).newInstance(
new Object[]{context.getDir("dex", 0).getAbsolutePath() + File.separator + replaceAll,
context.getDir("dex", 0).getAbsolutePath(), patchDexFile, obj});
cls.getMethod("loadClass", new Class[]{String.class}).invoke(newInstance, new Object[]{patchClassName});
setField(obj, PathClassLoader.class, "mPaths",
appendArray(getField(obj, PathClassLoader.class, "mPaths"), getField(newInstance, cls, "mRawDexPath")));
setField(obj, PathClassLoader.class, "mFiles",
combineArray(getField(obj, PathClassLoader.class, "mFiles"), getField(newInstance, cls, "mFiles")));
setField(obj, PathClassLoader.class, "mZips",
combineArray(getField(obj, PathClassLoader.class, "mZips"), getField(newInstance, cls, "mZips")));
setField(obj, PathClassLoader.class, "mLexs",
combineArray(getField(obj, PathClassLoader.class, "mLexs"), getField(newInstance, cls, "mDexs")));
}
@TargetApi(14)
private static void injectBelowApiLevel14(Context context, String str, String str2)
throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
PathClassLoader obj = (PathClassLoader) context.getClassLoader();
DexClassLoader dexClassLoader =
new DexClassLoader(str, context.getDir("dex", 0).getAbsolutePath(), str, context.getClassLoader());
dexClassLoader.loadClass(str2);
setField(obj, PathClassLoader.class, "mPaths",
appendArray(getField(obj, PathClassLoader.class, "mPaths"), getField(dexClassLoader, DexClassLoader.class,
"mRawDexPath")
));
setField(obj, PathClassLoader.class, "mFiles",
combineArray(getField(obj, PathClassLoader.class, "mFiles"), getField(dexClassLoader, DexClassLoader.class,
"mFiles")
));
setField(obj, PathClassLoader.class, "mZips",
combineArray(getField(obj, PathClassLoader.class, "mZips"), getField(dexClassLoader, DexClassLoader.class,
"mZips")));
setField(obj, PathClassLoader.class, "mDexs",
combineArray(getField(obj, PathClassLoader.class, "mDexs"), getField(dexClassLoader, DexClassLoader.class,
"mDexs")));
obj.loadClass(str2);
}
private static void injectAboveEqualApiLevel14(Context context, String str, String str2)
throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
PathClassLoader pathClassLoader = (PathClassLoader) context.getClassLoader();
Object a = combineArray(getDexElements(getPathList(pathClassLoader)),
getDexElements(getPathList(
new DexClassLoader(str, context.getDir("dex", 0).getAbsolutePath(), str, context.getClassLoader()))));
Object a2 = getPathList(pathClassLoader);
//新的dexElements对象重新设置回去
setField(a2, a2.getClass(), "dexElements", a);
pathClassLoader.loadClass(str2);
}
/**
* 通过反射先获取到pathList对象
*
* @param obj
* @return
* @throws ClassNotFoundException
* @throws NoSuchFieldException
* @throws IllegalAccessException
*/
private static Object getPathList(Object obj) throws ClassNotFoundException, NoSuchFieldException,
IllegalAccessException {
return getField(obj, Class.forName("dalvik.system.BaseDexClassLoader"), "pathList");
}
/**
* 从上面获取到的PathList对象中,进一步反射获得dexElements对象
*
* @param obj
* @return
* @throws NoSuchFieldException
* @throws IllegalAccessException
*/
private static Object getDexElements(Object obj) throws NoSuchFieldException, IllegalAccessException {
return getField(obj, obj.getClass(), "dexElements");
}
private static Object getField(Object obj, Class cls, String str)
throws NoSuchFieldException, IllegalAccessException {
Field declaredField = cls.getDeclaredField(str);
declaredField.setAccessible(true);//设置为可访问
return declaredField.get(obj);
}
private static void setField(Object obj, Class cls, String str, Object obj2)
throws NoSuchFieldException, IllegalAccessException {
Field declaredField = cls.getDeclaredField(str);
declaredField.setAccessible(true);//设置为可访问
declaredField.set(obj, obj2);
}
//合拼dexElements
private static Object combineArray(Object obj, Object obj2) {
Class componentType = obj2.getClass().getComponentType();
int length = Array.getLength(obj2);
int length2 = Array.getLength(obj) + length;
Object newInstance = Array.newInstance(componentType, length2);
for (int i = 0; i < length2; i++) {
if (i < length) {
Array.set(newInstance, i, Array.get(obj2, i));
} else {
Array.set(newInstance, i, Array.get(obj, i - length));
}
}
return newInstance;
}
private static Object appendArray(Object obj, Object obj2) {
Class componentType = obj.getClass().getComponentType();
int length = Array.getLength(obj);
Object newInstance = Array.newInstance(componentType, length + 1);
Array.set(newInstance, 0, obj2);
for (int i = 1; i < length + 1; i++) {
Array.set(newInstance, i, Array.get(obj, i - 1));
}
return newInstance;
}
}
我们需要读写文件的权限(在Android6.0之后),所以还要在AndroidManifest.xml
中添加读写权限。然后加上动态申请权限的代码。
然后在底部的termial
中执行gradlew makeJar
,只要执行结果有BUILD SUCCESSFUL即可。
...
Deprecated Gradle features were used in this build, making it incompatible with Gradle 6.0.
Use '--warning-mode all' to show the individual deprecation warnings.
See https://docs.gradle.org/5.1.1/userguide/command_line_interface.html#sec:command_line_warnings
BUILD SUCCESSFUL in 5s
60 actionable tasks: 5 executed, 55 up-to-date
这样我们在app->build->libs
目录下面有一个log.jar
的文件,这个文件的文件名可以在前面的gradle
里面的task makeJar
中baseName
设置。
我们要手动把它转换成dex文件。这里用到的是在AndroidSDK目录下面的dx
工具
执行
dx --dex --output=./test log.jar
就会在当前目录下的test目录下生成一个classes.dex文件,我们用这个文件就能热加载了。此处我把classes.dex
文件重命名为happyclass.dex
文件
再强调一下,我们先编译生成的应该是修复后的目标代码。
我们把happyclass.dex
文件放到/sdcard/
目录下面,也就是卡根目录。
我们现在写bug代码,然后测试热修复。
我们再把那个计算类修改为如下
public class TestCaculate {
public void caculate(Context context) {
int a = 10;
int b = 10;
Toast.makeText(context, "结果" + a / b, Toast.LENGTH_LONG).show();
}
}
最后,我们要把as里面的instant run
关闭
编译运行测试即可。
PS:
如果遇到
> Task :app:compileReleaseJavaWithJavac
注: *****\app\src\main\java\com\example\bugfix\MainActivity.java使用或覆盖了已过时的 API。
注: 有关详细信息, 请使用 -Xlint:deprecation 重新编译。
注: *****\app\src\main\java\com\example\bugfix\HotFixUtil.java使用了未经检查或不安全的操作。
注: 有关详细信息, 请使用 -Xlint:unchecked 重新编译。
> Task :app:lint
Ran lint on variant debug: 5 issues found
Ran lint on variant release: 5 issues found
这样的红色错误,可以在gradle配置文件中添加
allprojects {
gradle.projectsEvaluated {
tasks.withType(JavaCompile) {
options.compilerArgs << "-Xlint:unchecked" << "-Xlint:deprecation"
}
}
}
reference
https://blog.csdn.net/hq942845204/article/details/81044158