前语

由于作业需求,我反编译了某个App的代码并分析某个事务功用的完结。然后,我注意到源码里几乎在每一个函数的最初都能看到类似这样的代码:

美团热更新方案Robust介绍
查阅了一些资料得知,这些是美团的热更新计划Robust主动生成的代码。这个项目已经开源到了github上(github.com/Meituan-Dia…),在里面咱们能够看到这样的介绍: Robust是一个有着高兼容性以及高安稳性的安卓热修正解决计划,它能够马上修正bug,甚至不需求从头启动运用

不需求从头启动运用就能够改动代码逻辑?出于猎奇,我去研究了一下这个结构的运用办法,并进一步探求了它的完结原理。

热更新计划产生布景

咱们都知道,客户端App有发版的概念,当线上呈现一些紧迫事故的时分,假设走正常的发版流程,让一切用户去从头下载装置一个问题已被修正的版别,那估计用户早都全跑完了。尤其是像美团这样的App,其庞大的用户量以及O2O交易场景的杂乱性决议了这个App的安稳性必须达到近乎苛刻的要求。然而,就算再完善的开发测验流程也不可能确保不会把Bug带到线上,于是乎,热更新计划由此应运而生。有了热更新,客户端App就能完结不经过发版就能够实时修正线上问题,同时也拥有用户无感知、节省用户流量、修正成功率高级长处。

美团热更新方案Robust介绍

Robust结构运用办法

个人认为,要探求一个结构的原理,首要要学会运用这个结构,调查其表现作用。这样到时分带着疑问去探求其代码完结时,从代码反推回现象,可能会更简略理解。假设想直接看原理的也能够直接移步下方Robust结构原理解析一节。

首要咱们新建一个简略的Demo工程,界面只有两个按钮,点击Jump按钮,会跳到另一个Activity,显现一行文字 error occur

美团热更新方案Robust介绍

美团热更新方案Robust介绍

假设咱们现在想要接入Robust结构,经过热补丁的方式来改动这行文字的显现,应该怎么做呢?

大致能够分为以下进程:

  1. 在app module的build.gradle,参加如下依赖:

美团热更新方案Robust介绍

美团热更新方案Robust介绍

  1. 在整个项目的build.gradle参加classpath:

美团热更新方案Robust介绍

  1. 在项目的 src 同级目录下装备robust.xml文件,详细项参阅github上面的robust.xml文件,咱们只将这两个地方修正成咱们自己工程的包名:

美团热更新方案Robust介绍

  1. 编写热修正相关代码 咱们来为Patch按钮添加热修正相关逻辑

美团热更新方案Robust介绍

美团热更新方案Robust介绍

PatchManipulateImp承继PatchManipulate,首要做一些拉取补丁以及校验文件的操作

美团热更新方案Robust介绍

RobustCallBackSample完结了RobustCallBack接口

美团热更新方案Robust介绍

PatchExecutor的run()办法

美团热更新方案Robust介绍

  1. 制造补丁

完结上述进程,咱们就算接入Robust热修正结构了。咱们把混杂功用翻开,打一个签名的release包, 装置到手机上,接下来就能够开端制造补丁。

在build目录下咱们能够看到主动生成了个robust文件夹,咱们在src的同级目录新建一个名为robust的文件夹,然后把mapping.txtmethodsMap.robust这两个文件复制进去

美团热更新方案Robust介绍

美团热更新方案Robust介绍

然后apply插件,用于主动化生成补丁

美团热更新方案Robust介绍

然后开端修正咱们的代码,在修正的办法处加上注解 @Modify,若是新增办法则注解 @Add

美团热更新方案Robust介绍

接着从头打一次release包,会报错,但是只需呈现下方红框里的字样就说明补丁生成成功了

美团热更新方案Robust介绍

咱们把生成的补丁push到之前在PatchManipulateImp的fetchPatchList办法里指定好的目录,模拟补丁推送成功的进程

adb push ./app/build/outputs/robust/patch.jar /sdcard/robust/patch.jar

这时分咱们翻开app,点击JUMP_SECOND_ACTIVITY,界面显现error occur,咱们点击PATCH按钮,然后再点击JUMP_SECOND_ACTIVITY,就会发现此时界面上显现的内容变成了error fixed!

调查log也能够看到补丁是加载成功了的

美团热更新方案Robust介绍

Robust结构原理解析

