tinker热修复原理学习

热修复这一功能对很多公司的开发者来说都会有集成,因为测试不是万能的,万一线上遇到一点小bug,就会导致流量流失或者其他造成公司损失的问题,我自己项目中也集成了非常有名的Tinker热修复,具体怎么集成这里就不记录了,主要看官方文档

这里主要记录一下Tinker热修复的源流。

先做个热修复原理的总结,然后再手写实现热修复

  1. APK从专业的角度来讲是一些Dex文件的集合,Classloader是专门管理Dex文件的类,在Classloader里面有个findClass方法,在activity或者其他类在加载的时候,会调用这个方法,去Dex文件里面找这个类,如果找到就把这个类放到容器DexElemengs[]里面里面装的是APK里面所有的类。
    
  2. dex文件就是所有类的.dex的集合,里面装载了所有类的dex,热修复的思路就是获取到apk的私有路径,再去遍历所有目录,寻找出classes.dex文件,包括当前应用的dex集合也包括我们写入(已修复的)的dex集合。然后再把两个dex集合的数组拿出来,再合并,合并后的数组给到dexElements,由于我们把修复的dex放在数组的前面,未修复的在后面,所以类加载器在加载的时候会选择加载修复后的dex
    
  3. 热修复的原理就是Hook了ClassLoader .pathLish .dexElements[]因为ClassLoader的FindClass是通过遍历DexElements[]中的Dex来寻找类的,比如MainActivity出现bug,只需要找到其对应的Dex文件,然后替换即可。类加载器有个特点就是  如果先找到要找的类dex,就不会再去找了,因此如果一个dex文件里面两个main.dex,只会加载第一个,第二个不会再去处理,因此补丁修复的原理就是把补丁的dex放到前面,原来有bug没修复的dex放到后面

接下来是代码实现,主要是ClassLoader及其子类api的调用

package com.dhd.hotrepair;

import android.content.Context;

import java.io.File;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.util.HashSet;

import dalvik.system.DexClassLoader;
import dalvik.system.PathClassLoader;

public class BugFixManager {

    private static HashSet<File> loadedDex = new HashSet<>();

    static {
        loadedDex.clear();
    }

