Android热补丁动态修复技术(二):实践

一、前言

上一篇博客中,我们简单的介绍了dex分包的原理,这节我们就通过代码实践的方式来阐述如何使用dex分包实现热修复的问题,要想实现我们需要面临解决两个问题。
1. 怎么将修复后的Bug类打包成dex
2. 怎么将外部的dex插入到ClassLoader中


(1)建立测试工程
这里写图片描述


(2)代码片段

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        this.findViewById(R.id.click).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                HelloHack hack = new HelloHack();
                Toast.makeText(MainActivity.this, hack.showHello(), Toast.LENGTH_SHORT).show();
            }
        });
    }

public class HelloHack {
    public String showHello() {
        return "hello world";
    }
}

布局很简单, 在activity_main中放了一个按钮,用来演示点击事件的触发,这里就不粘出来了.


(3)运行结果

这里写图片描述


假设这是我们公司的开发项目,刚刚上线就发现了严重bug,本来HelloHack应该返回”hello world fix by storm”,却返回了”hello world”
想修复bug,让用户再立刻更新一次显然很不友好,此时热补丁修复技术就派上用场了。


二.制作补丁
1. 首先我们将HelloHack类修复,然后重新编译项目。(Rebuild一下就行了)
2. 去保存项目的地方,将HelloHack.class文件拷贝出来,在这里
这里写图片描述


  1. 新建文件夹,要和HelloHack.class文件的包名一致(注意这里是根据包的层级结构建立对应的文件目录),然后将HelloHack.class复制到这里,如图
    这里写图片描述
    然后cd到makePatchJar目录,执行命令
    jar cvf fixbug.jar com/*
    这条命令就是把com下的所有文件打包到fixbug.jar文件中,最终会如图所示在当前目录生成fixbug.jar包.

4.接下需要把jar文件转换成dex文件: 在当前目录下执行
这里写图片描述
(前提是在环境变量中配置了dx命令,如何配置请自行百度,因为这是太基础的问题了,哈哈)。最后就会在当前目录下看的patch.jar文件.
这里写图片描述

tips:说下在执行过程中遇到的坑:
执行jar cvf fixbug.jar com/*
异常:bad class file magic (cafebabe) or version (0033.0000)
解决方法:在build.gradle中jdk的版本修改为1.6

compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_6
        targetCompatibility JavaVersion.VERSION_1_6
    }

导致这个问题的原因是由于Android项目属性中的Java Compiler设置为1.6版本的,而AndroidStudio中的被设置为1.7所致,也换成1.6就不会提示这个错误了.
大家看到了,上面的步骤有点繁琐,还需要手工操作很容易出现问题,其实我们完全可以使用gradle自动化生成dex文件,这需要对Groovy有一定功底的童鞋才可以做到 ,由于时间的原因后续我会把这部分加上.
三:加载补丁
3.1:思路
我们知道dex保存在这个位置
BaseDexClassLoader–>pathList–>dexElements
apk的classes.dex可以从应用本身的DexClassLoader中获取。 path_dex的dex需要new一个DexClassLoader加载后再获取。 分别通过反射取出dex文件,重新合并成一个数组,然后赋值给盈通本身的ClassLoader的dexElements(建议去看源码,加深理解,重点是pathClassLoader,DexClassLoader以及BaseDexClassLoader和DexPathList这几个文件,它们存在于davilk.system包下).
3.2:代码实现
加载外部dex,我们可以在Application中操作。
首先新建一个HotPatchApplication,然后在清单文件中配置,顺便加上读取sdcard的权限,因为补丁就保存在那里。了解了原理实现就比较简单了,我将代码分享给大家:

public class HotPatchApplication extends Application {

    @Override
    public void onCreate() {
        super.onCreate();
        initPathFromAssets(this, "patch.jar");
        }
/**
     * 从Assets里取出补丁,一般用于测试
     *
     * @param context
     * @param assetName
     */
    public static void initPathFromAssets(Context context, String assetName) {
        File dexDir = new File(context.getFilesDir(),
        PATCH_PATH);
        dexDir.mkdir();
        clearPaths(dexDir);
建议在实际开发中版本号不同时删除本地补丁,否则加载本地本地补丁而不是每次从网络读取
        String dexPath = null;
        try {
            dexPath = copyAsset(context, assetName, dexDir);
        } catch (IOException e) {
        } finally {
            if (dexPath != null && new File(dexPath).exists()) {
                inject(dexpath);
            }
        }
    }
    public static String copyAsset(Context context, String assetName, File dir) throws IOException {
        File outFile = new File(dir, assetName);
        if (!outFile.exists()) {
            AssetManager assetManager = context.getAssets();
            InputStream in = assetManager.open(assetName);
            OutputStream out = new FileOutputStream(outFile);
            copyFile(in, out);
            in.close();
            out.close();
        }
        return outFile.getAbsolutePath();
    }

    private static void copyFile(InputStream in, OutputStream out) throws IOException {
        byte[] buffer = new byte[1024];
        int read;
        while ((read = in.read(buffer)) != -1) {
            out.write(buffer, 0, read);
        }
    }

    /**
     * 要注入的dex的路径
     *
     * @param path
     */
    private void inject(String path) {
        try {
            // 获取classes的dexElements
            Class cl = Class.forName("dalvik.system.BaseDexClassLoader");
            Object pathList = getField(cl, "pathList", getClassLoader());
            Object baseElements = getField(pathList.getClass(), "dexElements", pathList);

            // 获取patch_dex的dexElements(需要先加载dex)
            String dexopt = getDir("dexopt", 0).getAbsolutePath();
            DexClassLoader dexClassLoader = new DexClassLoader(path, dexopt, dexopt, getClassLoader());
            Object obj = getField(cl, "pathList", dexClassLoader);
            Object dexElements = getField(obj.getClass(), "dexElements", obj);

            // 合并两个Elements
            Object combineElements = combineArray(dexElements, baseElements);

            // 将合并后的Element数组重新赋值给app的classLoader
            setField(pathList.getClass(), "dexElements", pathList, combineElements);

            //======== 以下是测试是否成功注入 =================
            Object object = getField(pathList.getClass(), "dexElements", pathList);
            int length = Array.getLength(object);
            Log.e("BugFixApplication", "length = " + length);

        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        }
    }

    /**
     * 通过反射获取对象的属性值
     */
    private Object getField(Class cl, String fieldName, Object object) throws NoSuchFieldException, IllegalAccessException {
        Field field = cl.getDeclaredField(fieldName);
        field.setAccessible(true);
        return field.get(object);
    }

    /**
     * 通过反射设置对象的属性值
     */
    private void setField(Class cl, String fieldName, Object object, Object value) throws NoSuchFieldException, IllegalAccessException {
        Field field = cl.getDeclaredField(fieldName);
        field.setAccessible(true);
        field.set(object, value);
    }

    /**
     * 通过反射合并两个数组
     */
    private Object combineArray(Object firstArr, Object secondArr) {
        int firstLength = Array.getLength(firstArr);
        int secondLength = Array.getLength(secondArr);
        int length = firstLength + secondLength;

        Class componentType = firstArr.getClass().getComponentType();
        Object newArr = Array.newInstance(componentType, length);
        for (int i = 0; i < length; i++) {
            if (i < firstLength) {
                Array.set(newArr, i, Array.get(firstArr, i));
            } else {
                Array.set(newArr, i, Array.get(secondArr, i - firstLength));
            }
        }
        return newArr;
    }
}

五、CLASS_ISPREVERIFIED
(这里说明下, 运行下demo是会报错的,但是在我的乐Max2,系统6.0.1的手机上却可以正常运行,我也没找到具体原因,

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值