Robust 最要害的技能点其实是学习了AS 2.0的一个新特性Instant Run,其原理并不杂乱,能够简略描绘为两点:

  1. 打基础包时插桩,在每个类里添加一个类型为 ChangeQuickRedirect 静态变量,在每个办法前刺进一段相关的逻辑(假设这个静态变量是null,走正常逻辑,不然走补丁的修正逻辑)
  2. 加载补丁时,从补丁包中读取要替换的类及详细替换的办法完结,新建 ClassLoader 加载补丁dex

插桩进程分析

类似 InstantRun , Robust 也是运用 Transform API 修正字节码文件,该 API 答应第三方插件在 .class 文件打包为 dex 文件之前操作编译好的 .class 字节码文件。

咱们看下‘robust’这个gradle插件的相关完结:

class RobustTransform extends Transform implements Plugin<Project> {
    ...
    @Override
    void apply(Project target) {
        //解析项目下robust.xml装备文件
        robust = new XmlSlurper().parse(new File("${project.projectDir}/${Constants.ROBUST_XML}"))
        ...
        project.android.registerTransform(this)
        project.afterEvaluate(new RobustApkHashAction())
    }
    @Override
    void transform(Context context, Collection<TransformInput> inputs, Collection<TransformInput> referencedInputs, TransformOutputProvider outputProvider, boolean isIncremental) throws IOException, TransformException, InterruptedException {
        ...
        ClassPool classPool = new ClassPool()
        project.android.bootClasspath.each {
            logger.debug "android.bootClasspath   " + (String) it.absolutePath
            classPool.appendClassPath((String) it.absolutePath)
        }
        ...
        def box = ConvertUtils.toCtClasses(inputs, classPool)
        insertRobustCode(box, jarFile)
        writeMap2File(methodMap, Constants.METHOD_MAP_OUT_PATH)
        ...
    }
}

首要读取 robust.xml 装备文件并初始化,可装备选项首要包含:

  • 一些开关选项
  • 需求热补的包名或许类名,这些包名下的一切类都被会刺进代码
  • 不需求热补的包名或许类名,能够除掉指定的类或许包

然后经过 Transform API 调用 transform() 办法,扫描一切类参加到 classPool 中,调用 insertRobustCode() 办法,这个办法做了以下作业:

  • 将class设置为public
  • 过滤掉不需求插桩的类跟办法,包含:接口、无办法类、robust.xml文件中装备的不需求热补的类,以及构造办法、抽象办法、native办法、synthetic办法等
  • 在类中刺进 public static ChangeQuickRedirect changeQuickRedirect这个静态变量
if (!addIncrementalChange) {
    //刺进 public static ChangeQuickRedirect changeQuickRedirect;
    addIncrementalChange = true;
    ClassPool classPool = it.declaringClass.classPool
    CtClass type = classPool.getOrNull(Constants.INTERFACE_NAME);
    CtField ctField = new CtField(type, Constants.INSERT_FIELD_NAME, ctClass);
    ctField.setModifiers(AccessFlag.PUBLIC | AccessFlag.STATIC)
    ctClass.addField(ctField)
    logger.debug "ctClass: " + ctClass.getName();
}
  • 在办法中刺进逻辑代码段
if (ctBehavior.getMethodInfo().isMethod()) {
    boolean isStatic = ctBehavior.getModifiers() & AccessFlag.STATIC;
    CtClass returnType = ctBehavior.getReturnType0();
    String returnTypeString = returnType.getName();
    def body = "if (${Constants.INSERT_FIELD_NAME} != null) {"
    body += "Object argThis = null;"
    if (!isStatic) {
        body += "argThis = $0;"
    }
    body += "   if (com.meituan.robust.PatchProxy.isSupport($args, argThis, ${Constants.INSERT_FIELD_NAME}, $isStatic, " + methodMap.get(ctBehavior.longName) + ")) {"
    body += getReturnStatement(returnTypeString, isStatic, methodMap.get(ctBehavior.longName));
    body += "   }"
    body += "}"
    ctBehavior.insertBefore(body);
}
  • 经过 zipFile() 办法写回class文件

最后调用 writeMap2File() 办法将插桩的办法信息写入 robust/methodsMap.robust 文件中,此文件和混杂的mapping文件需求备份。

补丁加载进程分析

在介绍补丁加载进程之前,先介绍下一个补丁里需求有哪些文件:

  1. PatchesInfoImpl

