理解 Robust

Robust,美团开源的新一代热更新系统,对 Android 版本无差别兼容,无需发版就可以做到随时修改线上 bug,快速对重大线上问题作出反应。Robust 热更新系统借鉴 Instant Run 原理,实现了一个兼容性更强而且实时生效的热更新方案。其基本思路是,Robust 热更新系统在一个方法的入口处插入一段跳转代码,当发现某个方法出现 bug 就跳转执行补丁中的代码,略过原有代码的执行,否则执行原有方法体逻辑

优缺点

  • 支持Android2.3-10版本
  • 高兼容性、高稳定性,修复成功率高达99.9%
  • 补丁实时生效,不需要重新启动
  • 支持方法级别的修复,包括静态方法
  • 支持增加方法和类
  • 支持ProGuard的混淆、内联、优化等操作

需要保存打包时生成的mapping文件以及build/outputs/robust/methodsMap.robust文件

当然,这套方案虽然对开发者是透明的,但毕竟在编译阶段有插件侵入了产品代码,对运行效率、方法数、包体积还是产生了一些副作用。

原理

Robust插件对每个产品代码的每个函数都在编译打包阶段自动的插入了一段代码,插入过程对业务开发是完全透明。如State.java的getIndex函数:

1
2
3
public long getIndex() {
return 100;
}

被处理成如下的实现:

1
2
3
4
5
6
7
8
9
10
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], this, changeQuickRedirect, false)).longValue();
}
}
return 100L;
}

可以看到Robust为每个class增加了个类型为ChangeQuickRedirect的静态成员,而在每个方法前都插入了使用changeQuickRedirect相关的逻辑,当 changeQuickRedirect不为null时,可能会执行到accessDispatch从而替换掉之前老的逻辑,达到fix的目的。 如果需将getIndex函数的返回值改为return 106,那么对应生成的patch,主要包含两个class:PatchesInfoImpl.java和StatePatch.java。 PatchesInfoImpl.java:

1
2
3
4
5
6
7
8
public class PatchesInfoImpl implements PatchesInfo {
public List<PatchedClassInfo> getPatchedClassesInfo() {
List<PatchedClassInfo> patchedClassesInfos = new ArrayList<PatchedClassInfo>();
PatchedClassInfo patchedClass = new PatchedClassInfo("com.meituan.sample.d", StatePatch.class.getCanonicalName());
patchedClassesInfos.add(patchedClass);
return patchedClassesInfos;
}
}

StatePatch.java:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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, Object[] paramArrayOfObject) {
String[] signature = methodSignature.split(":");
if (TextUtils.equals(signature[1], "a")) {//long getIndex() -> a
return true;
}
return false;
}
}

客户端拿到含有PatchesInfoImpl.java和StatePatch.java的patch.dex后,用DexClassLoader加载patch.dex,反射拿到PatchesInfoImpl.java这个class。拿到后,创建这个class的一个对象。然后通过这个对象的getPatchedClassesInfo函数,知道需要patch的class为com.meituan.sample.d(com.meituan.sample.State混淆后的名字),再反射得到当前运行环境中的com.meituan.sample.d class,将其中的changeQuickRedirect字段赋值为用patch.dex中的StatePatch.java这个class new出来的对象。这就是打patch的主要过程。通过原理分析,其实Robust只是在正常的使用DexClassLoader,所以可以说这套框架是没有兼容性问题的。

大体流程如下:

方法数

使用了Robust插件后,会导致方法数增加,原来能被ProGuard内联的函数不能被内联了。看了下ProGuard的Optimizer.java的相关片段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
if (methodInliningUnique) {
// Inline methods that are only invoked once.
programClassPool.classesAccept(
new AllMethodVisitor(
new AllAttributeVisitor(
new MethodInliner(configuration.microEdition,
configuration.allowAccessModification,
true,
methodInliningUniqueCounter))));
}
if (methodInliningShort) {
// Inline short methods.
programClassPool.classesAccept(
new AllMethodVisitor(
new AllAttributeVisitor(
new MethodInliner(configuration.microEdition,
configuration.allowAccessModification,
false,
methodInliningShortCounter))));
}

通过注释可以看出,如果只被调用一次或者足够小的函数,都可能被内联。深入分析代码,我们发现确实如此,只被调用了一次的私有函数、只有一行函数体的函数(比如get、set函数等)都极可能内联。 其实仔细思考下,那些可能被内联的只有一行函数体的函数,真的有被插件处理的必要吗?别说一行代码的函数出问题的可能性小,就算出问题了也可以通过patch内联它的那个函数来解决问题,或者patch这一行代码调用的那个函数。只调用了一次的函数其实是一样的。所以通过分析,这样的函数其实是可以不被插件处理的。那么有了这个认识,我们对插件做了处理函数的判断,跳过被ProGuard内联可能性比较大的函数。通过对打出来apk中的dex做分析,发现优化后的插件还是影响了内联效果,不过只导致方法数增加了不到1000个,所以算是临时简单的解决了这个问题。

