前语
由于作业需求,我反编译了某个App的代码并分析某个事务功用的完结。然后,我注意到源码里几乎在每一个函数的最初都能看到类似这样的代码: 查阅了一些资料得知,这些是美团的热更新计划Robust主动生成的代码。这个项目已经开源到了github上(github.com/Meituan-Dia…),在里面咱们能够看到这样的介绍: Robust是一个有着高兼容性以及高安稳性的安卓热修正解决计划,它能够马上修正bug,甚至不需求从头启动运用
不需求从头启动运用就能够改动代码逻辑?出于猎奇,我去研究了一下这个结构的运用办法,并进一步探求了它的完结原理。
热更新计划产生布景
咱们都知道,客户端App有发版的概念,当线上呈现一些紧迫事故的时分,假设走正常的发版流程,让一切用户去从头下载装置一个问题已被修正的版别,那估计用户早都全跑完了。尤其是像美团这样的App,其庞大的用户量以及O2O交易场景的杂乱性决议了这个App的安稳性必须达到近乎苛刻的要求。然而,就算再完善的开发测验流程也不可能确保不会把Bug带到线上,于是乎,热更新计划由此应运而生。有了热更新,客户端App就能完结不经过发版就能够实时修正线上问题,同时也拥有用户无感知、节省用户流量、修正成功率高级长处。
Robust结构运用办法
个人认为,要探求一个结构的原理,首要要学会运用这个结构,调查其表现作用。这样到时分带着疑问去探求其代码完结时,从代码反推回现象,可能会更简略理解。假设想直接看原理的也能够直接移步下方Robust结构原理解析一节。
首要咱们新建一个简略的Demo工程,界面只有两个按钮,点击Jump按钮,会跳到另一个Activity,显现一行文字 error occur
假设咱们现在想要接入Robust结构,经过热补丁的方式来改动这行文字的显现,应该怎么做呢?
大致能够分为以下进程:
- 在app module的build.gradle,参加如下依赖:
- 在整个项目的build.gradle参加classpath:
- 在项目的 src 同级目录下装备robust.xml文件,详细项参阅github上面的robust.xml文件,咱们只将这两个地方修正成咱们自己工程的包名:
- 编写热修正相关代码 咱们来为Patch按钮添加热修正相关逻辑
PatchManipulateImp承继PatchManipulate,首要做一些拉取补丁以及校验文件的操作
RobustCallBackSample完结了RobustCallBack接口
PatchExecutor的run()办法
- 制造补丁
完结上述进程,咱们就算接入Robust热修正结构了。咱们把混杂功用翻开,打一个签名的release包, 装置到手机上,接下来就能够开端制造补丁。
在build目录下咱们能够看到主动生成了个robust文件夹,咱们在src的同级目录新建一个名为robust的文件夹,然后把mapping.txt跟methodsMap.robust这两个文件复制进去
然后apply插件,用于主动化生成补丁
然后开端修正咱们的代码,在修正的办法处加上注解 @Modify,若是新增办法则注解 @Add
接着从头打一次release包,会报错,但是只需呈现下方红框里的字样就说明补丁生成成功了
咱们把生成的补丁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 最要害的技能点其实是学习了AS 2.0的一个新特性Instant Run,其原理并不杂乱,能够简略描绘为两点:
- 打基础包时插桩,在每个类里添加一个类型为 ChangeQuickRedirect 静态变量,在每个办法前刺进一段相关的逻辑(假设这个静态变量是null,走正常逻辑,不然走补丁的修正逻辑)
- 加载补丁时,从补丁包中读取要替换的类及详细替换的办法完结,新建 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文件需求备份。
补丁加载进程分析
在介绍补丁加载进程之前,先介绍下一个补丁里需求有哪些文件:
- PatchesInfoImpl
这个类用于记录要修正的类以及其对应的 ChangeQuickRedirect 接口的完结,咱们反编译补丁包得出以下成果:
- xxxPatchControl
这品种是ChangeQuickRedirect 接口的详细完结,是一个代理,详细的替换办法是在 xxxPatch 类中
- xxxPatch
这种便是详细的替换了办法完结的类,依据咱们修正的类主动生成
补丁加载的进程能够描绘为:
在补丁下载成功以后,会开启一个子线程,经过指定的路径去读patch文件的jar包(patch文件能够为多个,每个patch文件对应一个 DexClassLoader 去加载),加载时经过反射得到PatchesInfoImpl class,并创建其目标,然后经过getPatchedClassesInfo()办法得到那些要修正的类,然后遍历其间的类信息,进而再经过反射修正其间 ChangeQuickRedirect 目标的值,修正为xxxPatchControl.java 这个class new 出来的目标。
咱们再反编译打出来的release包,能够看到这样的代码:
这样就能够解释为什么不需求重启运用就能够实时收效了:在补丁加载完以后,changeQuickRedirect 被赋值了不再是 null,这时假设判别到当时办法是要热补的办法(经过办法对应的编号来判别),isSupported 会返回 true,进而会走到 changeQuickRedirect 的 accessPatch 逻辑,然后走到secondActivityPatch 的 getTextInfo 逻辑,而这便是咱们自己写的修正后的逻辑。
附一张官方链接的图:
关于主动化补丁东西
其实在Robust推行的初期,补丁基本是手动生成,一个补丁的制造和测验经常需求一天的时刻,大大降低了系统对线上问题的反应速度,这也成为限制Robust热更新系统推行的一个要素。于是Robust团队经过不懈努力,开发了一个主动化补丁东西,研发只需求正常修正代码,然后参加一些注解,然后花一次打包的时刻就能够生成安稳可用的补丁。
主动化补丁东西首要做了以下作业:
- 读取被 @Add、@Modify、RobustModify.modify() 标注的办法或类并记录
- 解析 mapping 文件并记录每个类和类中办法混杂前后对应的信息
- 依据得到的信息,经过 generatePatch 办法实际生成补丁
- 将生成的补丁class打包成jar包
其间最要害的是第三步,由于要处理的代码风格悬殊,需求支撑Java的各种语法,而且还要处理各种由于Java编译器优化以及ProGuard的优化作业导致的问题,例如混杂类名、办法名、字段名,修正办法、字段的访问性,办法的内联,甚至是削减办法的参数(这就改动了办法签名)等等,这些都极大地增加了主动化补丁的难度。能够说,Robust最中心的作业都在这个主动化补丁东西上。
感兴趣能够看这两篇文章:
Android热更新计划Robust开源,新增主动化补丁东西
Android热补丁之Robust(二)主动化补丁原理解析
其他常见热修正结构
热修正结构按照原理大致能够分为三类:
- 基于 multidex 机制来干预 ClassLoader 加载 dex,例如微信的 Tinker
- 经过native层hook来完结办法的替换,例如阿里的 AndFix
- instant-run 插桩计划,例如美团的 Robust
Robust的优劣:
长处:
- 正常运用DexClassLoader,兼容性和安稳性更好
- 即时收效,不需求重启
- 支撑办法级别的修正,支撑静态办法
- 支撑新增办法和类
- 支撑主动化生成补丁,能够处理ProGuard优化(混杂、内联等)以及Java编译器优化(桥办法、lambda、内部类等)导致的各种问题
缺点:
- 暂时不支撑新增字段,但能够经过新增类解决
- 接入流程较杂乱
- 为每个函数都刺进了一段逻辑,会对运转效率、办法数、包体积仍是产生了一些副作用(从数据上看影响较小)
- 没有安全校验,需求开发者在加载补丁之前自己做验证
总的来说,Robust仍是一款有着高兼容性、高安稳性、高修正成功率的优异的热修正结构。
参阅文献
Instant Run 浅析
Android热补丁之Robust原理解析(一)
Android热更新计划Robust
【Android 热修正】美团Robust热修正结构原理解析