Android Robust热修复方案实现原理浅析

前言

本文旨在通过分析源码一步步分析Robust热修复的实现原理,前半部分首先分析一下Robust思路中运用到的技术方案;后半部分多为源码部分,即Robust对于技术方案的实现与运用。

1、关于Robust

Robust is an Android HotFix solution with high compatibility and high stability. Robust can fix bugs immediately without a reboot.

2、简述Android APK生成原理

首先我们来看一下生成.apk文件时会经过的一些主要步骤:
  • 资源文件打包,并生成对应的R.java文件(用于项目中对于资源文件的映射)
  • 将aidl编译生成对应的java文件(Android中对于跨进程交互的一种方式)
  • 将上述两类和我们编写的源码.java文件通过javac编译成.class文件
  • 通过dx脚本将所有的.class文件打包成为一个.dex文件(dex文件为Android中虚拟机所需加载的文件格式)
  • 通过apkbuilder将dex文件打包生成.apk文件

3、热修复基本实现思路

  • source code中对每一个方法体内进行插桩
  • 加载补丁包时,查找到对应方法体及类,使用DexClassLoader加载补丁类实现代码修复
原始代码
public long getIndex() {
        return 100;
    }
插桩后方法体
public static ChangeQuickRedirect changeQuickRedirect;
    public long getIndex() {
        if(changeQuickRedirect != null) {
            //PatchProxy中封装了获取当前className和methodName的逻辑,并在其内部最终调用了changeQuickRedirect的对应函数
            if(PatchProxy.isSupport(new Object[0], this, changeQuickRedirect, false)) {
                return ((Long)PatchProxy.accessDispatch(new Object[0], false)).longValue();
            }
        }
        return 100L;
    }
Patch类
public class StatePatch implements ChangeQuickRedirect {
    @Override
    public Object accessDispatch(String methodSignature,Object[] paramArrayOfObject) {
        String[] signature = methodSignature.split(":");
        if (TextUtils.equals(signature[1],"a")) {//long getIndex() -> a
            return 106;
        }
        return null;
    }

    @Override
    public boolean isSupport(String methodSignature,"a")) {//long getIndex() -> a
            return true;
        }
        return false;
    }
}

A.代码插桩

ASM技术

首先,我们需要了解一个概念,ASM。ASM是一个Java字节码层面的代码分析及修改工具,它有一套非常易用的API,通过它可以实现对现有class文件的操纵,从而实现动态生成类,或者基于现有的类进行功能扩展。在Android的编译过程中,首先会将java文件编译为class文件,之后会将编译后的class文件打包为dex文件,我们可以利用class被打包为 dex 前的间隙,插入ASM相关的逻辑对class文件进行操纵。

Groovy Transform

Google在Gradle 1.5.0后提供了一个叫Transform的API,它的出现使得第三方的Gradle Plugin可以在打包dex之前对class文件进行进行一些操纵。我们本次就是要利用Transform API来实现这样一个Gradle Plugin

Transform具体操作的节点如上图所示,对于打包生成dex文件前的.class文件进行拦截,我们来看一看Transform为我们提供的可实现的方法:

表格中可见,getNamgetInputTypes等方法均为Transform的配置项,最后的transform方法需要重点关注,我们想要实现上面的拦截.class文件并进行代码插桩操作就需要在此方法中实现。其中着重看一下方法的型参inputs,通过inputs可以拿到所有的class文件。inputs中包括directoryInputsjarInputsdirectoryInputs为文件夹中的class文件,而jarInputs为jar包中的class文件。

B.加载补丁

DexClassLoader

关于Java中的ClassLoader,大家熟知的基础概念就是通过一个类的全名加载得到这个类的class对象,进而可以得到其实例对象。 在Android中的ClassLoader基本运作远离与Java中类似,不过开发者无法自己实现ClassLoader进行自定义操作,官方的api为我们提供了两个ClassLoader的子类,PathClassLoaderDexClassLoader。虽然两者继承于BaseDexClassLoaderBaseDexClassLoader继承于ClassLoader,但是前者只能加载已安装的Apk里面的dex文件,后者则支持加载apkdex以及jar,也可以从SD卡里面加载。 从上述的概念我们可以得知,想要实现一个热修复的方案,就需要依赖外部的dex文件,那么就需要使用 DexClassLoader 来帮助实现。 我们来看一下 DexClassLoader 的构造方法:

