文章

Android Robust

Android Robust

Robust 热修复原理·

通过在编译时对类方法进行代码插桩,针对每一个方法都进行如下方法的插桩,每个类都设置一个静态变量changeQuickRedirect。通过运行时ClassLoader加载补丁patch.jar,将补丁中的相关类对象解析反射获取后,再通过反射给已经插桩的原始类中的changeQuickRedirect赋值,这样进行热修复的方法执行前会被补丁类方法对象拦截执行补丁类中的逻辑,从而达到热修复的目的。

补丁加载流程

1.通过在子线程循环遍历加载的补丁文件

2.创建classLoader来加载补丁patch.jar文件

3.获取指定的补丁 list 类文件中的 list,得到补丁类与补丁执行类

4.再通过 classLoader 获取原始要修改补丁类中的changeQuickRedirect进行赋值

5.执行时即可调用补丁中的类逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
#com.meituan.robust.PatchExecutor中 run 方法执行后循环加载 list 中的补丁如下为核心代码

protected boolean patch(Context context, Patch patch) {
        if (!patchManipulate.verifyPatch(context, patch)) {
            robustCallBack.logNotify("verifyPatch failure, patch info:" + "id = " + patch.getName() + ",md5 = " + patch.getMd5(), "class:PatchExecutor method:patch line:107");
            return false;
        }
        ClassLoader classLoader = null;
        try {
            File dexOutputDir = getPatchCacheDirPath(context, patch.getMd5());
            String tempPath = patch.getTempPath();
            String absolutePath = dexOutputDir.getAbsolutePath();
            ClassLoader baseClassLoader = PatchExecutor.class.getClassLoader();
            classLoader = new DexClassLoader(tempPath, absolutePath, null, baseClassLoader);
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
        if (null == classLoader) {
            return false;
        }
        Class patchClass, sourceClass;
        Class patchesInfoClass;
        PatchesInfo patchesInfo = null;
        try {
            patchesInfoClass = classLoader.loadClass(patch.getPatchesInfoImplClassFullName());
            patchesInfo = (PatchesInfo) patchesInfoClass.newInstance();
        } catch (Throwable t) {}
        if (patchesInfo == null) {
            robustCallBack.logNotify("patchesInfo is null, patch info:" + "id = " + patch.getName() + ",md5 = " + patch.getMd5(), "class:PatchExecutor method:patch line:114");
            return false;
        }
        List<PatchedClassInfo> patchedClasses = patchesInfo.getPatchedClassesInfo();
        if (null == patchedClasses || patchedClasses.isEmpty()) {
            return true;
        }
        boolean isClassNotFoundException = false;
        for (PatchedClassInfo patchedClassInfo : patchedClasses) {
            String patchedClassName = patchedClassInfo.patchedClassName;
            String patchClassName = patchedClassInfo.patchClassName;
            if (TextUtils.isEmpty(patchedClassName) || TextUtils.isEmpty(patchClassName)) {
                robustCallBack.logNotify("patchedClasses or patchClassName is empty, patch info:" + "id = " + patch.getName() + ",md5 = " + patch.getMd5(), "class:PatchExecutor method:patch line:131");
                continue;
            }
            try {
                try {
                    sourceClass = classLoader.loadClass(patchedClassName.trim());
                } catch (ClassNotFoundException e) {
                    isClassNotFoundException = true;
                    continue;
                }
                Field[] fields = sourceClass.getDeclaredFields();
                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;
                }
                try {
                    patchClass = classLoader.loadClass(patchClassName);
                    Object patchObject = patchClass.newInstance();
                    changeQuickRedirectField.setAccessible(true);
                    //设置补丁类的对象到原始类的ChangeQuickRedirect字段中
                    changeQuickRedirectField.set(null, patchObject);
                } catch (Throwable t) {
                    robustCallBack.exceptionNotify(t, "class:PatchExecutor method:patch line:163");
                }
            } catch (Throwable t) {
                Log.e("robust", "patch failed! ");
//                robustCallBack.exceptionNotify(t, "class:PatchExecutor method:patch line:169");
            }
        }
        if (isClassNotFoundException) {
            return false;
        }
        return true;
    }

补丁分析

补丁构建会生成如下包结构的类文件

PatchesInfoImpl.java 补丁管理类主要是获取修复了那几个类

XXXXPatch.java 热修复修改之后的类逻辑执行

XXXXPatchControl.java 实现了ChangeQuickRedirect接口,根据isSupport()与accessDispatch()方法来对XXXXPatch热修复后的方法进行调用。

XXXPatchRobustAssist.java 针对方法中的 super 继承处理

