0
点赞
收藏
分享

微信扫一扫

Android 热修复之路,未来更加澎湃

柠檬果然酸 2022-04-13 阅读 100
javaandroid

前言

什么是热修复?

热修复其实在外面大家日常生活中很常见,就比如现在很火的腾讯手游,在进入游戏经常有一些数据包进行更新,然后再进入到游戏中,这其实也就是游戏中出现了一些BUG,检查更新也就是对这个BUG进行修复,这个数据包也可以称之为补丁包

效果图如下:
在这里插入图片描述

热修复的优势

热修复通常具有无需重新对应用程序进行下载的功能,极大的方便了用户对应用程序的体验感,也因其热修复也使用户不用消耗过多的流量,所以变向的节省了用户的流量

对公司而言,热修复技术能够极大的提高对BUG的修复率,同时也避免了因为这种BUG的出现导致对公司的带来的业务中的损失,可以将这种损失达到最低化

热修复的不足

热修复虽然可以对BUG的修复率能够进行极大的提高,但并不能保证所有的热修复框架进行一个完全的修复,也就是说对BUG的修复只是相对的

热修复的框架的核心技术

热修复的代码是其最为常见的,同时也是我们要进行热修复中必不可少的一部分,通常在程序发生错误的时候其实都是代码逻辑中出现的错误。我们进行热修复所使用的方案也不过是仅仅对戴安热修复进行支持,下面是对框架结构的说明

框架结构主要分为三类

代码修复:
一个app程序重新启动中,会对补丁中的类进行一个优先加载,以此来达到其热修复的结果,andfix所使用的方式就是使用替换功能在已经加载了类中的native层进行替换,也就是在其本身的类上进行修改,以此来对其达到即时生效的结果

资源修复:
基本上在当前市面上有很多的资源热修复方案都是以lnstant Run为其参考实现
资源修复总的来说一共分为两步,首先先构造出一个新的AssetManager,让其通过反射进行调用addAssetPath,然后将其进入到AssetManager中,这样就能够得到一个拥有其所有新资源的AssetManager,最后再找到引用到AssManager的所有之前的地方,让其通过反射,将引用出替换成AssetManager,这样就达到了资源修复的结果

动态链修复:
现在的操作系统中通常有很多以DLL作为后缀的文件,称之为应用程序拓展,也就是系统里的动态链接库文件,一般来说,有很多的应用程序进行运行的过程中基本上都会对这些动态库文件进行一个调用,同样的,在对应用程序进行卸载的时候,这些动态链文件也会被其误删,这样就会使其他程序进行运行时因无法找到动态链文件而发生错误,当然了,遇到这种问题也并不需要对其重装操作系统,只要对其动态库文件进行修复即可解决

在这里插入图片描述

项目文件结构图:

在这里插入图片描述
现在我们就开始实现热修复的功能

第一步:首先编写一个带有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().startsWith(REPAIR_FILE_NAME) &&
                        (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());//开启热修复
    }
 
}

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

权限:

<!-- 文件的读取和写入权限 -->
    <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会显示

在这里插入图片描述

操作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;
    }
 
}

修复后完整效果图:

在这里插入图片描述

总结:

本次关于Android热修复相关的知识就讲到这里了,总的来说此次的细节比较多,还是需要各位同学一步一步跟上我的节奏,方能成功
文章中的源码有需要的同学可以在评论区下方留言或者私信我

举报

相关推荐

0 条评论