4、Robust的实现

A.代码插桩

这一节中,我们来看一下Robust的具体实现方案以及一些大致的接入流程。 上一节讲述了Robust热修复方案中需要运用到的技术,接下来我们来看一看Robust的具体代码逻辑,如何将上述的思路融汇。 首先看一下Robust为接入用户提供的一个配置相关的文件

<?xml version="1.0" encoding="utf-8"?>
<resources>

    <switch>
        <!--true代表打开Robust,请注意即使这个值为true,Robust也默认只在Release模式下开启-->
        <!--false代表关闭Robust,无论是Debug还是Release模式都不会运行robust-->
        <turnOnRobust>false</turnOnRobust>

        <!--是否开启手动模式,手动模式会去寻找配置项patchPackname包名下的所有类,自动的处理混淆,然后把patchPackname包名下的所有类制作成补丁-->
        <!--这个开关只是把配置项patchPackname包名下的所有类制作成补丁,适用于特殊情况,一般不会遇到-->
        <!--<manual>true</manual>-->
        <manual>false</manual>

        <!--是否强制插入插入代码,Robust默认在debug模式下是关闭的,开启这个选项为true会在debug下插入代码-->
        <!--但是当配置项turnOnRobust是false时,这个配置项不会生效-->
        <forceInsert>true</forceInsert>
        <!--<forceInsert>false</forceInsert>-->

        <!--是否捕获补丁中所有异常,建议上线的时候这个开关的值为true,测试的时候为false-->
        <catchReflectException>true</catchReflectException>
        <!--<catchReflectException>false</catchReflectException>-->

        <!--是否在补丁加上log,建议上线的时候这个开关的值为false,测试的时候为true-->
        <patchLog>true</patchLog>
        <!--<patchLog>false</patchLog>-->

        <!--项目是否支持progaurd-->
        <!--<proguard>true</proguard>-->
        <proguard>false</proguard>

        <!--项目是否支持ASM进行插桩,默认使用ASM,推荐使用ASM,Javaassist在容易和其他字节码工具相互干扰-->
        <useAsm>true</useAsm>
        <!--<useAsm>false</useAsm>-->
    </switch>

    <!--需要热补的包名或者类名,这些包名下的所有类都被会插入代码-->
    <!--这个配置项是各个APP需要自行配置,就是你们App里面你们自己代码的包名,
    这些包名下的类会被Robust插入代码,没有被Robust插入代码的类Robust是无法修复的-->
    <packname name="hotfixPackage">
        <name>operation.enmonster.com.gsoperation</name>
        <name>com.enmonster.lib.shop</name>
    </packname>

    <!--不需要Robust插入代码的包名,Robust库不需要插入代码,如下的配置项请保留,还可以根据各个APP的情况执行添加-->
    <exceptPackname name="exceptPackage">
    </exceptPackname>

    <!--补丁的包名,请保持和类PatchManipulateImp中fetchPatchList方法中设置的补丁类名保持一致( setPatchesInfoImplClassFullName("com.meituan.robust.patch.PatchesInfoImpl")),
    各个App可以独立定制,需要确保的是setPatchesInfoImplClassFullName设置的包名是如下的配置项,类名必须是:PatchesInfoImpl-->
    <patchPackname name="patchPackname">
        <name>com.meituan.robust.patch</name>
    </patchPackname>

    <!--自动化补丁中,不需要反射处理的类,这个配置项慎重选择-->
    <noNeedReflectClass name="classes no need to reflect">

    </noNeedReflectClass>
</resources>

接下来看一下Robust自己实现的Transform API内做了什么

class RobustTransform extends Transform implements Plugin<Project> {