三种结构的类文件demo示例如下

PatchesInfoImpl.java

1
2
3
4
5
6
7
8
public class PatchesInfoImpl implements PatchesInfo {
    public List getPatchedClassesInfo() {
        ArrayList arrayList = new ArrayList();
        arrayList.add(new PatchedClassInfo("com.meituan.sample.SecondActivity", "com.meituan.robust.patch.SecondActivityPatchControl"));
        EnhancedRobustUtils.isThrowable = true;
        return arrayList;
    }
}

XXXXPatch.java 热修复修改之后的类逻辑执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class SecondActivityPatch {
    SecondActivity originClass;

    public SecondActivityPatch(Object obj) {
        this.originClass = (SecondActivity) obj;
    }

    public Object[] getRealParameter(Object[] objArr) {
        if (objArr == null || objArr.length < 1) {
            return objArr;
        }
        Object[] objArr2 = (Object[]) Array.newInstance(objArr.getClass().getComponentType(), objArr.length);
        for (int i = 0; i < objArr.length; i++) {
            Object obj = objArr[i];
            if (obj instanceof Object[]) {
                objArr2[i] = getRealParameter((Object[]) obj);
            } else if (obj == this) {
                objArr2[i] = this.originClass;
            } else {
                objArr2[i] = obj;
            }
        }
        return objArr2;
    }

    public String getTextInfo() {
        String[] strArr = (String[]) EnhancedRobustUtils.invokeReflectMethod("getArray", this.originClass, new Object[0], (Class[]) null, SecondActivity.class);
        Log.d("robust", "invoke  method is       No:  1 getArray");
        return "error fixed";
    }

    public String[] getArray() {
        return new String[]{"hello", "world"};
    }
}

XXXXPatchControl.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
public class SecondActivityPatchControl implements ChangeQuickRedirect {
    public static final String MATCH_ALL_PARAMETER = "(\\w*\\.)*\\w*";
    private static final Map<Object, Object> keyToValueRelation = new WeakHashMap();

    public Object getRealParameter(Object obj) {
        return obj instanceof SecondActivity ? new SecondActivityPatch(obj) : obj;
    }

    public boolean isSupport(String str, Object[] objArr) {
        Log.d("robust", new StringBuffer().append("arrivied in isSupport ").append(str).append(" paramArrayOfObject  ").append(objArr).toString());
        String str2 = str.split(":")[3];
        Log.d("robust", new StringBuffer().append("in isSupport assemble method number  is  ").append(str2).toString());
        Log.d("robust", new StringBuffer().append("arrivied in isSupport ").append(str).append(" paramArrayOfObject  ").append(objArr).append(" isSupport result is ").append(":7D38F0CB3C6A783027FFD1B00CDB120F:".contains(new StringBuffer().append(":").append(str2).append(":").toString())).toString());
        return ":7D38F0CB3C6A783027FFD1B00CDB120F:".contains(new StringBuffer().append(":").append(str2).append(":").toString());
    }

    public Object accessDispatch(String methodName, Object[] paramArrayOfObject) {
        SecondActivityPatch secondActivityPatch;
        Log.d("robust", new StringBuffer().append("arrivied in AccessDispatch ").append(methodName).append(" paramArrayOfObject  ").append(paramArrayOfObject).toString());
        if (methodName.split(":")[2].equals("false")) {
            Map<Object, Object> map = keyToValueRelation;
            if (map.get(paramArrayOfObject[paramArrayOfObject.length - 1]) == null) {
                Log.d("robust", "keyToValueRelation not contain");
                secondActivityPatch = new SecondActivityPatch(paramArrayOfObject[paramArrayOfObject.length - 1]);
                map.put(paramArrayOfObject[paramArrayOfObject.length - 1], null);
            } else {
                secondActivityPatch = (SecondActivityPatch) map.get(paramArrayOfObject[paramArrayOfObject.length - 1]);
            }
        } else {
            Log.d("robust", "static method forward ");
            secondActivityPatch = new SecondActivityPatch(null);
        }
        String str = methodName.split(":")[3];
        Log.d("robust", new StringBuffer().append("assemble method number  is  ").append(str).toString());
        if ("7D38F0CB3C6A783027FFD1B00CDB120F".equals(str)) {
            Log.d("robust", "invoke method is com.meituan.robust.patch.SecondActivityPatch.getTextInfo() ");
            return secondActivityPatch.getTextInfo();
        }
        return null;
    }

    private static Object fixObj(Object booleanObj) {
        if (booleanObj instanceof Byte) {
            byte byteValue = ((Byte) booleanObj).byteValue();
            boolean booleanValue = byteValue != 0;
            return new Boolean(booleanValue);
        }
        return booleanObj;
    }
}