这个类用于记录要修正的类以及其对应的 ChangeQuickRedirect 接口的完结,咱们反编译补丁包得出以下成果:

美团热更新方案Robust介绍

  1. xxxPatchControl

这品种是ChangeQuickRedirect 接口的详细完结,是一个代理,详细的替换办法是在 xxxPatch 类中

美团热更新方案Robust介绍

  1. xxxPatch

这种便是详细的替换了办法完结的类,依据咱们修正的类主动生成

补丁加载的进程能够描绘为:

在补丁下载成功以后,会开启一个子线程,经过指定的路径去读patch文件的jar包(patch文件能够为多个,每个patch文件对应一个 DexClassLoader 去加载),加载时经过反射得到PatchesInfoImpl class,并创建其目标,然后经过getPatchedClassesInfo()办法得到那些要修正的类,然后遍历其间的类信息,进而再经过反射修正其间 ChangeQuickRedirect 目标的值,修正为xxxPatchControl.java 这个class new 出来的目标。

咱们再反编译打出来的release包,能够看到这样的代码:

美团热更新方案Robust介绍

这样就能够解释为什么不需求重启运用就能够实时收效了:在补丁加载完以后,changeQuickRedirect 被赋值了不再是 null,这时假设判别到当时办法是要热补的办法(经过办法对应的编号来判别),isSupported 会返回 true,进而会走到 changeQuickRedirect 的 accessPatch 逻辑,然后走到secondActivityPatch 的 getTextInfo 逻辑,而这便是咱们自己写的修正后的逻辑。

附一张官方链接的图:

美团热更新方案Robust介绍

关于主动化补丁东西

其实在Robust推行的初期,补丁基本是手动生成,一个补丁的制造和测验经常需求一天的时刻,大大降低了系统对线上问题的反应速度,这也成为限制Robust热更新系统推行的一个要素。于是Robust团队经过不懈努力,开发了一个主动化补丁东西,研发只需求正常修正代码,然后参加一些注解,然后花一次打包的时刻就能够生成安稳可用的补丁。

主动化补丁东西首要做了以下作业:

  1. 读取被 @Add、@Modify、RobustModify.modify() 标注的办法或类并记录
  2. 解析 mapping 文件并记录每个类和类中办法混杂前后对应的信息
  3. 依据得到的信息,经过 generatePatch 办法实际生成补丁
  4. 将生成的补丁class打包成jar包

其间最要害的是第三步,由于要处理的代码风格悬殊,需求支撑Java的各种语法,而且还要处理各种由于Java编译器优化以及ProGuard的优化作业导致的问题,例如混杂类名、办法名、字段名,修正办法、字段的访问性,办法的内联,甚至是削减办法的参数(这就改动了办法签名)等等,这些都极大地增加了主动化补丁的难度。能够说,Robust最中心的作业都在这个主动化补丁东西上。

感兴趣能够看这两篇文章:

Android热更新计划Robust开源,新增主动化补丁东西

Android热补丁之Robust(二)主动化补丁原理解析

其他常见热修正结构

热修正结构按照原理大致能够分为三类:

  1. 基于 multidex 机制来干预 ClassLoader 加载 dex,例如微信的 Tinker
  2. 经过native层hook来完结办法的替换,例如阿里的 AndFix
  3. instant-run 插桩计划,例如美团的 Robust

美团热更新方案Robust介绍

Robust的优劣:

长处:

  • 正常运用DexClassLoader,兼容性和安稳性更好
  • 即时收效,不需求重启
  • 支撑办法级别的修正,支撑静态办法
  • 支撑新增办法和类
  • 支撑主动化生成补丁,能够处理ProGuard优化(混杂、内联等)以及Java编译器优化(桥办法、lambda、内部类等)导致的各种问题

缺点:

  • 暂时不支撑新增字段,但能够经过新增类解决
  • 接入流程较杂乱
  • 为每个函数都刺进了一段逻辑,会对运转效率、办法数、包体积仍是产生了一些副作用(从数据上看影响较小)
  • 没有安全校验,需求开发者在加载补丁之前自己做验证

总的来说,Robust仍是一款有着高兼容性、高安稳性、高修正成功率的优异的热修正结构。

参阅文献

Instant Run 浅析

Android热补丁之Robust原理解析(一)

Android热更新计划Robust

【Android 热修正】美团Robust热修正结构原理解析