Android—热修复实践

1.编译class文件

点Make Project

 将整个包路径还有修复好的class文件复制下来。

2.class转dex

我们把自己需要修复的java文件通过AS编译成class文件之后

 再用sdk目录下的dx.bat工具将class文件转成dex文件。

打开cmd,如果你设置了环境变量,可以直接在c盘调用语句,不然你就需要把路径切换到跟dx.bat一样。

比如我上面就是cd C:\Users\*****\AppData\Local\Android\Sdk\build-tools\30.0.1

把你的包放到cmd所对应的路径下,如果配置了环境变量就可以直接把包放到桌面。

 

 回车后 

dx --dex --output = com\example\hotfixdemo\classes2.dex com\example\hotfixdemo\Text.class

该命令前面对应的是生成的dex文件放置路径(相对于你现在cmd的路径)+文件名,后面的就是class文件所对应的路径(相对于你现在cmd的路径)

3.编写热修复工具类

package com.example.hotfixdemo;

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 FixDexUtils {

    private static final String DEX_SUFFIX = ".dex";
    private static final String APK_SUFFIX = ".apk";
    private static final String JAR_SUFFIX = ".jar";
    private static final String ZIP_SUFFIX = ".zip";
    private static final HashSet<File> loadedDex = new HashSet<File>();

    /**
     * 加载补丁,使用默认目录:data/data/包名/files/odex
     *
     * @param context
     */
    public static void loadFixedDex(Context context) {
        loadFixedDex(context, null);
    }

    /**
     * 加载补丁
     *
     * @param context       上下文
     * @param patchFilesDir 补丁所在目录
     */
    public static void loadFixedDex(Context context, File patchFilesDir) {
        if (context == null) {
            return;
        }
        // 遍历所有的修复dex
        File fileDir = patchFilesDir != null ? patchFilesDir : new File(context.getExternalCacheDir().getAbsolutePath());// data/data/包名/cache(这个可以任意位置)
        File[] listFiles = fileDir.listFiles();
        for (File file : listFiles) {
            if (file.getName().startsWith("classes") &&
                    (file.getName().endsWith(DEX_SUFFIX)
                            || file.getName().endsWith(APK_SUFFIX)
                            || file.getName().endsWith(JAR_SUFFIX)
                            || file.getName().endsWith(ZIP_SUFFIX))) {
                loadedDex.add(file);// 存入集合
            }
        }
        // dex合并之前的dex
        doDexInject(context);
    }

    private static void doDexInject(Context appContext) {
        String optimizeDir = appContext.getFilesDir().getAbsolutePath();// data/data/包名/files (这个必须是自己程序下的目录)
        File fopt = new File(optimizeDir);
        if (!fopt.exists()) {
            fopt.mkdirs();
        }
        try {
            // 1.加载应用程序的dex
            PathClassLoader pathLoader = (PathClassLoader) appContext.getClassLoader();
            for (File dex : FixDexUtils.loadedDex) {
                // 2.加载指定的修复的dex文件
                DexClassLoader dexLoader = new DexClassLoader(
                        dex.getAbsolutePath(),// 修复好的dex(补丁)所在目录
                        fopt.getAbsolutePath(),// 存放dex的解压目录(用于jar、zip、apk格式的补丁)
                        null,// 加载dex时需要的库
                        pathLoader// 父类加载器
                );
                // 3.合并
                Object dexPathList = getPathList(dexLoader);
                Object pathPathList = getPathList(pathLoader);
                Object leftDexElements = getDexElements(dexPathList);
                Object rightDexElements = getDexElements(pathPathList);
                // 合并完成
                Object dexElements = combineArray(leftDexElements, rightDexElements);
                // 重写给PathList里面的Element[] dexElements;赋值
                Object pathList = getPathList(pathLoader);// 一定要重新获取,不要用pathPathList,会报错
                setField(pathList, pathList.getClass(), dexElements);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 反射给对象中的属性重新赋值
     */
    private static void setField(Object obj, Class<?> cl, Object value) throws NoSuchFieldException, IllegalAccessException {
        Field declaredField = cl.getDeclaredField("dexElements");
        declaredField.setAccessible(true);
        declaredField.set(obj, value);
    }

    /**
     * 反射得到对象中的属性值
     */
    private static Object getField(Object obj, Class<?> cl, String field) throws NoSuchFieldException, IllegalAccessException {
        Field localField = cl.getDeclaredField(field);
        localField.setAccessible(true);
        return localField.get(obj);
    }


    /**
     * 反射得到类加载器中的pathList对象
     */
    private static Object getPathList(Object baseDexClassLoader) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
        return getField(baseDexClassLoader, Class.forName("dalvik.system.BaseDexClassLoader"), "pathList");
    }

    /**
     * 反射得到pathList中的dexElements
     */
    private static Object getDexElements(Object pathList) throws NoSuchFieldException, IllegalAccessException {
        return getField(pathList, pathList.getClass(), "dexElements");
    }

    /**
     * 数组合并
     */
    private static Object combineArray(Object left, Object right) {
        Class<?> componentType = left.getClass().getComponentType();
        int i = Array.getLength(left);// 得到左数组长度(补丁数组)
        int j = Array.getLength(right);// 得到原dex数组长度
        int k = i + j;// 得到总数组长度(补丁数组+原dex数组)
        Object result = Array.newInstance(componentType, k);// 创建一个类型为componentType,长度为k的新数组
        System.arraycopy(left, 0, result, 0, i);
        System.arraycopy(right, 0, result, i, j);
        return result;
    }
}

 4.测试

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        TextView tv1 = findViewById(R.id.tv1);
        TextView tv2 = findViewById(R.id.tv2);
        tv1.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                FixDexUtils.loadFixedDex(MainActivity.this);
            }
        });
        tv2.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Toast.makeText(MainActivity.this, Text.getString(),Toast.LENGTH_SHORT).show();
            }
        });
    }
}

直接看代码注释,这里我们把修复好的文件放在了外部存储的缓存文件夹中,我们可以调试手机直接把文件放进去

加载到我们的dex文件后他会存放到我们指定的另一个内部存储文件夹中。

我们主要修复的是Test类。

修复前:

public class Text {
    public static String getString(){
        return "出错了!";
    }
}

修复后:

public class Text {
    public static String getString(){
        return "修复了";
    }
}

这里需要注意你所修复的类被加载的时机,如果你这里修复的是MainActivity,该activity已经被加载了你再去修复是没有用的,因为该类已经被加载成一个对象存在内存中。这里我们是点了tv2才会去加载Test类。

类被加载时机:

  1. 定义了main的类,启动main方法时该类会被加载
  2. 创建类或子类的实例,即new对象的时候
  3. 访问类的静态方法
  4. 访问类的静态变量
  5. 反射 Class.forName()

如果我们先点击tv1再点tv2此时就会显示修复了,如果先点tv2则一直显示是出错了。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值