替换dex实现热修复

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/likuan0214/article/details/78712722

安卓App热补丁动态修复技术介绍

本文就是对上面的原理进行简单实现。
主要思想:
首先一个确保apk是由多个dex组成的,dex1、dex2、dex3等。
dex1一般会包含application等。

假如dex2中出现了bug,那么我们可以修复相应的bug,生成对应的newdex2,然后将newdex2放置到dexements数组的前面。

那么其他方法调用dex2中的方法时,会先从数组的由前往后遍历,如果在newdex2中找到了对应的方法,就不再向后读取dex2.

所以实现的关键在于:
1.apk多分包
2.修复bug后,生成patch.dex
3.将patch.dex插入到dex2前面

apk多分包

这里写图片描述

这里写图片描述

上面124步骤,即可实现多dex分包。

那么3是什么意思呢?
首先dex.keep

com.study.dexhotfix/MainActivity.class
com.study.dexhotfix/MyApplication.class

multiDexKeepFile file(‘dex.keep’)的作用就是将dex.keep定义的类放到第一个dex中

生成patch.dex

先看一下新建项目后的目录结构
这里写图片描述

TestBug.java
修复前

public class TestBug {
    public int calculate(){
        int i = 0;
        int j = 10;
        return j/i;
    }
}

修复后

public class TestBug {
    public int calculate(){
        int i = 1;
        int j = 10;
        return j/i;
    }
}

修复后,Build->Rebuild Project

然后在/Users/likuan/Desktop/ST/DexHotFix/app/build/intermediates/classes/debug/com/study/dexhotfix/TestBug.class中找到TestBug.class

在桌面新建一个文件夹bug,然后将TestBug.class拷贝到bug/com/study/dexhotfix文件下
这里写图片描述
bug文件下的路径,一定要和项目中的路径一致

打开终端,找到/Users/xxx/Library/Android/sdk/build-tools/26.0.2目录(其他也可以)
然后执行如下命令:

./dx --dex --output=/Users/xxx/Desktop/bug/patch.dex /Users/xxx/Desktop/bug

./dx –dex –output={生成的dex目录} {要加载的class文件目录,目录下可以有多个class文件}

然后在bug目录下生成:patch.dex,这就是我们后面要用到的补丁dex

将patch.dex插入到bugdex前面

关键都在BugFixUtils.java

package com.study.dexhotfix;

import android.content.Context;
import android.os.Environment;
import android.util.Log;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;

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

/**
 * Created by ygdx_lk on 17/12/4.
 */

public class BugFixUtils {
    public static final String DEX_DIR = "odex";
    public static final String dexName = "patch.dex";
    private static final String TAG = "BugFixUtils";
    public static void fixbug(Context context){
        String patchPath = Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + dexName;
        downLoadPatch(context, patchPath);
        loadPatch(context);
    }

    //加载补丁dex
    public static void loadPatch(Context context) {
        List<File> dexs = getPatchDexs(context);
        if(dexs != null && dexs.size() > 0){
            inject(context, dexs);
        }
    }

    //注入
    private static void inject(Context context, List<File> dexs) {
        Log.e(TAG, "inject: ");
        //dex存储目录
        File fileDir = context.getDir(DEX_DIR, Context.MODE_PRIVATE);

        //dex加载后的缓存目录
        String optimizeDir = fileDir.getAbsolutePath() + File.separator + "opt_dex";
        File optFile = new File(optimizeDir);
        if(!optFile.exists()){
            optFile.mkdirs();
        }

        //获取app的类加载器
        PathClassLoader pathClassLoader = (PathClassLoader) context.getClassLoader();
        for (File dex : dexs) {
            Log.i(TAG, "inject: 获取dex。。。" + dex.getAbsolutePath());
            //加载修复的dex文件
            //public DexClassLoader(
            //String dexPath, dex路径
            // String optimizedDirectory, dex加载的缓存路径
            // String librarySearchPath,
            // ClassLoader parent)
            DexClassLoader dexClassLoader = new DexClassLoader(dex.getAbsolutePath(), optFile.getAbsolutePath(), null, pathClassLoader);

            try {
                //获取DexClassLoader和PathClassLoader中的DexElements
                Object dexObj = getPathList(dexClassLoader);
                Object pathObj = getPathList(pathClassLoader);

                Object dexElements = getDexElements(dexObj);
                Object pathElements = getDexElements(pathObj);

                //合并
                Object mergeElements = combineArray(dexElements, pathElements);

                //将mergeElements覆盖pathClassLoader中的elements
                setField(pathObj,"dexElements", mergeElements);
                Log.e(TAG, "inject: merge");

            }catch (Exception e){
                Log.e(TAG, "inject: " + e.getMessage());
                e.printStackTrace();
            }
        }

    }

