Android 热修复

本文详细介绍了Android热修复的原理与实现,包括腾讯的Tinker、阿里的AndFix和美团的Robust等主流框架。热修复通过类加载、底层替换等方式实现,避免了因bug导致的APP重新发布。文章探讨了3种修复方式的优缺点,并阐述了热修复的流程,涉及ClassLoader、Dex动态加载等技术。此外,还展示了如何将dex和apk作为插件进行加载的示例代码。

1.Android热修复

热修复,就是对线上版本的静默更新。当APP发布上线之后,如果出现了严重的bug,通常需要重新发版来修复,但是重新走发布流程可能时间比较长,重新安装APP用户体验也不友好,所以出现了热修复,热修复就是通过发布一个插件,使APP运行的时候加载插件里面的代码,从而解决缺陷,并且对用户来说是无感的(有时候可能需要重启一下APP)。

热修复的实现方案,一种是类加载方案,即dex插桩,这种思路在插件化中也会用到;还有一种是底层替换方案,即修改替换ArtMethod。采用类加载方案的主要是以腾讯系为主,包括微信的Tinker、qq空间的QZone、美团的Robust、饿了么的Amigo;采用底层替换方案的主要是阿里系的AndFix等。

热修复包括3部分:开发端、服务端和用户端。在开发端,通过Gradle插件生成补丁包,并上传到云端,客户端通过判断是否需要下载新的补丁包,并执行热修复。

d26ebd7fe4fb4a939f695be26a7177da.png

目前主流的热修复框架有腾讯的Tinker、QZone,阿里的AndFix、Sophix,美团的Robust。他们采用的修复方式不同,比如AndFix和Robust采用native层hook Java层代码 bug fix,他们是即时生效的;而Tinker和QZone采用类替换,需要重启APP才能生效。

①阿里的AndFix(已经不再维护)

在native层动态替换java层的方法,通过native层hook java层的代码。通过在native层实现热修复,是不需要重启修复,这是即时生效的。

11bc0185c6e0469aa0ceb8c496f5597c.png

例如方法B中有bug,需要通过热修复替代这个方法。我们知道,所有方法的调用,都会在JVM中入栈,执行完成之后出栈,方法在JVM中是一个ArtMethod结构体,那么在JVM运行这个方法之前,在Native层完成这个方法的替换,那么就完成了热修复的工作,而且是即时生效的。这是基于方法进行修复的。

②美团的Robust

对每个函数都在编译打包阶段自动的插入一段代码,类似于代理,将方法执行的代码重定向到其他方法中。

Robust采用的技术是编译时字节码插桩技术,这个过程在gradle-plugin中发生,在编译打包阶段,对每个函数注入一段逻辑代码,通过判断是否执行插入的这段代码,这个过程也是即时生效的。

③腾讯的Tinker

Tinker采用的是Dex动态加载技术,通过反射的方式,将待修复的类放在dexElements数组的前面,在类加载的时候,首先加载这个待修复的类,因为类加载机制不会重复加载类,达到修复的目的。但这个方式是需要重启生效的(出现bug的类在ClassLoader中是不能替换的,存在缓存中,只能重启重新进行类加载)。

Tinker通过计算对比指定的Base Apk中的dex与修复后的Apk中的dex的区别,生成补丁包,所以补丁包中的内容即为两者差分的描述。运行时将Base Apk中的dex与补丁包进行合成,重启后加载全新的合成后的dex文件。

以上3种方式是目前热修复常见的3种方式,其实各有利弊,像native层处理需要大量的开发成本,跟Robust一样,只能达到修复bug的目的,不能新增类和轻量级的功能;而Tinker则是需要重启才能生效。

 

热修复用到的技术包括ClassLoader类加载机制、Dex动态加载技术 – hook反射、差分打包技术 – bsdiff、字节码插桩 – ASM或Javassist、Gradle插件 – 发布差分包、so库的编译。

 

2.热修复流程

根据类加载机制,可以知道热修复的原理就是将补丁包dex文件放到dexElements数组靠前的位置,这样在加载class时,优先找到补丁包中的dex文件,加载到class之后就不再寻找,从而原来apk里同名的类就不会再使用,达到修复的目的。

知道了原理,实现就很简单了,就是添加新的dex对象到当前app的classLoader对象(也就是BaseDexClassLoader)的pathList里面的dexElements。要添加就要先创建,先使用DexClassLoader加载插件,然后再生成插件的dexElements,最后再添加就好了。

①获取到当前应用的PathClassLoader;

②反射获取到DexPathList属性对象pathList;

③反射修改pathList的dexElement:

(1)把补丁包patch.dex转化为Element[](patch)

(2)获得pathList的dexElements属性(old)

(3)patch+dexElements合并,并反射赋值给patchList的dexElements

可以看到,整个过程大量运用了反射。反射是指在运行过程中,对于任意一个类,都能够知道这个类的所有属性和方法,对于任意一个对象,都能够调用他的任意一个方法和属性。

常用的两种方法是使用apk作为插件和使用dex文件作为插件。

下面的两个实现都是对程序中的一个方法进行了修改,然后分别打了 dex包和apk包,程序运行起来执行的方法就是插件里面的方法而不是程序本身的方法。

①dex插件

对于dex文件作为插件,和之前说的流程完全一致,先将修改后的类打包成dex包,再将dex进行加载,插入到dexElements集合的前面即可。而打包流程是先将.java文件编译成.class文件,然后使用SDK工具打包成dex文件并发布到远程服务端,然后APP端请求下载,下载完毕加载即可。

