Android手写实现class文件的热修复(仿Tinker)

前言:

           又有一阵子没有更新博客了,最近本人在完成一个开源项目。马上又到年尾了,所以时间比较赶。等完成之后会给大家分享一下,希望小伙伴们能多多支持(star)啊!好了,回到今天的主题,热修复这个技术想必大家都听过,或者也试过。这确实是一个非常牛逼的技术。但其实已经不算是新东西了。这些年来国内各大互联网巨头也都推出了自己的热修复方案,比如我要说的微信的tinker,还有像阿里的andfix,等等。之所以会出现这么一个东西,估计大家也都能猜个七八。无非就是传统的APP上线流程带来的各种弊端。小公司的小项目也许可能影响不大。但像大厂的那些DAU海量的项目那就影响非常巨大了。大家也都看到了,我今天分享的是class文件的修复,其实热修复能做的远不止这些啊,它还能对资源文件,so库等等进行修复。限于篇幅和复杂度呢,接下来我将不会涉及到这些。大家如果有兴趣可以去看看Tinker的官网,好了,下面就让我带大家来看一看热修复的简单实现吧!

一,过去与现在:

下面通过两张图我们来看看传统APP开发与热修复开发的不同之处:

 

 很明显在第五步开始出现了不同,那我来总结一下热修复的优势有哪些:

  1. 无需重新发布新版本省时省力
  2. 用户无感修复,无需下载新版本,体验很好
  3. 修复效率高,降低损失

二 ,热修复原理:

关于原理我也直接画了张图,带大家看看

总结起来就是利用反射,把修复好的dexclassloader的pathlist的dexElements,和系统原本的dexElements合并,注意合并时把修复好的放前面,最后把这个合并的赋值给系统的pathList即可。说完这些我估计有很多小伙伴会很懵,首先要给大家说的是,这种class文件修复的做法原理和tinker差不多,这个小伙伴也可以去看看tinker的源码。可能有些小伙伴对这些API或者流程很不理解啊,首先大家要知道,一个apk文件它其实就是打包了dex文件和资源文件,小伙伴可以将apk文件的后缀名改为zip,再去解压一下就知道了。那么dex文件呢就是我们Android工程内所有class文件的合集。因为APP最终运行的时候就是靠Android系统去执行这些dex文件,整个过程就是从我们写下Java文件开始(源码期),经由JVM编译成class文件(编译期),然后通过dex工具转换成dex文件,最后经过Dalvik虚拟机去执行。所以我们的要做到class修复,重点就是要改变这些有bug的dex文件。而这些dex文件就存在于dexclassloader的pathlist的dexElements数组中,之后重点就是各种反射的用法了。关于反射小伙伴们如果不熟悉可以自己去查查资料回顾一下。那么,以上所有这些流程,下面我将通过代码实例展示给大家看看。

三,编码实现: 

下面我直接上代码,注释已经很清楚了:

//这里我直接将已修复的dex文件打包好复制到APP私有路径中,实际运用中我们先是从服务器拉取
public void goHotFix(View view) {
        //获取到apk的私有存储路径
        File filesDir = getDir("dex", Context.MODE_PRIVATE);
        //获取到没有bug的dex文件的名字
        String name = "Test.dex";
        //创建该dex文件的file
        String path = new File(filesDir, name).getAbsolutePath();
        //根据这个路径去创建一个新的file对象
        File file = new File(path);
        //如果这个文件存在就删除掉
        if(file.exists()){
            file.delete();
        }
        //创建io流
        InputStream is = null;
        FileOutputStream os = null;
        try {
           is = new FileInputStream(new File(Environment.getExternalStorageDirectory(),name));
           os = new FileOutputStream(path);
           int len = 0;
           byte[] bytes = new byte[1024];
           while ((len = is.read(bytes))!=-1){
                os.write(bytes,0,len);
           }
            File f  = new File(path);
           if(f.exists()){
               Toast.makeText(this, "文件复制成功", Toast.LENGTH_SHORT).show();
           }
           FixManager.loadDex(this);
        }catch (Exception e){
           e.printStackTrace();
        }finally {
            try {
                is.close();
                os.close();
            } catch (IOException e) {
                e.printStackTrace();
            }

        }
    }
/**
 * Author by YX, Date on 2019/12/21.
 * 修复Dex文件的工具类
 */
public class FixManager {

    //创建一个用来存储加载到的dex文件的集合
    private static HashSet<File> loadedDexSet = new HashSet<>();

    static {
        //保证集合操作前为空
        loadedDexSet.clear();
    }