    def robust
    InsertcodeStrategy insertcodeStrategy;

    @Override
    void apply(Project target) {
        ...
        initConfig()
        project.android.registerTransform(this)
    }

    def initConfig() {
        ...
        /*对文件进行解析*/
        for (name in robust.packname.name) {
            hotfixPackageList.add(name.text());
        }
        for (name in robust.exceptPackname.name) {
            exceptPackageList.add(name.text());
        }
        for (name in robust.hotfixMethod.name) {
            hotfixMethodList.add(name.text());
        }
        for (name in robust.exceptMethod.name) {
            exceptMethodList.add(name.text());
        }
        ...
    }

    @Override
    void transform(Context context, Collection<TransformInput> inputs, Collection<TransformInput> referencedInputs, TransformOutputProvider outputProvider, boolean isIncremental) throws IOException, TransformException, InterruptedException {
        ...
        def box = ConvertUtils.toCtClasses(inputs, classPool)
        ...
        insertcodeStrategy.insertCode(box, jarFile);
        writeMap2File(insertcodeStrategy.methodMap, Constants.METHOD_MAP_OUT_PATH)
        ...
    }

    private void writeMap2File(Map map, String path) {
        ...
    }
}

在初始化方法apply中,进行了配置文件的解析,就是上述.xml文件中关于Robust的一些配置项; 在transform中,通过inputs方法过滤筛选得到一个box变量,是一个类型为CtClass的集合;

CtClassJavassist框架中的类,Javassist是一个开源的分析、编辑和创建Java字节码的类库,可以直接编辑和生成Java生成的字节码。Java字节码存储在名叫class file的二进制文件里。每个class文件包含一个Java类或者接口。Javassit.CtClass是一个class文件的抽象表示。一个CtClass(compile-time class)对象可以用来处理一个class文件。

而后的代码插桩逻辑都放在了 insertcodeStrategy 方法内进行; 在代码插桩结束后,将名为 methodMap 的集合写入到文件 methodsMap.robust 中; methodsMap文件用于后续生成patch.jar文件时,查找方法映射。

@Override
protected void insertCode(List<CtClass> box, File jarFile) throws IOException, CannotCompileException {
    ZipOutputStream outStream = new JarOutputStream(new FileOutputStream(jarFile));
    //get every class in the box,ready to insert code
    for (CtClass ctClass : box) {
        //change modifier to public,so all the class in the apk will be public,you will be able to access it in the patch
        ctClass.setModifiers(AccessFlag.setPublic(ctClass.getModifiers()));
        if (isNeedInsertClass(ctClass.getName()) && !(ctClass.isInterface() || ctClass.getDeclaredMethods().length < 1)) {
            //only insert code into specific classes
            zipFile(transformCode(ctClass.toBytecode(), ctClass.getName().replaceAll("\\.", "/")), outStream, "/") + ".class");
        } else {
            zipFile(ctClass.toBytecode(), "/") + ".class");

        }
        ctClass.defrost();
    }
    outStream.close();
}

public byte[] transformCode(byte[] b1, String className) throws IOException {
    ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
    ClassReader cr = new ClassReader(b1);
    ClassNode classNode = new ClassNode();
    Map<String, Boolean> methodInstructionTypeMap = new HashMap<>();
    cr.accept(classNode, 0);
    final List<MethodNode> methods = classNode.methods;
    for (MethodNode m : methods) {
        InsnList inList = m.instructions;
        boolean isMethodInvoke = false;
        for (int i = 0; i < inList.size(); i++) {
            if (inList.get(i).getType() == AbstractInsnNode.METHOD_INSN) {
                isMethodInvoke = true;
            }
        }
        methodInstructionTypeMap.put(m.name + m.desc, isMethodInvoke);
    }
    InsertMethodBodyAdapter insertMethodBodyAdapter = new InsertMethodBodyAdapter(cw, className, methodInstructionTypeMap);
    cr.accept(insertMethodBodyAdapter, ClassReader.EXPAND_FRAMES);
    return cw.toByteArray();
}