补丁调用流程

在补丁加载流程中已经给接口对象changeQuickRedirectField进行了赋值 现在通过插桩的热修复方法进行调用在PatchProxy.proxy(…)调用中会最终通过changeQuickRedirect.isSupport(…)判断是否调用accessDispatch(…)方法。accessDispatch 方法中已经写好了 XXXpatchPatch.java 热修复之后的调用逻辑。

1
if (PatchProxy.proxy(new Object[]{bundle}, this, changeQuickRedirect, false, 15, new Class[]{Bundle.class}, Void.TYPE).isSupported) { return;}

Gradle 部分详解

借鉴了 InstantRun 的原理。 主要分插桩,补丁生成,加载流程。

Robust 的核心原理是通过字节码插桩技术,在编译期间修改 Android 应用的字节码。

有两种实现字节码操作库:

  1. ASM :默认使用的字节码操作库(推荐)
  2. JavaAssist :可选的字节码操作库

下载 Robust 官方开源库,发现官方库很久没有维护了,开始调整开源库升级到目前业务环境的版本 Gradle 2.x -> Gradle 4.x

升级robust框架

  1. 升级官方使用的 gradle 版本 2.x 到 4.x
  2. 升级AMS 版本org.ow2.asm:asm:5.0.1到9.2
    1. 查看官方文档https://asm.ow2.io/faq.html#Q15 自 4.0 之后向下兼容,安全方案是升级到 6.x,因为目前业务只需要支持到 jdk1.8 即可
    2. 升级之后无法找到AMSUtils 这个类,怀疑可能在低版本的 gradle 中有,高版本已经废弃,修改类判断进行替代
    3. 通过上面改动之后发现插件已经可以使用了,但是仓库发布需要重新适配一下,这里又回忆了一下[[Gradle-plugin id=dad4279a-71bb-4829-9d36-87ce6c394e7f]]
    4. 配置 maven-publish 发布到本地仓库中,插桩 ok,补丁生成出现问题,失败原因提示无法读取到注解 Modify的配置
    5. 暂时跳过补丁生成的问题,先了解原理
      1. 问题 1,robust 框架是如何阻止内联问题的?
        1. [[内联编译 id=bc7b8282-dee1-48f0-a639-e75c0f06329e]]
        2. 通过编译时进行插桩,插桩代码是用来判断是否存在补丁类,是静态方法且不确定回调的结果,从而阻止内联优化,编译时就不会将类方法放到一起。
      2. 问题 2,为什么每个方法插入的这么复杂?而不是一个简单的反射判断?
        1. 这个问题robust 的注释中说明了,减少包大小,指令更少,修复线程安全问题。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
     * 原来的插桩逻辑为这样:
     * if (PatchProxy.isSupport()) {
     *     return PatchProxy.accessDispatch();
     * }
     * 封装一下变成下面这种代码
     * PatchProxyResult patchProxyResult = PatchProxy.proxy();
     * if (patchProxyResult.isSupported) {
     *     return patchProxyResult.result;
     * }
     * 这样做的好处有两个:
     * 1. 减少包大小。 不是开玩笑,虽然后者代码看起来变得复杂,但实质产生的指令更少。
     * 之前两个函数调用,每次都需要load 7个参数到栈上,这7个参数还不是简单的基本类型,这意味着比后者多出若干条指令。
     * 数据显示在5W个方法的插桩下,后者能比前者节省200KB
     * 2. fix一个bug。robust其实支持采用将ChangeQuickRedirect置为null的方法实时下线一个patch,那原来的插桩逻辑就存在线程安全的问题。
     * 根源在于原来的逻辑中ChangeQuickRedirect是每次都直接去取的static变量值
     * 如果在执行isSupport的时候ChangeQuickRedirect有值,但执行到accessDispatch时ChangeQuickRedirect被置为空,那就意味着被patch的方法该次将不执行任何代码
     * 这样会带来一系列的不可知问题。
     * 封装之后能保证这两个方法读取到的ChangeQuickRedirect是同一份。
     */
