(十四)Tinker 热修复原理及手写实现

本文深入解析了热修复技术,对比了阿里AndFix、美团Robust、QZone超级补丁及Tinker热修复方案的特点,详细介绍了Tinker的工作原理,包括类替换、So替换和资源替换等。并通过手写代码实现了热修复过程,包括修复实现、模拟异常、模拟更新包下载、在Application中修复以及生成修复包等步骤。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

版权声明:本文为博主原创文章,未经博主允许不得转载。
本文纯个人学习笔记,由于水平有限,难免有所出错,有发现的可以交流一下。

一、热修复

热补丁方案有很多,其中比较出名的有阿里的 AndFix、美团的 Robust 以及 QZone 的超级补丁方案。以及我们要分析的 Tinker 热修复。

TinkerQZoneAndFixRobust
类替换yesyesnono
So 替换yesnonono
资源替换yesyesnono
全平台支持yesyesyesyes
即时生效nonoyesyes
性能消耗较小较大较小较小

Tinker 与QZone 是在java层进行替换,可以直接替换整个java 类,AndFix 和 Robust 是在 ndk 层进行替换,只能替换局部方法。

这四种热修复中,只有 Tinker 可以进行 so 的替换,其他不行,现在还有种 sophix 热修复也可以进行 so 的替换,但是 sophix 没有开源。

在安卓版本支持上来说,Tinker 是支持所有的。AndFix 在某些版本支持没有很好。

在性能损耗上,Tinker 这边虽然写着较小,但是个人感觉 Tinker 与 QZone 性能损耗应该都是比较大的。

二、Tinker 原理

在上一篇(十三)Dex 加解密与多 Dex 加载 中提到,ClassLoader 是通过加载自身私有属性 dexElements 的 dex 数组进行多 dex 的加载。

ClassLoader 在加载一个类的时候,会从头开始遍历 dexElements 数组,对各个 dex 进行查找,如果查找到要加载的类,则直接返回,不再继续查找。

例如:dexElements 有三个 dex,分别是 class.dex、class1.dex 和 class2.dex,如果 class1.dex 和 class2.dex 中都有一个 Test.class,那么按这个规则,在加载 Test 这个类的时候,会先找到 class1.dex 中的 Test.class,然后直接返回。
在这里插入图片描述

利用这一特性,Tinker 热修复把要修复的类文件打包成 dex,然后插入到 dexElements 数组中靠前的位置即可。

三、手写实现

1.修复实现

修复的核心内容与(十三)Dex 加解密与多 Dex 加载一样,这边直接贴代码,进行了小修改。

loadDex:

   /**
     * 加载 dex
     * @param dexFiles 需要加载的 dex 集合
     * @param optimizedDirectory dex 加载缓存目录
     */
    private static void loadDex(List<File> dexFiles, File optimizedDirectory){

        try {
            /**
             * 1.获得系统 classloader 中的 dexElements 数组
             */
            // 获得 classloader 中的 pathList(是一个 DexPathList)
            Field pathListField = ClassUtil.findField(App.getInstance().getClassLoader(), "pathList");
            Object pathList = pathListField.get(App.getInstance().getClassLoader());

            // 获得pathList类中的 dexElements
            Field dexElementsField = ClassUtil.findField(pathList, "dexElements");
            Object[] dexElements = (Object[]) dexElementsField.get(pathList);

            /**
             * 2.创建新的 element 数组 -- 解密后加载dex
             */
            // 需要适配安卓版本,5.0、6.0、7.0 都不一样
            // 具体要看各个版本的 dexElements 的创建方法是哪个,对这个方法进行反射
            ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
            Method makeDexElements = null;
            // Element 数组
            Object[] addElements = null;
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && Build.VERSION.SDK_INT <
                    Build.VERSION_CODES.M) {
                makeDexElements = ClassUtil.findMethod(pathList, "makeDexElements", ArrayList.class,
                        File.class, ArrayList.class);
                addElements = (Object[]) makeDexElements.invoke(pathList, dexFiles,
                        optimizedDirectory, suppressedExceptions);
            } else if(Build.VERSION.SDK_INT < Build.VERSION_CODES.N){
                makeDexElements = ClassUtil.findMethod(pathList, "makePathElements", ArrayList.class,
                        File.class, ArrayList.class);
                addElements = (Object[]) makeDexElements.invoke(pathList, dexFiles,
                        optimizedDirectory, suppressedExceptions);
            }else if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.N){
                makeDexElements = ClassUtil.findMethod(pathList, "makePathElements", ArrayList.class,
                        File.class, ArrayList.class, ClassLoader.class);
                addElements = (Object[]) makeDexElements.invoke(pathList, dexFiles,
                        optimizedDirectory, suppressedExceptions, App.getInstance().getClassLoader());
            }

            /**
             * 3.合并两个数组
             */
            //创建一个数组
            Object[] newElements = (Object[]) Array.newInstance(dexElements.getClass()
                    .getComponentType(), dexElements.length +
                    addElements.length);

	    //合并数组,更新包一定要放在前面,这样才会先被加载
            System.arraycopy(addElements, 0, newElements, 0, addElements.length);
            System.arraycopy(dexElements, 0, newElements, addElements.length, dexElements.length);

            /**
             * 4.替换classloader中的 element数组
             */
            dexElementsField.set(pathList, newElements);

        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
    }