Android SDK提供了dx.bat工具将class文件转成dex文件,目录如下:

e44dacc110d34d9db25c25453692fa3e.png

1)编译class文件

编写修改好bug的.java文件,然后点击Make Project,就会在app/build/intermediates/javac/debug/classes/package name/目录下生成与.java文件对应的.class文件,将整个包路径还有修复好的class文件复制下来。

2)class转dex

把需要修复的java文件通过AS编译成class文件之后,再用sdk目录下的dx.bat工具将class文件转成dex文件。

把.class包放到cmd所对应的路径下,如果配置了环境变量就可以直接把包放到桌面。然后在cmd输入命令:

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

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

回车后 ,在.class所在目录下就生成了dex文件。

3)编写热修复工具类

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/包

    public static void loadFixedDex(Context context) {

        loadFixedDex(context, null);

    }

    //加载补丁包

    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.getN ame().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(), fopt.getAbsolutePath(), null, 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;

    }

}

 

 

 

 

到了这一步,已经打包好了dex文件,下面看一下具体的实现:

//在Application中进行替换

public class MApplication extends Application {

    @Override

    public void onCreate() {

        super.onCreate();

        //dex作为插件进行加载

        dexPlugin();

    }

    ...

  /*dex作为插件加载 */

    private void dexPlugin(){

        //插件包文件

        File file = new File( "/sdcard/FixDexTest.dex");

        if (!file.exists()) {

            Log.i("MApplication", "插件包不在");

            return;

        }

        try {

            //获取到 BaseDexClassLoader 的 pathList字段 private final DexPathList pathList;

            Field pathListField = BaseDexClassLoader.class.getDeclaredField("pathList");

            //破坏封装,设置为可以调用

            pathListField.setAccessible(true);

            //拿到当前ClassLoader的pathList对象

            Object pathListObj = pathListField.get( getClassLoader());

            //获取当前ClassLoader的pathList对象的字节码文件(DexPathList )

            Class<?> dexPathListClass = pathListObj.getClass();

            //拿到DexPathList 的 dexElements字段

            // private final Element[] dexElements;

            Field dexElementsField = dexPathListClass.getDeclaredField("dexElements");

            //破坏封装,设置为可以调用

            dexElementsField.setAccessible(true);

            //使用插件创建 ClassLoader

            DexClassLoader pathClassLoader = new DexClassLoader(file.getPath(), getCacheDir().getAbsolutePath(), null, getClassLoader());

            //拿到插件的DexClassLoader 的 pathList对象

            Object newPathListObj = pathListField.get(pathClassLoader);

            //拿到插件的pathList对象的 dexElements变量

            Object newDexElementsObj = dexElementsField.get(newPathListObj);

            //拿到当前的pathList对象的 dexElements变量

            Object dexElementsObj = dexElementsField.get(pathListObj);

            int oldLength = Array.getLength( dexElementsObj);

            int newLength = Array.getLength( newDexElementsObj);

            //创建一个dexElements对象

            Object concatDexElementsObject = Array.newInstance(dexElementsObj.getClass().getComponentType(), oldLength + newLength);

            //先添加新的dex添加到dexElement

            for (int i = 0; i < newLength; i++) {

                Array.set(concatDexElementsObject, i, Array.get(newDexElementsObj, i));

            }

            //再添加之前的dex添加到dexElement

            for (int i = 0; i < oldLength; i++) {

                Array.set(concatDexElementsObject, newLength + i, Array.get(dexElementsObj, i));

            }

            //将组建出来的对象设置给 当前ClassLoader的pathList对象

            dexElementsField.set(pathListObj, concatDexElementsObject);

        } catch (Exception e) {

            e.printStackTrace();

        }

    }

}

②apk插件

apk作为插件,就是重新打了一个新的apk包作为插件,打包很简单方便,缺点就是文件大。使用apk的话就没必要是将dex插入dexElements里面去,直接将之前的dexElements替换就可以了。

下面看一下apk插件的具体实现:

// apk作为插件加载

private void apkPlugin() {

    //插件包文件

    File file = new File("/sdcard/FixDexTest.apk");

    if (!file.exists()) {

        Log.i("MApplication", "插件包不在");

        return;

    }

    try {

        //获取到 BaseDexClassLoader 的 pathList字段 private final DexPathList pathList;

        Field pathListField = BaseDexClassLoader.class.getDeclaredField("pathList");

        //破坏封装,设置为可以调用

        pathListField.setAccessible(true);

        //拿到当前ClassLoader的pathList对象

        Object pathListObj = pathListField.get(getClassLoader());

        //获取当前ClassLoader的pathList对象的字节码文件(DexPathList )

        Class<?> dexPathListClass = pathListObj.getClass();

        //拿到DexPathList 的 dexElements字段

        // private final Element[] dexElements;

        Field dexElementsField = dexPathListClass.getDeclaredField("dexElements");

        //破坏封装,设置为可以调用

        dexElementsField.setAccessible(true);

        //使用插件创建 ClassLoader

        DexClassLoader pathClassLoader = new DexClassLoader(file.getPath(), getCacheDir().getAbsolutePath(), null, getClassLoader());

        //拿到插件的DexClassLoader 的 pathList对象

        Object newPathListObj = pathListField.get(pathClassLoader);

        //拿到插件的pathList对象的 dexElements变量

        Object newDexElementsObj = dexElementsField.get(newPathListObj);

        //将插件的 dexElements对象设置给 当前ClassLoader的pathList对象

        dexElementsField.set(pathListObj, newDexElementsObj);

    } catch (Exception e) {

        e.printStackTrace();

    }

}

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值