该方法内遍历了上述操作中过滤得到的CtClass对象集合,首先通过 isNeedInsertClass 方法判断是否需要进行代码插桩,若需要则进入 transformCode 方法;

该方法内其中首先遍历方法节点集合,遍历过滤指令集类型,将method类型的指令集存入 methodInstructionTypeMap 中,该mapvalue值为该method是否有被调用,如果没有被调用的方法,value则为false。接下来的代码插桩操作具体实现在 InsertMethodBodyAdapter中进行;

InsertMethodBodyAdapter中主要执行了两部操作:

  • 在class对象中,注入一个名为 ChangeQuickRedirect 的成员变量
  • 在过滤后的方法体中,注入一段代码逻辑

至此,Robust的代码插桩实现完毕。

B.加载补丁

Robust关于DexClassLoader的具体运用在PatchExecutor类中

public class PatchExecutor extends Thread {

    @Override
    public void run() {
        //...
        //拉取补丁列表
        List<Patch> patches = fetchPatchList();
        //应用补丁列表
        applyPatchList(patches);
    }

    protected void applyPatchList(List<Patch> patches) {
        ...
        for (Patch p : patches) {
            if (p.isAppliedSuccess()) {
                continue;
            }
            if (patchManipulate.ensurePatchExist(p)) {
                boolean currentPatchResult = false;
                try {
                    currentPatchResult = patch(context, p);
                } catch (Throwable t) {
                    robustCallBack.exceptionNotify(t, "class:PatchExecutor method:applyPatchList line:69");
                }
            }
        }
    }

    protected boolean patch(Context context, Patch patch) {
        //...
        ClassLoader classLoader = new DexClassLoader(patch.getTempPath(), dexOutputDir.getAbsolutePath(),
                null, PatchExecutor.class.getClassLoader());

        //加载dex文件中的PatchesInfoImpl类文件
        Class patchesInfoClass = classLoader.loadClass(patch.getPatchesInfoImplClassFullName());
        PatchesInfo patchesInfo = (PatchesInfo) patchesInfoClass.newInstance();

        //拿到其中的本次需要的补丁类集合
        List<PatchedClassInfo> patchedClasses = patchesInfo.getPatchedClassesInfo();

        //...
        boolean isClassNotFoundException = false;
        Class patchClass, sourceClass;
        for (PatchedClassInfo patchedClassInfo : patchedClasses) {
            //目标类className
            String patchedClassName = patchedClassInfo.patchedClassName;
            //补丁类className
            String patchClassName = patchedClassInfo.patchClassName;
            try {
                sourceClass = classLoader.loadClass(patchedClassName.trim());
            } catch (ClassNotFoundException e) {
                isClassNotFoundException = true;
                continue;
            }

            Field[] fields = sourceClass.getDeclaredFields();
            //遍历目标类,找到其中的ChangeQuickRedirect字段
            Field changeQuickRedirectField = null;
            for (Field field : fields) {
                if (TextUtils.equals(field.getType().getCanonicalName(), ChangeQuickRedirect.class.getCanonicalName()) && TextUtils.equals(field.getDeclaringClass().getCanonicalName(), sourceClass.getCanonicalName())) {
                    changeQuickRedirectField = field;
                    break;
                }
            }
            if (changeQuickRedirectField == null) {
                robustCallBack.logNotify("changeQuickRedirectField  is null,patch info:" + "id = " + patch.getName() + ",md5 = " + patch.getMd5(), "class:PatchExecutor method:patch line:147");
                continue;
            }
            //通过反射,对ChangeQuickRedirect字段进行赋值
            patchClass = classLoader.loadClass(patchClassName);
            Object patchObject = patchClass.newInstance();
            changeQuickRedirectField.setAccessible(true);
            changeQuickRedirectField.set(null, patchObject);
        }
        if (isClassNotFoundException) {
            return false;
        }
        return true;
    }
}

上述代码中可以看到PatchExecutor继承了Thread接口,内部操作都是异步执行的;