    //获取dalvik.system.BaseDexClassLoader中的DexPathList pathList
    private static Object getPathList(Object baseDexClassLoader) throws IllegalArgumentException, NoSuchFieldException, IllegalAccessException, ClassNotFoundException {
        return getField(baseDexClassLoader, Class.forName("dalvik.system.BaseDexClassLoader"), "pathList");
    }

    private static Object getField(Object obj, Class<?> cl, String field) throws NoSuchFieldException, IllegalArgumentException, IllegalAccessException {
        Field localField = cl.getDeclaredField(field);
        localField.setAccessible(true);
        return localField.get(obj);
    }

    //获取DexPathList中的Element[] dexElements
    private static Object getDexElements(Object paramObject) throws IllegalArgumentException, NoSuchFieldException, IllegalAccessException {
        return getField(paramObject, paramObject.getClass(), "dexElements");
    }

    private static void setField(Object obj, String field, Object value) throws NoSuchFieldException, IllegalArgumentException, IllegalAccessException {
        Class<?> cl = obj.getClass();
        Field localField = cl.getDeclaredField(field);
        localField.setAccessible(true);
        localField.set(obj, value);
    }

    /**
     *
     * @param arrayLhs 放到前面的数组
     * @param arrayRhs 放到后面的数组
     * @return
     */
    private static Object combineArray(Object arrayLhs, Object arrayRhs) {
        Class<?> localClass = arrayLhs.getClass().getComponentType();
        int i = Array.getLength(arrayLhs);
        int j = i + Array.getLength(arrayRhs);
        Log.i(TAG, "combineArray: i" + i + "  j" + (j - i));
        Object result = Array.newInstance(localClass, j);
        for (int k = 0; k < j; ++k) {
            if (k < i) {
                Array.set(result, k, Array.get(arrayLhs, k));
            } else {
                Array.set(result, k, Array.get(arrayRhs, k - i));
            }
        }
        return result;
    }

    //获取DEX_DIR下的补丁dexs
    private static List<File> getPatchDexs(Context context) {
        List<File> patchDexs = new ArrayList<>();
        File fileDir = context.getDir(DEX_DIR, Context.MODE_PRIVATE);
        if(fileDir != null && fileDir.exists()){
            File[] files = fileDir.listFiles();
            if(files != null){
                for (File file : files) {
                    if (file != null) {
                        String fileName = file.getName();
                        //判断是否是补丁dex(patch.dex)可能有多个
                        if(fileName.startsWith("patch") && fileName.endsWith(".dex")){
                            patchDexs.add(file);
                        }
                    }
                }
            }
        }
        return patchDexs;
    }

    //从服务器或本地下载补丁dex
    private static void downLoadPatch(Context context, String patchPath) {
        // /data/data/packagename/odex
        File fileDir = context.getDir(DEX_DIR, Context.MODE_PRIVATE);
        String filePath = fileDir.getAbsolutePath() + File.separator + dexName;
        File file = new File(filePath);
        if(file.exists()){
            file.delete();
        }
        //拷贝补丁到/data/data/packagename/odex下
        FileInputStream is = null;
        FileOutputStream os = null;
        try {
            is = new FileInputStream(patchPath);
            os = new FileOutputStream(file);
            int len = 0;
            byte[] buffer = new byte[1024];
            while ((len = is.read(buffer)) != -1){
                Log.e(TAG, "downLoadPatch: " + len);
                os.write(buffer, 0, len);
            }
            Log.e(TAG, "downLoadPatch: " + file.getAbsolutePath());
        }catch (Exception e){
            Log.e(TAG, "downLoadPatch: " + e.getMessage());
            e.printStackTrace();
        }finally {
            if(os != null){
                try {os.close();} catch (IOException e) {e.printStackTrace();}
                os = null;
            }

            if(is != null){
                try {is.close();} catch (IOException e) {e.printStackTrace();}
                is = null;
            }
        }
    }
}

然后在MyApplication.java中添加loadPatch方法,这样每次启动应用,都会加载之前的补丁

    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
        MultiDex.install(base);
        new BugFixUtils().loadPatch(base);
    }

MainActivity.java

public class MainActivity extends AppCompatActivity {

    private TextView tv_result;

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

    public void calculate(View view) {
        int result = new TestBug().calculate();
        tv_result.setText("计算结果:" + result);
    }

    public void fixbug(View view) {
        BugFixUtils.fixbug(this);
    }
}

测试

1.首先运行程序,点击计算执行calculate方法,报错。
2.然后,点击修复bug执行fixbug方法。
3.这时候点击calculate方法,依然会报错。
4.重启,点击计算calculate,发现计算结果为10,修复成功。

源码下载

猜你喜欢

转载自blog.csdn.net/likuan0214/article/details/78712722