自己动手实现Android热修复

项目构成

最后代码文件组成

先写一个要修复的类

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 makeJarbaseName设置。
我们要手动把它转换成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

你可能感兴趣的:(自己动手实现Android热修复)