    /**
     *这里就是代替dex文件的操作
     * 根据上下文来加载dex文件,并放入集合中
     * @param context
     */
    public static void loadDex(Context context){
        if(context == null){
            return;
        }
        //获取当前应用所在私有路径,也就是dex文件的目录
        File odexDir = context.getDir("dex", Context.MODE_PRIVATE);
        //通过该目录获得目录下所有文件的数组
        File[] files = odexDir.listFiles();
        for (File file : files) {
            if(file.getName().startsWith("classes")||file.getName().endsWith(".dex")){
                loadedDexSet.add(file);
            }
        }
        //创建一个目录,用来装载解压的文件
        String optimizeDir = odexDir.getAbsolutePath() + File.separator + "opt_dex";
        File fopt = new File(optimizeDir);
        //如果这个目录不存在就创建
        if(!fopt.exists()){
            fopt.mkdirs();
        }
        //遍历这个dex集合
        for (File file : loadedDexSet) {
            //获取当前dex类加载器
            DexClassLoader dexClassLoader = new DexClassLoader(file.getAbsolutePath(), fopt.getAbsolutePath(), null, context.getClassLoader());
            //实现一个类加载器的对象
            PathClassLoader pathClassLoader = (PathClassLoader) context.getClassLoader();
            try {
                //通过反射拿到系统类加载器
                Class<?> baseDexClassLoaderClass = Class.forName("dalvik.system.BaseDexClassLoader");
                Field systemPathList = baseDexClassLoaderClass.getDeclaredField("pathList");
                systemPathList.setAccessible(true);
                Object splObj = systemPathList.get(pathClassLoader);
                Class<?> pathListClass = splObj.getClass();
                Field dexElements = pathListClass.getDeclaredField("dexElements");
                dexElements.setAccessible(true);
                Object dexElementsObj = dexElements.get(splObj);

                //创建自己的类加载器
                Class<?> myBaseDexClassLoaderClass = Class.forName("dalvik.system.BaseDexClassLoader");
                Field myPathList = myBaseDexClassLoaderClass.getDeclaredField("pathList");
                myPathList.setAccessible(true);
                Object mysplObj = myPathList.get(dexClassLoader);
                Class<?> myPathListClass = mysplObj.getClass();
                Field myDexElements = myPathListClass.getDeclaredField("dexElements");
                myDexElements.setAccessible(true);
                Object mydexElementsObj = myDexElements.get(mysplObj);

                //进行dex文件的融合
                Class<?> componentType = dexElementsObj.getClass().getComponentType();
                //分别得到两个dexElements的长度
                int systemDEL = Array.getLength(dexElementsObj);
                int myDEL = Array.getLength(mydexElementsObj);
                //创建一个能放入它们的数组
                int newL = systemDEL + myDEL;
                Object newDEL = Array.newInstance(componentType, newL);
                for (int i = 0; i < newL; i++) {
                    if(i < myDEL){
                        Array.set(newDEL,i,Array.get(mydexElementsObj,i));
                    }else {
                        Array.set(newDEL,i,Array.get(dexElementsObj,i-myDEL));
                    }
                }
                //将融合后的数组赋值给系统
                Field systemDexElements = pathListClass.getDeclaredField("dexElements");
                systemDexElements.setAccessible(true);
                systemDexElements.set(splObj,newDEL);
            } catch (Exception e) {
                e.printStackTrace();
            }finally {
                Toast.makeText(context, "修复成功", Toast.LENGTH_SHORT).show();
            }
        }
    }
}
public class MyApp extends Application{

    @Override
    public void onCreate() {
        super.onCreate();
    }

    @Override
    protected void attachBaseContext(Context base) {
        //注意这里要到gradle里面先开启分包
        MultiDex.install(base);
        FixManager.loadDex(base);
        super.attachBaseContext(base);
    }
}

 上面代码就是总体实现,之后我们将编译好的apk解压,要注意的是,由于做了分包处理,我们要把size更大的那个dex文件提取出来使用,为什么要分包,给你一个数字65536。可能有些小伙伴会觉得这样做,岂不是apk的体积会变得很庞大。其实呢这只是一种简单通俗的做法。但也确实会存在上述问题。我们也可以单独把修复的文件打成dex文件,这样就更好一点。当然我还是要说这只是我自己的做法,小伙伴们可以自己去研究tinker的源码看看。如果不知道如何单独把出错经修复的Java文件打包成dex文件,下面我也分几步告诉大家 :

  1. 首先rebuild一下工程,在app\build\intermediates\classes\debug下拿到想要代替的class文件
  2. 在AndroidSDK的build-tools下随便选一个版本进去打开cmd命令,
  3. 输入dx --dex --no-strict --output空格 生成dex文件的路径 空格 class文件的路径  最后回车即可

如果是按我的做法就是把这个dex文件取名为Test放到sdcard根目录下即可,随后就能完成修复工作了。工程源码我已开源,想要的小伙伴看这里 https://github.com/OMGyan/XHotFix

以上!!!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值