1
2
3
4
5
6
  3. 问题 3,补丁加载执行是否有兼容性问题?
        1. 目前没有发现。切换线程执行业务逻辑正常。
  4. 问题 4,robust 支持实时热修复吗?
     1. 根据加载插件的调用时机来决定。 3. 修改插桩方法映射,数字编号修改成 MD5,测试 ok 4. 补丁构建失败,比较复杂,研究了一下如何 debug 断点调试[[ Gradle-plugin 断点调试 id=da4b1dd8-ada7-416f-a3e5-168ed1e7186f]] 5. 补丁构建失败,开始调整补丁问题    1. 发现构建补丁时 jar 转 dex 失败,将dx 升级d8.jar 来实现    2. 初始化时拼接的 jar 转 dex 命令有问题导致,修改后正常 6. 准备打包构建时发现普通方法没有插桩,开始研究为什么普通方法没有插桩?(单行方法中的插桩没有意义)    1. 梳理插桩逻辑
  1. 读取 robust.xml 配置并注册插桩task
  2. 遍历所有的类并使用 AMS 插桩 7. 回到补丁生成流程    1. 重新构建生成补丁 ok 8. 打包测试,一切 ok!

ok!初步的升级已经完成了,接下来只需要检测一下业务可用性,以及代码兼容性。根据业务需要进行改造就可以了。

Robust 插桩与补丁构建

插桩流程

插桩流程主要如下

1.自定义插件 transform 并注册,在java -> class -> dex 时,对 class 文件进行插桩,插桩流程需要对 class 文件结构非常理解,这里有两种方式 AMS 与 javaassets 方式。

2.读取配置文件 robust.xml 来针对要插桩的类进行方法前的插桩操作,侵入性很强

3.插桩时为每个插桩类生成全局对象 Chanxxxredirt,并为每个方法生成全局唯一 id,主要作用有亮点,补丁映射与补丁方法对比

4.接口与 R 文件等相关不需要插桩

补丁生成流程

1.读取配置文件 robust.xml,针对加入注解@Modify与@Add 或 Robust.modify()调用的方法进行映射。

2.编译时根据插桩生成的文件methodsMap.robust对修复方法进行映射

4.生成XXXPatchControl.java(实现了ChangeQuickRedirect接口的类),XXXPatch.java(补丁修复后的执行类),PatchedClassInfo(补丁管理类),XXXPatchRobustAssist.java(方法中含有 super 的处理,如果有 super 才会存在该类),具体逻辑比较复杂,就不展开了,这里涉及到关于静态方法,非静态方法的判断,以及super的处理。

5.通过 ZIP 压缩成meituan.jar后再进行,jar 转 dex,这里已经将 dx升级成使用d8处理了

6.将 d8处理后的 dex 反编译成 smali,对 smali 文件进行混淆和方法中super 处理(invoke-virtual替换成

invoke-super过程)

7.通过 smali.jar再将 smali 转换成 classes.dex,最后转成 patch.jar

生成 dex 后,通过 smali.jar 反编译成 smali 文件,优化处理完 smali 之后重新转成 dex

以上流程分析过程可能还存在不完整的部分,比如在梳理过程中差点对 super的处理过程给忽略了,最后做了测试才发现处理的过程。后续如果需要补充我会继续进行完善。由此深知,分析源码是一个非常需要耐心的过程,认识不等于理解。

其他

  • 兼容性边界:Android 6~Android 15 测试Demo补丁加载 ok,多线程下测试调用 ok,未测试模拟器。
  • 性能开销:插桩会增加每个方法的调用开销,未针对在高频调用场景下性能开销对应用的影响?例如,是否会导致帧率下降或启动时间延长?
  • 安全性:补丁分发的安全性如何保证?是否存在被恶意补丁攻击的风险?需要研究补丁的加密和校验机制。

后期需要优化的地方

  • 插桩阶段/补丁生成阶段可以并发操作
  • 插桩时遍历的类可以提前规避掉哪些类需要插桩(已完成)
  • 静态方法调用静态方法生成补丁时出现异常(补丁生成流程问题导致,生成后及时中断 Task 任务即可)
  • 运行时原始类加载时使用应用的 classLoader 而非补丁的 classLoader,已经确定的情况下可以不使用双亲委派模式

基本的使用测试 ok,现在在使用之前,需要进行性能开销测试了,使用插桩技术究竟对业务有多少影响?为测试这个流程,打算增加一个性能监听工具类用于测试。

拓展

  • 与 tinker,Sophix的对比?
  • 是否尝试将 Robust 与 Kotlin 协程或 Jetpack 组件结合,验证在新架构下的表现?
  • 编写一个简单的 ASM 插件,尝试修改方法逻辑(如插入日志)。
  • 阅读《深入理解 Java 虚拟机》中的字节码章节,结合 Robust 的插桩代码分析。
  • 分析 Robust 的 ClassVisitor 和 MethodVisitor 实现,尝试添加自定义插桩逻辑(如日志记录)。
  • 阅读《Gradle for Android》,掌握 Transform API 和 Task 自定义。
本文由作者按照 CC BY 4.0 进行授权