Android——Android热修复简单实现

本章会写出可以让你直接运行的Dome(后面附带本章节的源码)

一步一步跟着来肯定会成功!!!

再此先祝你成功啦。

APP更新,请参考 APP版本更新 该章节

前言:热修复是什么?

在刚进入王者荣耀的时候有没有遇见过,检测更新....再下载更新包...再解压压缩包...最后就可以进入游戏了。

没错!王者荣耀在上线的时候难免会出现bug,王者荣耀这是在修复bug。要是王者荣耀出现bug让你去重新下载一个没有bug的版本(一万只羊驼路过....),所以让玩家在进入王者时检测是否加载了最新的补丁包,是明智的选择。

效果图:

           

            一个有bug的 1.0图                                       修复bug后的 1.1图

本章节项目文件结构图:

实现起来也非常简单,但需要细心。接下来一起跟着我一步一步来实现吧。


第一步:编写我们带bug代码文件 BugClass.java

public class BugClass {

   static String str1 = "bug已修复,优秀!";    //修复后的代码
   static String str2 = "一个完美的bug";        //默认是带有bug的

    public static String Bug(Context context){
        Toast.makeText(context,str2,Toast.LENGTH_SHORT).show();
        return str2;
    }

}

第二步:复制 FixDexUtil.java 热修复工具类

public class FixDexUtil {

    //这下面两个属性可自己修改
    private final static String REPAIR_FILE_NAME = "BugClass";  //修复文件名
    private final static String REPAIR_FILE_PATH = "1079";      //修复文件路径(默认初始路径为根目录)

    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";
    private static final String DEX_DIR = "odex";
    private static final String OPTIMIZE_DEX_DIR = "optimize_dex";
    private static HashSet<File> loadedDex = new HashSet<>();

    static {
        loadedDex.clear();
    }

    /**
     * 开启修复
     *
     * @param context
     */
    public static void startRepair(final Context context) {
        File externalStorageDirectory = Environment.getExternalStorageDirectory();
        // 遍历所有的修复dex , 因为可能是多个dex修复包
        File fileDir = externalStorageDirectory != null ?
                new File(externalStorageDirectory, FixDexUtil.REPAIR_FILE_PATH) :
                new File(context.getFilesDir(), FixDexUtil.DEX_DIR);// data/user/0/包名/files/odex(这个可以任意位置)
        if (!fileDir.exists()) {//如果目录不存在就创建所有目录,这里需要添加权限
            fileDir.mkdirs();
        }
        if (FixDexUtil.isGoingToFix(context)) {
            FixDexUtil.loadFixedDex(context, Environment.getExternalStorageDirectory());
            Log.i("GT_", "正在修复");
        }
    }


    /**
     * 加载补丁,使用默认目录: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 2019/10/10 11:42
     * @desc 验证是否需要热修复
     */
    public static boolean isGoingToFix(@NonNull Context context) {
        boolean canFix = false;
        File externalStorageDirectory = Environment.getExternalStorageDirectory();

        // 遍历所有的修复dex , 因为可能是多个dex修复包
        File fileDir = externalStorageDirectory != null ?
                new File(externalStorageDirectory, REPAIR_FILE_PATH) :
                new File(context.getFilesDir(), DEX_DIR);// data/data/包名/files/odex(这个可以任意位置)

        File[] listFiles = fileDir.listFiles();
        if (listFiles != null) {
            for (File file : listFiles) {
                if ((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<File> 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;
    }

}

第三步:我们在 MyApp.java 中修复bug

public class MyApp extends Application {

    @Override
    public void onCreate() {
        super.onCreate();
        FixDexUtil.startRepair( getApplicationContext());//开启热修复
    }

}

第四步:在 AndroidManifest.xml (清单文件)中添加权限与指定MyApp的代码

权限:

<!-- 文件的读取和写入权限 -->
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>

MyApp的指定:

 <application
        android:name=".MyApp"
        ...
        >

</application>

MyApp指定的参考图:

第五步:在显示 Activity 与 xml 的内容 MainActivity.java 与 activity_main.xml

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        TextView tv = findViewById(R.id.tv);
        tv.setText(BugClass.Bug(this));
    }

}
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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"
    tools:context=".MainActivity">

    <TextView
        android:id="@+id/tv"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

好,现在咋们所有要写的代码已经写完了,就剩操作了。

操作1:安装带bug的app

此时直接运行项目,你的app会显示

           

            一个有bug的 1.0图           

操作2:做一个修复bug的 补丁包

现在你的app有bug,需要一个补丁去修复,那么我们现在就来教你做补丁包

先在项目中修复好bug,再将当前已经修复好bug的 类 打包成 dex 文件

修复bug:

public class BugClass {

   static String str1 = "bug已修复,优秀!";
   static String str2 = "一个完美的bug";

    public static String Bug(Context context){
        Toast.makeText(context,str1,Toast.LENGTH_SHORT).show();
        return str1;
    }

}

将 类 打包成 dex 文件 其实是将 class文件转成 dex 文件,我们先获取 class 文件先

第一节:点击 build --> Rebuild Project

第二节:展开 Project 结构 单独复制出 java 的编译文件 class (BugClass.class)

第三节:在D盘新建文件夹 dex ,并将 BugClass.class 文件复制到该目录

操作3:将 class 文件转为 dex 文件(关键点!!!)

先找到你的 Adnroid SDK 目录 中 build-tools 目录里的 dx.bat 文件。

博主这里的地址为:D:\King\E\sdk\build-tools\24.0.3(每个人的路径应该都不同)

找到后就打开 cmd 开始来转换把。

cmd 关键代码:

dx --dex --no-strict --output=D:\dex\GT_BugClass1.dex D:\dex\BugClass1.class

来查看dex文件夹吧,此时我们就有 dex 补丁包了

将该补丁包 复制到手机的 补丁文件夹下再重启 app (app需要停止运行)bug就修复了。

   

咋们重启APP后效果就是这样了。

            修复bug后的 1.1图

当然,除了这样获取dex进行热修复外,你还可以用这些方式获取:编译后会在这个目录出现这个apk文件,可以直接把这个文件放到 1079 目录也照样会解析成dex进行热修复,

 还可以直接将后缀名改为 zip后,直接放1079目录也行,当然,zip里的文件也可以删除一些不需要的

 我们也可以将zip里面的 dex 全部拿出来,放1079目录也可以。或者在zip里删除掉除dex外的所有文件,再将这个精简的 zip 放入1079文件,也一样

总结:本章节细节地方比较多,只要你一步一步跟着来,肯定会成功的。

本章节源码网址:GitHub - 1079374315/ThermalRemediation: APP 热修复 Dome

源码中我已将 dex 文件拷贝到项目中 具体地址请参考该图

APP更新,请参考 APP版本更新 该章节

点个关注点个赞呗(〃'▽'〃),关注博主最新发布库:GitHub - 1079374315/GT

美帝 框架,让创造变得如此简单!

评论 13
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

PlayfulKing

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值