入口方法中首先获取到开发者所配置的补丁文件位置等信息集合,接下来在 applyPatchList 方法中遍历补丁文件,依次执行 patch 方法;

该方法中首先通过补丁中的 getPatchesInfoImplClassFullName 获取到 PatchsInfoImpl 的类全名,并使用 DexClassLoader 加载并实例化;

在该类中通过 getPatchedClassesInfo 方法中可以拿到 PatchedClassInfo 对象的集合,此对象中包含本次需要修复的类信息以及修复使用的补丁类信息,分别对应变量 sourceClasspatchClass

首先加载 sourceClass 类,遍历该类中所有的字段,拿到前文中代码插桩环节中注入的 ChangeQuickRedirect 变量,再加载 patchClass ,将该对象赋值给 ChangeQuickRedirect 变量;

至此,加载补丁逻辑结束,需修复的补丁类中 ChangeQuickRedirect 被赋值,方法体内的插桩逻辑将会执行并修复原有逻辑。

结尾

谢谢大家看完,如有不恰当、不充分的地方,欢迎大家指正。针对这块知识点我在学习过程中,进行了详细的整理梳理了一些学习笔记,有需要参考学习的小伙伴可以 点击这里查看获取方式 传送门直达 !!!

原文地址:https://blog.csdn.net/weixin_61845324

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