补丁问题

要制作出补丁,我们可能会面临如下两个问题: 1. 如何解决混淆问题? 2. 被补的函数中使用了super相关的调用怎么办?

混淆问题

混淆的问题比较好处理。先针对混淆前的代码生成patch.class,然后利用生成release包时对应的mapping文件中的class的映射关系,对patch.class做字符串上的处理,让它使用线上运行环境中混淆的class。

super相关的调用

比如某个Activity的onCreate方法中需要调用super.onCreate,而现在这个bad.Class的badMethod就是这个Activity的onCreate方法,那么在patched.class的patchedMethod中如何通过这个Activity的对象,调用它父类的onCreate方法呢?通过分析Instant Run对这个问题的处理,发现它是在每个class中都添加了一个代理函数,专门来处理super的问题的。为每个class都增加一个函数无疑会增加总的方法数,这样做肯定会遇到65536这个问题。所以直接使用Instant Run的做法显然是不可取的。 在Java中super是个关键字,也无法通过别的对象来访问到。看来,想直接在patched.java代码中通过Activity的对象调用到它父类的onCreate方法有点不太可能了。不过通过对class文件做分析,发现普通的函数调用是使用JVM指令集的invokevirtual指令,而super.onCreate的调用使用的是invokesuper指令。那是不是将class文件中这个调用的指令改为invokesuper就好了?

看如下的例子: 产品代码SuperClass.java:

1
2
3
4
5
6
7
8
9
public class SuperClass {
String uuid;
public void setUuid(String id) {
uuid = id;
}
public void thisIsSuper() {
Log.d("SuperClass", "thisIsSuper "+uuid);
}
}

产品代码TestSuperClass.java:

1
2
3
4
5
6
7
8
9
10
11
public class TestSuperClass extends SuperClass{
String subUuid;
public void setSubUuid(String id) {
subUuid = id;
}

@Override
public void thisIsSuper() {
Log.d("TestSuperClass", "thisIsSuper no call");
}
}

TestSuperPatch.java是DexClassLoader将要加载的代码:

1
2
3
4
5
6
7
8
9
public class TestSuperPatch {
public static void testSuperCall() {
TestSuperClass testSuperClass = new TestSuperClass();
String t = UUID.randomUUID().toString();
Log.d("TestSuperPatch", "UUID " + t);
testSuperClass.setUuid(t);
testSuperClass.thisIsSuper();
}
}

对TestSuperPatch.class的testSuperClass.thisIsSuper()调用做invokesuper的替换,并且将invokesuper的调用作用在testSuperClass这个对象上,然后加载运行:

1
Caused by: java.lang.NoSuchMethodError: No super method thisIsSuper()V in class Lcom/meituan/sample/TestSuperClass; or its super classes (declaration of 'com.meituan.sample.TestSuperClass' appears in /data/app/com.meituan.robust.sample-3/base.apk)

报错信息说在TestSuperClass和TestSuperClass的父类中没有找到thisIsSuper()V函数!但是实际上TestSuperClass和父类中是存在thisIsSuper()V函数的,而且通过apk反编译看也确实存在的,那怎么就找不到呢?分析invokesuper指令的实现,发现系统会在执行指令所在的class的父类中去找需要调用的方法,所以要将TestSuperPatch跟TestSuperClass一样作为SuperClass的子类。修改如下:

1
2
3
public class TestSuperPatch extends SuperClass {
...
}

然后再做一次尝试:

1
2
08-11 09:12:03.012 1787-1787/? D/TestSuperPatch: UUID c5216480-5c3a-4990-896d-58c3696170c5
08-11 09:12:03.012 1787-1787/? D/SuperClass: thisIsSuper c5216480-5c3a-4990-896d-58c3696170c5

看一下testSuperCall的实现,将UUID.randomUUID().toString()的结果,通过setUuid赋值给了testSuperClass这个对象的父类的uuid字段。从日志可以看出,对testSuperClass.thisIsSuper处理后,确实是调用到了testSuperClass这个对象的super的thisIsSuper函数。OK,super的问题看来解决了,而且这种方式不会增加方法数。

参考资料

1.Robust源码
2.官方原理:Android热更新方案Robust
3.热修复之仿Robust实现
4.Android-美团Robust热修复接入实践问记录