2.模拟异常

新建一个项目,在 MainActivity 中调用 Test 类的方法,触发一个除 0 异常。

MainActivity:

public class MainActivity extends AppCompatActivity {

    private final static String TAG = "MainActivity";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

    }

    public void jisuan(View view) {

        Test test = new Test();
        Toast.makeText(this, "计算结果为" + test.test(), Toast.LENGTH_LONG).show();
    }

    /**
     * 修复 bug
     * @param view
     */
    public void xiufu(View view) {

    }
}

activity_main.xml:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="计算"
        android:onClick="jisuan"/>
    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="修复"
        android:onClick="xiufu"/>
</LinearLayout>

Test :

public class Test {

    public int test(){

        int a = 10;
        int b = 0;

        return a / b;
    }

}

3.模拟更新包下载

在 xiufu 这个方法中,从 sd 卡目录下拷贝更新包,模拟实际项目中从网络上下载更新包。修复包保存路径为 /data/data/包名/app_dexs/out.dex。

    public void xiufu(View view) {

        // 修复包保存为 /data/data/包名/app_dexs/out.dex
        File filesDir = this.getDir("dexs", Context.MODE_PRIVATE);
        String name = "out.dex";
        File dexFile =  new File(filesDir, name);
        Log.i(TAG, "dexFile: " + dexFile.getAbsolutePath());
        if (dexFile.exists()) {
            dexFile.delete();
        }

        InputStream is = null;
        FileOutputStream os = null;
        try {
            // 获取 sd 卡下的 out.dex
            is = new FileInputStream(new File(Environment.getExternalStorageDirectory(), name));
            os = new FileOutputStream(dexFile);
            int len;
            byte[] buffer = new byte[1024];
            while ((len = is.read(buffer)) != -1) {
                os.write(buffer, 0, len);
            }

        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } finally {
            try {
                os.close();
                is.close();
            } catch (IOException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
        //调用修复
        FixManager.loadDex(this);
    }

loadDex:

    public static void loadDex(Context context){

        List<File> dexFiles = new ArrayList<>();
        File filesDir = context.getDir("dexs", Context.MODE_PRIVATE);

        // dex 缓存目录
        File dexCache = new File(filesDir, "cache");
        if (!dexCache.exists()) {
            dexCache.mkdirs();
        }

        File[] listFiles = filesDir.listFiles();
        for (File file : listFiles) {
            if(file.getName().endsWith(".dex")){
                Log.i("INFO", "dexName:"+file.getName());
                dexFiles.add(file);
            }
        }

        loadDex(dexFiles, dexCache);
    }

注:dex 的缓存目录不能跟 dex 的存储目录在同一个文件夹下。

4.在 Application 中修复

新建 App 继承 Application,在程序初始化的时候去加载所有已有的更新包。

App :

public class App extends Application {

    private static volatile App sigleton = null;

    public static App getInstance(){

        if (sigleton == null) {
            synchronized (App.class) {
                if (sigleton == null) {
                    sigleton = new App();
                }
            }
        }
        return sigleton;
    }
    @Override
    public void onCreate() {
        super.onCreate();
    }

    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);

        sigleton = this;
        FixManager.loadDex(this);
    }
}

5.生成修复包

运行程序,点击 “计算” 按钮,发生除 0 异常,程序奔溃。

修改 Test 中的代码,进行修复这个除 0 异常。

Test:

public class Test {

    public int test(){

        int a = 10;
        int b = 1;

        return a / b;
    }
}

点击 Rebuild Project ,对项目重新进行编译。

在这里插入图片描述

找到系统生成的 Test.class 文件。
在这里插入图片描述

按对应包名建立文件夹,把 Test.class 拷贝过来。
在这里插入图片描述

使用指令进行打包生成 dex 文件。

dx --dex --output C:\Users\ZX\Desktop\dex\out.dex C:\Users\ZX\Desktop\dex

在这里插入图片描述

6.修复

把生成的 out.dex 放到 sd 卡所在目录(模拟更新包下载),运行程序,点击“修复”,然后点击“计算”。

在这里插入图片描述

刚开始点击“计算”时候,运行出现除 0 异常,点击“修复”进行 dex 包的加载,并修复,然后就可以正常进行计算。

四、附

代码链接

评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值