热修复

目前国内Android热修复技术已经发展的可以说百花齐放了,从实现方式来大致分类,可以分为:

  1. Native层实现
  2. Java层实现

之前也有人简单分析过阿里开源的Andfix实现原理(基于Native层),这里就不再多说了感兴趣的可以搜索看下。

本文简单分析一下Java层实现热修复逻辑,简单实现代码热修复Demo,以Tinker为例(当然Tinker是支持代码修复,资源修复,so修复的,感兴趣的小伙伴自行移步官网~)

第一次看我文章的小伙伴可以关注一下我,顺便关注一下我的专栏:Android高级开发架构,每天更新各种技术干货,分享更多最热程序员圈内事。
Android高级开发架构​zhuanlan.zhihu.com
d307dab995ef514d76c5c35cca997f91.png

先梳理下思路:

Java类编译过程

就是java类通过javac编译成.class文件,再由dx.bat编译成.dex文件的过程,不做赘述,简单画张图~

723ec655aebd39dec04fc752267f0b0b.png

ClassLoader简介

Android中的java.lang.ClassLoader这个类也不同于Java中的java.lang.ClassLoader。 Android中的ClassLoader类型也可分为系统ClassLoader和自定义ClassLoader。其中系统ClassLoader包括3种分别是:

  • BootClassLoader,Android系统启动时会使用BootClassLoader来预加载常用类,与Java中的Bootstrap ClassLoader不同的是,它并不是由C/C++代码实现,而是由Java实现的。BootClassLoader是ClassLoader的一个内部类。
  • PathClassLoader,全名是dalvik/system.PathClassLoader,可以加载已经安装的Apk,也就是/data/app/package 下的apk文件,也可以加载/vendor/lib, /system/lib下的nativeLibrary。
  • DexClassLoader,全名是dalvik/system.DexClassLoader,可以加载一个未安装的apk文件。 PathClassLoader和DexClasLoader都是继承自 dalviksystem.BaseDexClassLoader,它们的类加载逻辑全部写在BaseDexClassLoader中。 下图展示了Android中的ClassLoader中的继承体系,其中SecureClassLoader和UrlClassLoader是在Java中的类加载器,在Android中是没法办使用的。

f7288ea3465d60477835f66ef41091a2.png

.dex文件加载

通过源码得知,.dex文件是通过BaseDexClassLoader类(ClassLoader的子类)进行加载的,这个类里面有个成员变量DexPathList对象,而这个对象中有个有个数组存放的是DexElement对象,也就是从文件中加载的.dex文件,切入点就在这里

对于项目来说,一般项目会分包(方法数大于64k,及大于65535个时候,google提供的分包策略),如果采用Java代码实现热修复,分包是肯定要做的,因为要保证主包没有bug,分包简单说就是打包的apk一般有多个.dex文件

如: classes.dex,classes2.dex 等等...

那么比如我们的classes2.dex中某个类的某个方法出现了异常,我们就可以创建一个修复包(修复后的classes2.dex文件),然后通过自定义的类加载器将修复后的classes2.dex文件copy到私有目录,再插队到系统ClassLoader的dexPathList对象的dexElement的数组中,让系统优先加载修复后的classes2.dex文件,以做到热修复的目的,这种实现方式必须要执行修复逻辑后,重启app才能实现效果~

了解了这些信息大致思路就有了,我们需要修复后的.dex文件加载解析,然后插队旧的安装包装的.dex文件,做到插队的操作,相当于欺骗了Android系统,大致如下:

实现原理

思路大概是,我们需要一个修复bug的.dex文件,插队到BaseDexClassLoader类下的DexPathList对象的DexElement数组中,并且排序到最前面,让系统加载到我们修复后的.dex文件不会再加载有bug的dex文件,完成插队(插装),这里会有个类加载机制的知识,本文不做详细介绍,后面会单独写一篇总结~ 大致实现步骤如下:

22725791f857748ddc318d94514e20bf.png

Demo实现

1、基础配置-主包配置

配置分包,配置分包的目的主要是打包做出来的apk会有多个.dex文件,实际项目应用中要保证主包不要有bug,demo中加载.dex文件的时候也排除了主包文件classes.dex,大致如下: 创建BaseApplication,BaseActivity,MainActivity放置在主包,其中MainActivity主要是为了分包占位,只做了点击跳转分包中SecondActivity的逻辑 app目录下的build.gradle开启分包支持,在android→defaultConfig下增加配置,其中multiDex-config.txt是配置保留在主包内类文件

 //开启分包
        multiDexEnabled true
        //分包的配置,将配置文件中的放置在主包
        multiDexKeepFile file("multiDex-config.txt")

添加分包依赖:

  //multidex分包依赖
    implementation 'com.android.support:multidex:1.0.3'

Application开启分包:

public class BaseApplication extends MultiDexApplication {
    @Override
    public void onCreate() {
        super.onCreate();
    }

    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
        //安装分包配置
        MultiDex.install(this);
    }
}

2、分包配置

分包就创建了一个SecondActivity类做模拟异常和修复异常的入口,和一个Calculate模拟异常,做了10/0的操作,修复后为10/1 注:获取修复后的classes2.dex文件可以通过直接buildapk直接解压获取,或者用build-tools下的dx.bat执行命令获取