更新Android SDK到3.0版本时,遇到Failed to rename directory E:\android\tools to E:\android\temp\ToolPackage.old01问题,导致无法更新,出现该问题的原因是由于3.0版本与较早的sdk版本之间文件结构有冲突,解决
Android 如何解决dialog弹出时无法捕捉Activity的back事件 在一些情况下,我们需要捕捉back键事件,然后在捕捉到的事件里写入我们需要进行的处理,通常可以采用下面三种办法捕捉到back事件: 1)重写onKeyDown或者onKeyUp方法 2)重写onBackPressed方
Android实现自定义带文字和图片的Button 在Android开发中经常会需要用到带文字和图片的button,下面来讲解一下常用的实现办法。一.用系统自带的Button实现 最简单的一种办法就是利用系统自带的Button来实现,这种方式代码量最小。在Button的属性中有一个是drawable
Android中的&quot;Unable to start activity ComponentInfo&quot;的错误 最近在做一款音乐播放器的时候,然后在调试的过程中发现一直报这个错误&quot;Unable to start activity ComponentInfo&quot;,从字面
Android 关于长按back键退出应用程序的实现最近在做一个Android上的应用,碰到一个问题就是如何实现长按back键退出应用程序。在网上查找了很多资料,发现几乎没有这样的实现,大部分在处理时是双击back键来退出应用程序。参考了一下双击back键退出应用程序的代码,网上主流的一种方法是下面
android自带的时间选择器只能精确到分,但是对于某些应用要求选择的时间精确到秒级,此时只有自定义去实现这样的时间选择器了。下面介绍一个可以精确到秒级的时间选择器。 先上效果图: 下面是工程目录: 这个控件我也是用的别人的,好像是一个老外写的,com.wheel中的WheelView是滑动控件的主
Android平台下利用zxing实现二维码开发 现在走在大街小巷都能看到二维码,而且最近由于项目需要,所以研究了下二维码开发的东西,开源的二维码扫描库主要有zxing和zbar,zbar在iPos平台上应用比较成熟,而在Android平台上主流还是用zxing库,因此这里主要讲述如何利用zxing
Android ListView的item背景色设置以及item点击无响应等相关问题 在Android开发中,listview控件是非常常用的控件,在大多数情况下,大家都会改掉listview的item默认的外观,下面讲解以下在使用listview时最常见的几个问题。1.如何改变item的背景色和按
如何向Android模拟器中导入含有中文名称的文件在进行Android开发的时候,如果需要向Android模拟器中导入文件进行测试,通过DDMS下手动导入或者在命令行下通过adb push命令是无法导入含有中文文件名的文件的。后来发现借用其他工具可以向模拟器中导入中文名称的文件,这个工具就是Ultr
Windows 下搭建Android开发环境一.下载并安装JDK版本要求JDK1.6+,下载JDK成功后进行安装,安装好后进行环境变量的配置【我的电脑】-——&gt;【属性】——&gt;【高级】 ——&gt;【环境变量】——&gt;【系统变量】中点击【新建】:变量名:CLASSPATH变量值:……
如何利用PopupWindow实现弹出菜单并解决焦点获取以及与软键盘冲突问题 在android中有时候可能要实现一个底部弹出菜单,此时可以考虑用PopupWindow来实现。下面就来介绍一下如何使用PopupWindow实现一个弹出窗。 主Activity代码:public void onCreat
解决Android中的ERROR: the user data image is used by another emulator. aborting的方法 今天调试代码的时候,突然出现这个错误,折腾了很久没有解决。最后在google上找到了大家给出的两种解决方案,下面给出这两种方法的链接博客:ht
AdvserView.java package com.earen.viewflipper; import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapFactory;
ImageView的scaleType的属性有好几种,分别是matrix(默认)、center、centerCrop、centerInside、fitCenter、fitEnd、fitStart、fitXY。 |值|说明| |:--:|:--| |center|保持原图的大小,显示在ImageVie
文章浏览阅读8.8k次,点赞9次,收藏20次。本文操作环境:win10/Android studio 3.21.环境配置 在SDK Tools里选择 CMAKE/LLDB/NDK点击OK 安装这些插件. 2.创建CMakeLists.txt文件 在Project 目录下,右键app,点击新建File文件,命名为CMakeLists.txt点击OK,创建完毕! 3.配置文件 在CMa..._link c++ project with gradle
文章浏览阅读1.2w次,点赞15次,收藏69次。实现目的:由mainActivity界面跳转到otherActivity界面1.写好两个layout文件,activity_main.xml和otherxml.xmlactivity_main.xml&lt;?xml version="1.0" encoding="utf-8"?&gt;&lt;RelativeLayout ="http://schemas..._android studio 界面跳转
文章浏览阅读3.8w次。前言:最近在找Android上的全局代理软件来用,然后发现了这两款神作,都是外国的软件,而且都是开源的软件,因此把源码下载了下来,给有需要研究代理这方面的童鞋看看。不得不说,国外的开源精神十分浓,大家相互使用当前基础的开源软件,然后组合成一个更大更强的大开源软件。好吧,废话不多说,下面简单介绍一下这两款开源项目。一、ProxyDroid:ProxyDroid功能比较强大,用到的技术也比较多,源码也_proxydroid
文章浏览阅读2.5w次,点赞17次,收藏6次。创建项目后,运行项目时Gradle Build 窗口却显示错误:程序包R不存在通常情况下是不会出现这个错误的。我是怎么遇到这个错误的呢?第一次创建项目,company Domain我使用的是:aven.com,但是创建过程在卡在了Building 'Calculator' Gradle Project info这个过程中,于是我选择了“Cancel”第二次创建项目,我还是使用相同的项目名称和项目路_r不存在
文章浏览阅读8.9w次,点赞4次,收藏43次。前言:在Android上使用系统自带的代理,限制灰常大,仅支持系统自带的浏览器。这样像QQ、飞信、微博等这些单独的App都不能使用系统的代理。如何让所有软件都能正常代理呢?ProxyDroid这个软件能帮你解决!使用方法及步骤如下:一、推荐从Google Play下载ProxyDroid,目前最新版本是v2.6.6。二、对ProxyDroid进行配置(基本配置:) (1) Auto S_proxydroid使用教程
文章浏览阅读1.1w次,点赞4次,收藏17次。Android Studio提供了一个很实用的工具Android设备监视器(Android device monitor),该监视器中最常用的一个工具就是DDMS(Dalvik Debug Monitor Service),是 Android 开发环境中的Dalvik虚拟机调试监控服务。可以进行的操作有:为测试设备截屏,查看特定进程中正在运行的线程以及堆栈信息、Logcat、广播状态信息、模拟电话_安卓摄像头调试工具