    public static void loadDex(Context context) {
        if (context == null) {
            return;
        }
        //获取当前上下文的私有路径  也就是Dex的文件目录
        File fileDir = context.getDir("odex", Context.MODE_PRIVATE);
        //通过私有路径获取到路径目录中所有文件的数据
        File[] files = fileDir.listFiles();
        //遍历
        for (File file : files) {
            //判断文件对象的集合是不是符合我们的条件
            if (file.getName().startsWith("classes") || file.getName().endsWith(".dex")) {
                loadedDex.add(file);
            }
        }
        //创建一个目录,用来装载解压的文件
        String optmizeDir = fileDir.getAbsolutePath() + File.separator + "opt_dex";
        File fopt = new File(optmizeDir);
        if (!fopt.exists()) {
            fopt.mkdir();
        }
        //循环所有找到的Dex文件集合
        for (File dex : loadedDex) {
            //获取到系统的类加载器
            DexClassLoader dexClassLoader = new DexClassLoader(dex.getAbsolutePath(), fopt.getAbsolutePath(), null, context.getClassLoader());
            //实例一个类加载器对象
            PathClassLoader pathClassLoader = (PathClassLoader) context.getClassLoader();
            //***********************通过反射获取系统的类加载器************************
            try {
                //获取到DexClassLoader类加载器的父类
                Class<?> baseDexClazzloader = Class.forName("dalvik.system.BaseDexClassLoader");
                //从父类中获取到pathList的成员变量
                Field pathListField = baseDexClazzloader.getDeclaredField("pathList");
                pathListField.setAccessible(true);
                //获取成员变量对象的值,比如Main里面的有个私有变量a的值
                Object pathListObject = pathListField.get(pathClassLoader);
                Class<?> systemPathListClass = pathListObject.getClass();
                //通过类对象获取里面的dexElements的成员变量
                Field dexElementsField = systemPathListClass.getDeclaredField("dexElements");
                dexElementsField.setAccessible(true);
                //获取到dexElements成员变量的值
                Object systemElements = dexElementsField.get(pathListObject);

                //*******************************创建自己的类加载器******************************
                Class<?> myDexClazzLoader = Class.forName("dalvik.system.BaseDexClassLoader");
                Field myPathListField = baseDexClazzloader.getDeclaredField("pathList");
                myPathListField.setAccessible(true);
                Object myPathListObj = myPathListField.get(dexClassLoader);
                Field mydexElementsField = myPathListObj.getClass().getDeclaredField("dexElements");
                mydexElementsField.setAccessible(true);
                Object myElements = mydexElementsField.get(myPathListObj);
                //*******************************进行dex文件融合******************************
                //获取到systemElements的类对象
                Class<?> componentType = systemElements.getClass().getComponentType();
                //得到系统(未修复的)的dexElements的长度
                int systemLength = Array.getLength(systemElements);
                int mylength = Array.getLength(systemElements);
                //创建一个新的长度
                int newSystemLength = systemLength + mylength;
                //生成一个新的数组
                Object newElementsArray = Array.newInstance(componentType, newSystemLength);
                for (int x = 0; x < newSystemLength; x++) {
                    if (x < mylength) {
                        //将自己定义的数组(补丁dex数组)放到前面
                        Array.set(newElementsArray, x, Array.get(myElements, x));
                    } else {
                        //系统未修复bug的dex放到后面
                        Array.set(newElementsArray, x, Array.get(systemElements, x - mylength));
                    }
                }
                //将系统的数组和我们定义的数组融合之后,再放入系统的数组中
                Field dexElements = pathListObject.getClass().getDeclaredField("dexElements");
                dexElements.setAccessible(true);
                dexElements.set(pathListObject, newElementsArray);


            } catch (Exception e) {
                e.printStackTrace();
            }
        }


    }
}

然后修复类已经写好了,怎么调用呢,比如MainActivity里面有bug,可以如下方式调用


public class MainActivity extends AppCompatActivity {
    private TextView mResult;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mResult = findViewById(R.id.tv);

    }

    public void calculate(View view) {
        mResult.setText(BugTest.getResult(3, 0)+"");
    }

    //吧补丁的dex文件全部写入当前应用的私有路径下面,然后去私有路径下面拿到所有dex文件,然后再遍历,再融合
    public void repair(View view) {
        //获取apk的私有存储路径
        File filesDir = this.getDir("odex", Context.MODE_PRIVATE);
        //获取到没有bug的dex文件的名字
        String name = "out.dex";
        //创建一个修复好的dex文件存储路径
        String filePth = new File(filesDir, name).getAbsolutePath();
        //根据这个路径去创建一个新的file对象
        File file = new File(filePth);
        if (!file.exists()) {
            file.delete();
        }
        InputStream is = null;
        FileOutputStream os = null;
        try {
            is = new FileInputStream(new File(Environment.getExternalStorageDirectory(), name));
            os = new FileOutputStream(filePth);
            int len = 0;
            byte[] buffer = new byte[1024];
            while ((len = is.read(buffer)) != -1) {
                os.write(buffer, 0, len);
            }
            File f = new File(filePth);
            if (f.exists()) {
                Toast.makeText(this, "修复成功", Toast.LENGTH_SHORT).show();
            }
            BugFixManager.loadDex(this);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                is.close();
                os.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

其中代码里面提到的out.dex文件是我们已经修复后打包的dex文件,通过AS的DeviceFileExplorer将这个out.dex上传到模拟器或者手机的sdcard路径下面,再找到这个路径,就可以实现dex的替换。

 

那Tinker中是如何替换有bug的dex文件的呢?

用过Tinker的都知道,Tinker有个后台管理,每次发布热补丁都要将patch_signed_7zip.apk上传到服务器,其实这个操作就是Tinker将我们上传的补丁包push到了手机的sdcard目录下面;推送完毕就是上面代码中的流程了

详细的Tinker内部源码解析参阅

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值