简单贴下SecondActivity,点击fix按钮后的代码,完整代码见文末~:

  private void update() {
        //将下载的修复包,复制到私有目录,解压从.dex文件中取到对应的.class文件
        //从sd卡取修复包
        File sourceFile = new File(Environment.getExternalStorageDirectory(), Constants.DEX_NAME);
        //目标文件
        File targetFile = new File(getDir(Constants.DEX_DIR, Context.MODE_PRIVATE).getAbsolutePath() + File.separator + Constants.DEX_NAME);
        if (targetFile.exists()) {
            targetFile.delete();
            Log.e("update","删除原有dex文件(已使用的)");
        }
        //将SD卡中的修复包copy到私有目录
        FileUtils.copyFile(sourceFile,targetFile);
        Log.e("update","copy完成");
        FixDexUtils.loadDexFile(this);
    }

3、FixModule

新建一个Module处理热修复的相关逻辑

35314140005afe05dc0b8e42e3b4ac8c.png

只有五个文件,核心文件代码在FixDexUtils,其它是工具类,还有个定义了几个常量 看下FixDexUtils中的代码

public class FixDexUtils {

    //修复文件可能有多个
    private static HashSet<File> loadedDex = new HashSet<>();

    //不建议这么写,demo演示用
    static {
        loadedDex.clear();
    }

    public static void loadDexFile(Context context) {
        //获取私有目录
        File fileDir = context.getDir(Constants.DEX_DIR, Context.MODE_PRIVATE);
        //遍历筛选私有目录中的.dex文件
        File[] listFiles = fileDir.listFiles();

        for (int i = 0; i < listFiles.length; i++) {
            //文件名以.dex结尾,且不是主包.dex文件
            if (listFiles[i].getName().endsWith(Constants.DEX_SUFFIX) && !"classes.dex".equalsIgnoreCase(listFiles[i].getName())) {
                loadedDex.add(listFiles[i]);
            }
        }
        //创建自定义的类加载器
        createDexClassLoader(context ,fileDir);
    }

    /**
     * @param context
     * @param fileDir
     * 创建自己的类加载器,加载私有目录的.dex文件,上面已经将修复包中的dex文件copy到私有目录
     */
    private static void createDexClassLoader(Context context, File fileDir) {
        //解压目录
       String optimizedDir = fileDir.getAbsolutePath()+File.separator+"opt_dex";
        File fileOpt = new File(optimizedDir);
        if (!fileOpt.exists()) {
            fileOpt.mkdirs();
        }

        for (File dex : loadedDex) {
            //创建自己的类加载器,临时的
            DexClassLoader classLoader = new DexClassLoader(dex.getAbsolutePath(), optimizedDir, null, context.getClassLoader());
            //有一个修复文件,就插装一次
            hotFix(classLoader,context);
        }
    }

    private static void hotFix(DexClassLoader classLoader, Context context) {
        try {
            //获取系统的PathClassLoader类加载器
            PathClassLoader pathClassLoader = (PathClassLoader)context.getClassLoader();
            //获取自己的dexElements数组

            Object myElements = ReflectUtils.getDexElements(ReflectUtils.getPathList(classLoader));
            //获取系统的dexElements数组
            Object systemElements = ReflectUtils.getDexElements(ReflectUtils.getPathList(pathClassLoader));
            //合并数组,并排序,生成一个新的数组
            Object dexElements=ArrayUtils.combineArray(myElements,systemElements);
            //通过反射获取系统的pathList属性
            Object systemPathList = ReflectUtils.getPathList(pathClassLoader);
            //通过反射,将合并后新的dexElements赋值给系统的pathList
            ReflectUtils.setFieldValue(systemPathList,"dexElements",dexElements);
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

主要做的工作就是:就是上面那张流程图,就是先通过遍历,解压等操作,获取到需要执行热修复的.dex文件集合,遍历该集合,对应每次创建一个临时的DexClassLoader,然后执行修复步骤,细分就是六步:

1e5545dd16112f274eb7a8f190363ce2.png

最终实现效果如图(Demo使用手机是华为8.0的手机): 注:这里为了效果图更直观,已经重启过一次app 注: 这种方式实现的热修复必须要重启App才可以实现修复,这一点也是类加载机制决定的,如下图中修复之后再次打开加载执行修复后的classes.dex文件是在BaseApplication中调用了修复方法

c60e1fc7cbe307c556d9394c95318160.gif
Android学习PDF+架构视频+面试文档+源码笔记​shimo.im
b0f324e91e80059776640d172fe1a127.png

最后

在这里我也分享一份由几位大佬一起收录整理的Android学习PDF+架构视频+面试文档+源码笔记高级架构技术进阶脑图、Android开发面试专题资料,高级进阶架构资料

如果你有需要的话,可以私信我【进阶】我发给你

喜欢本文的话,不妨顺手给我点个小赞、评论区留言或者转发支持一下呗~

74d8060fbf77617d1bb811e0a9529292.png

9fd3dc26d0c06b12dedcd9b4fff736d5.png

acf1b062f9e65137040681d0a566d502.png

60523c1fe8e81bef82bf8530eb2b3e0b.png
Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