Android 热修复

这篇具有很好参考价值的文章主要介绍了Android 热修复。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

1.Android热修复

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

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

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

Android 热修复

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

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

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

Android 热修复

例如方法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文件,目录如下:

Android 热修复

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;

    }

}文章来源地址https://www.toymoban.com/news/detail-400644.html

 

 

 

 

到了这一步,已经打包好了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();

    }

}

到了这里,关于Android 热修复的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • Android APK 签名打包原理分析(三)【静默安装的实现方案】

    小编目前从事的系统定制类工作,有客户提出了,需要后台“静默安装”他们的app,也就是悄无声息的安装,而且特别强调,不可以跳出任何安装引导页面,他们的app下载完成之后,后台调用公开的android install代码,系统就后台完成安装,安装完成之后,重新打开应用就可以

    2024年02月01日
    浏览(62)
  • 【干货】Android系统定制基础篇:第三部分(Android静默安装、Android支持usb打印机)

    一些产品要求APP在升级时能够实现静默安装,而无需弹出安装界面让用户确认。这里提出两种实现方案: 方案一 APP调用『pm』命令实现静默安装,此方案无须修改Android源码,但需要root权限。 方案二 修改Android PackageInstaller 源码,增加Intent参数来指定是否要静默安装,同时支

    2024年02月10日
    浏览(51)
  • 【MySQL8入门到精通】基础篇- Linux系统静默安装MySQL,跨版本升级

    📢📢📢📣📣📣 哈喽!大家好,我是【IT邦德】,江湖人称jeames007,10年DBA工作经验 一位上进心十足的【大数据领域博主】!😜😜😜 中国DBA联盟(ACDU)成员,目前从事DBA及程序编程 擅长主流数据Oracle、MySQL、PG 运维开发,备份恢复,安装迁移,性能优化、故障应急处理等。

    2023年04月09日
    浏览(51)
  • uniapp 之 小程序线上版本一直处于加载状态

    最开始小程序都是体验版的,后来应老大需求,把体验版提交审核为正式版本(线上版本), 原本以为版本审核得花费几天时间,没想到它这审核速度挺快的,不到3小时就审核通过了,审核通过后还要点击 提交发布  才能转为线上版本。 转为线上版本后就可以直接在搜索小

    2024年02月09日
    浏览(40)
  • 【Android】线上自助点餐系统

    【Android】线上自助点餐系统 效果图 商品数据 由于没有服务器,无法网络获取商品信息,故化繁从简,将需要的商品信息保存成xml文件,次项目将商品信息分为三个文件:标题文件,商品详细信息文件,商品图片文件。 商品标题文件 商品详细文件信息 由于篇幅过长,就没有

    2024年02月08日
    浏览(47)
  • 【android每日一问】怎么检测UI卡顿?(线上及线下)

    在Android系统中,我们知道UI线程负责我们所有视图的布局,渲染工作,UI在更新期间,如果UI线程的执行时间超过16ms,则会产生丢帧的现象,而大量的丢帧就会造成卡顿,影响用户体验。 在UI线程中做了大量的耗时操作,导致了UI刷新工作的阻塞。 系统CPU资源紧张,APP所能分

    2024年04月15日
    浏览(43)
  • 【android每日一问】怎么检测UI卡顿?(线上及线下)(1)

    msg.target.dispatchMessage(msg); if (logging != null) { logging.println(\\\" Finished to \\\" + msg.target + \\\" \\\" + msg.callback); } … } } 注意到两个很关键的地方是 logging.println(\\\" Dispatching to \\\" + msg.target + \\\" \\\" + msg.callback + \\\": \\\" + msg.what); 和 logging.println(\\\" Finished to \\\" + msg.target + \\\" \\\" + msg.callback); 这两行代码,它调用的

    2024年04月15日
    浏览(50)
  • 扫描二维码进入体验版小程序却一直进入线上版本?这个配置要注意!

    1. 前期准备 服务器 域名 2. 配置扫描普通二维码进入小程序页面 开发小程序过程中,我们会遇到很多不同的场景,其中,扫描普通二维码进入小程序就是其中之一。下面先来看下如何配置扫描普通二维码进入小程序,首先登录开发平台,打开开发管理,进入开发设置,如下图

    2024年02月15日
    浏览(37)
  • 微信小程序开发,小程序类目符合,线上版本无权限申请wx.getLocation接口

    我开发 的小程序类目符合wx.getLocation接口的申请标准      但是却还是显示无权限申请 后来研究好久才发现,小程序需要在发布线上版本时提交用户隐私保护指引  如未设置也可以在 设置-服务内容声明-用户隐私保护指引-声明处理用户信息项并补充填写后提交用户隐私协议

    2024年02月15日
    浏览(50)
  • OpenSSH 漏洞修复升级最新版本

    Centos7系统ssh默认版本一般是OpenSSH7.4左右,低版本是有漏洞的而且是高危漏洞,在软件交付和安全扫描上是过不了关的,一般情况需要升级OpenSSH的最新版本 今天详细说下升级最新版本的处理过程(认真看会发现操作很简单,因为写的操作很详细...) 现在绝大多数服务器的操

    2024年01月22日
    浏览(53)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包