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

作者简介:Devleo Deng,Android开发工程师,2023年加入37手游技术部,目前负责海外游戏发行 Android SDK 开发。

一、各大厂热修复框架

目前各大厂的热修复框架五花八门,主要有AndFix、Tinker、Robust等等。

热修复框架按照原理大致可以分为三类:

1.腾讯系Tinker:

基于Multidex机制干预ClassLoader加载dex:将热修复的类放在dexElements的最前面,优先加载到要修复类以达到修复目的。

2.阿里系AndFix:

Native替换方法结构体:修改java方法在native层的函数指针,指向修复后的方法以达到修复目的。

3.美团系Robust:

Instant-Run插桩方案:在出包apk包编译阶段对Java层每个方法的前面都织入一段控制执行逻辑代码。

二、热修复方案的优劣势

技术方案 Tinker QZone AndFix Robust
类替换 yes yes no no
So替换 yes no no no
资源替换 yes yes no no
即时生效 no no yes yes
性能损耗 较小 较小 较小 较小
补丁大小 较小 较大 一般 最小
复杂度 较低 较低 复杂 复杂
成功率 较高 较高 一般 最高(99.99%)

AndFix: github.com/alibaba/And...

Tinker: github.com/Tencent/tin...

Robust: github.com/Meituan-Dia...

三、美团 Robust 热修复核心原理

Robust 插件对APP应用Java层的每个方法都在编译打包阶段自动的插入了一段代码(备注:可以通过包名列表来配置需要插入代码的范围)。

通过判断if(changeQuickRedirect != null)来确定是否进行热修复,当changeQuickRedirect不为null时,调用补丁包中patch.dex中同名Patch类的同名方法达到 修复目的。

3.1 以Hotfix类为例

java 复制代码
public class Hotfix {
    public int needToHotfix() {
        return 0;
    }
}

3.2 插桩后的Hotfix

java 复制代码
public class Hotfix {
    public static ChangeQuickRedirect changeQuickRedirect;
    public int needToHotfix() {
        if (changeQuickRedirect != null) {
            //HotfixPatch中封装了获取当前类的className和methodName的逻辑,并在其内部最终调用了changeQuickRedirect的对应accessDispatch方法
            if (HotfixPatch.isSupport(new Object[0], this, changeQuickRedirect, false)) {
                return ((Long) HotfixPatch.accessDispatch(new Object[0], this, changeQuickRedirect, false)).intValue();
            }
        }
        return 0;
    }
}

3.3 生成的patch类

主要包含两个class:PatchesInfoImpl.java和HotfixPatch.java。

  1. 生成一个PatchesInfoImpl补丁包说明类,可以获取补丁对象;对象包含被修复类名及该类对应的补丁类。
java 复制代码
public class PatchesInfoImpl implements PatchesInfo {
    public List<PatchedClassInfo> getPatchedClassesInfo() {
        List<PatchedClassInfo> patchedClassesInfos = new ArrayList<PatchedClassInfo>();
        PatchedClassInfo patchedClass = new PatchedClassInfo("com.robust.demo.Hotfix", HotfixPatch.class.getCanonicalName());
        patchedClassesInfos.add(patchedClass);
        return patchedClassesInfos;
    }
}
  1. 生成一个HotfixPatch类, 创一个实例并反射赋值给Hotfix中的changeQuickRedirect变量。
java 复制代码
public class HotfixPatch implements ChangeQuickRedirect {
    @Override
    public Object accessDispatch(String methodSignature, Object[] paramArrayOfObject) {
        String[] signature = methodSignature.split(":");
        // 没有开启混淆方法名依旧为needToHotfix,开启混淆后【needToHotfix】会变成【混淆后的对应方法名】
        // int needToHotfix() -> needToHotfix
        if (TextUtils.equals(signature[1], "needToHotfix")) {
            return 1;
        }
        return null;
    }

    @Override
    public boolean isSupport(String methodSignature, Object[] paramArrayOfObject) {
        String[] signature = methodSignature.split(":");
        // 没有开启混淆方法名依旧为needToHotfix,开启混淆后【needToHotfix】会变成【混淆后的对应方法名】
        // int needToHotfix() -> needToHotfix
        if (TextUtils.equals(signature[1], "needToHotfix")) {
            return true;
        }
        return false;
    }
}

执行需要修复的代码needToHotfix方法时,会转而执行HotfixPatch中逻辑。 由于Robust的修复过程中并没有干扰系统加载dex过程的逻辑,所以这种方案兼容性无疑是最好。

四、Robust 组成部分

Robust 的实现可以分成三个部分:基础包插桩、生成补丁包、加载补丁包。

4.1 基础包插桩

Robust 通过配置文件 robust.xml来指定是否开启插桩、哪些包下需要插桩、哪些包下不需要插桩,在编译 Release 包时,RobustTransform 这个插件会自动遍历所有的类,并根据配置文件中指定的规则,对类进行以下操作:

java 复制代码
class RobustTransform extends Transform implements Plugin<Project> {
    @Override
    void apply(Project target) {
        ...
        // 解析对应的APP应用的配置文件robust.xml,确定需要插桩注入代码的类
        robust = new XmlSlurper().parse(new File("${project.projectDir}/${Constants.ROBUST_XML}"));
        // 将该类注册到对应的APP工程的Transform过程中
        project.android.registerTransform(this);
        ...
    }
}
  1. 类中增加一个静态变量 ChangeQuickRedirect changeQuickRedirect
  2. 在方法前插入一段代码,如果是需要修补的方法就执行补丁包中对应修复方法的相关逻辑,如果不是则执行原有逻辑。
  3. 美团 Robust 分别使用了ASM、Javassist两个字节码框架实现了插桩修改字节码的操作,以 javaassist 操作字节码为例进行阐述:
java 复制代码
class JavaAssistInsertImpl {
    @Override
    protected void insertCode(List<CtClass> box, File jarFile) throws CannotCompileException, IOException, NotFoundException {
        for (CtBehavior ctBehavior : ctClass.getDeclaredBehaviors()) {
            // 第一步: 增加 静态变量 changeQuickRedirect
            if (!addIncrementalChange) {
                //insert the field
                addIncrementalChange = true;
                // 创建一个静态变量并添加到 ctClass 中
                ClassPool classPool = ctBehavior.getDeclaringClass().getClassPool();
                CtClass type = classPool.getOrNull(Constants.INTERFACE_NAME);  // com.meituan.robust.ChangeQuickRedirect
                CtField ctField = new CtField(type, Constants.INSERT_FIELD_NAME, ctClass);  // changeQuickRedirect
                ctField.setModifiers(AccessFlag.PUBLIC | AccessFlag.STATIC);
                ctClass.addField(ctField);
            }
            // 判断这个方法需要修复
            if (!isQualifiedMethod(ctBehavior)) {
                continue;
            }
            try {
                // 判断这个方法需要修复
                if (ctBehavior.getMethodInfo().isMethod()) {
                    CtMethod ctMethod = (CtMethod) ctBehavior;
                    boolean isStatic = (ctMethod.getModifiers() & AccessFlag.STATIC) != 0;
                    CtClass returnType = ctMethod.getReturnType();
                    String returnTypeString = returnType.getName();
                    // 第二步: 方法前插入一段代码...
                    String body = "Object argThis = null;";
                    if (!isStatic) {
                        body += "argThis = $0;";
                    }
                    String parametersClassType = getParametersClassType(ctMethod);
                    // 在 javaassist 中 $args 表达式代表 方法参数的数组,可以看到 isSupport 方法传了这些参数:方法所有参数,当前对象实例,changeQuickRedirect,是否是静态方法,当前方法id,方法所有参数的类型,方法返回类型
                    body += "   if (com.meituan.robust.PatchProxy.isSupport($args, argThis, " + Constants.INSERT_FIELD_NAME + ", " + isStatic +
                            ", " + methodMap.get(ctBehavior.getLongName()) + "," + parametersClassType + "," + returnTypeString + ".class)) {";
                    body += getReturnStatement(returnTypeString, isStatic, methodMap.get(ctBehavior.getLongName()), parametersClassType, returnTypeString + ".class");
                    body += "   }";
                    // 第三步:把我们写出来的body插入到方法执行前逻辑
                    ctBehavior.insertBefore(body);
                }
            } catch (Throwable t) {
                //here we ignore the error
                t.printStackTrace();
                System.out.println("ctClass: " + ctClass.getName() + " error: " + t.getMessage());
            }
        }
    }
}

4.2 生成补丁包

4.2.1 Robust支持补丁自动化生成,具体操作如下:

  1. 在修复完的方法上添加@Modify注解;
  2. 新创建的方法或类添加@Add注解。
  3. 工程添加依赖 apply plugin: 'auto-patch-plugin',编译完成后会在outputs/robust目录下生成patch.jar。
java 复制代码
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.TYPE, ElementType.CONSTRUCTOR})
@Retention(RetentionPolicy.CLASS)
@Documented
public @interface Modify {
    String value() default "";
}

对于要修复的方法,直接在方法声明时增加 Modify注解

java 复制代码
public class NeedModify {
    @Modify
    public String getNeedToModify() {
        return "ErrorText";
    }
}

生成补丁包环节结束...

4.2.2 补丁结构

每个补丁包含以下三个部分:PatchesInfoImpl(补丁包说明类)、PatchControl(补丁类)、xxPatch(具体补丁方法的实现)

  1. PatchesInfoImpl:补丁包说明类,可以获取所有补丁对象;每个对象包含被修复类名及该类对应的补丁类。
java 复制代码
public class PatchesInfoImpl implements PatchesInfo {
    public List getPatchedClassesInfo() {
        ArrayList arrayList = new ArrayList();
        arrayList.add(new PatchedClassInfo("com.meituan.sample.NeedModify", "com.meituan.robust.patch.NeedModifyPatchControl"));
        EnhancedRobustUtils.isThrowable = false;
        return arrayList;
    }
}
  1. PatchControl:补丁类,具备判断方法是否执行补丁逻辑,及补丁方法的调度。 Robust 会从模板类的基础上生成一个这个类专属的 ChangeQuickRedirect 类, 模板类代码如下:
java 复制代码
public class NeedModifyPatchControl implements ChangeQuickRedirect {
    
    //1.方法是否支持热修
    @Override
    public boolean isSupport(String methodName, Object[] paramArrayOfObject) {
        ...
        return true;
    }
    
    //2.调用补丁的热修逻辑
    @Override
    public Object accessDispatch(String methodName, Object[] paramArrayOfObject) {
        ...
        return null;
    }
}
  1. Patch:具体补丁方法的实现。该类中包含被修复类中需要热修的方法。
java 复制代码
public class NeedModifyPatch
{
    NeedModify originClass;
    public NeedModifyPatch(Object paramObject)
    {
        this.originClass = ((NeedModify)paramObject);
    }
    //热修的方法具体实现
    private String getNeedToModifyText()
    {
        Object localObject = getRealParameter(new Object[] { "ModifyText" });
        return (String)EnhancedRobustUtils.invokeReflectConstruct("java.lang.String", (Object[])localObject, new Class[] { String.class });
    }
}

补丁包的生成逻辑:

  1. 反射获取PatchesInfoImpl中补丁包映射关系,如PatchedClassInfo("com.meituan.sample.NeedModify", "com.meituan.robust.patch.NeedModifyPatchControl")。
  2. 反射获取NeedModify类插桩生成changeQuickRedirect对象,实例化NeedModifyPatchControl,并赋值给 changeQuickRedirect
    备注:生成的补丁包是jar格式的,需要使用jar2dex工具jar包转换成dex包

4.3 加载补丁包

自定义PatchManipulate实现类,需要实现拉取补丁、校验补丁等逻辑。

java 复制代码
public abstract class PatchManipulate {
    /**
     * 获取补丁列表
     * @return 相应的补丁列表
     */
    protected abstract List<Patch> fetchPatchList(Context context);
    
    /**
     * 努力确保补丁文件存在,验证md5是否一致。
     * 如果不存在,则动态下载
     * @return 是否存在
     */
    protected abstract boolean ensurePatchExist(Patch patch);

    /**
     * 验证补丁文件md5是否一致
     * 如果不存在,则动态下载
     * @return 校验结果
     */
    protected abstract boolean verifyPatch(Context context, Patch patch);
}

当线上应用出现bug时,可以推送的方式通知客户端拉取对应的补丁包,下载补丁包完成后,会开一个子线程执行以下操作: (同时建议:在应用启动时,也执行一次更新补丁包操作)

java 复制代码
// 1. 拉取补丁列表
List<Patch> patches = patchManipulate.fetchPatchList(context);
for (Patch patch : patches) {
    //2. 验证补丁文件md5是否一致
    if (patchManipulate.ensurePatchExist(patch)) {
        patch(context, patch);
        ...
        return true;
    }
}

致此,所有的操作流程完成,线上问题得以修复。

五. 常见问题

1. Robust 导致Proguard 方法内联失效

Proguard是一款代码优化、混淆利器,Proguard 会对程序进行优化,如果某个方法很短或者只被调用了一次,那么Proguard会把这个方法内部逻辑内联到调用处。 Robust的解决方案是找到内联方法,不对内联的方法插桩。

2. lambada 表达式修复

方案一:对于lambada表达式无法直接添加注解,Robust提供了一个RobustModify类,modify方法是空方法,在编译时使用ExprEditor检测是否调用了RobustModify类,调用则认为此方法需要修复。

java 复制代码
private void init() {
    mBindButton.setOnClickListener(v -> {
        RobustModify.modify();
        System.out.print("Hello Devleo");
   });
}

方案二:重写这部分代码,将其展开,并在对应的方法上打上@Modify标签,自定义一个类自实现OnClickListener执行相关逻辑:

java 复制代码
@Modify
private void init() {
     mBindButton.setOnClickListener(new OnClickListenerImpl());
}

@Add
public static class OnClickListenerImpl implements OnClickListener {
    @Override
    public void onClick(View v) {
        System.out.print("Hello Devleo");
    }
}

六、总结

优点:

1.兼容性好:Robust采用Instant Run插桩的方案。

2.实时生效,且修复率高。

3.UI问题也可以通过动态添加和移除View等方式解决。

缺点:

1.由于需要插入代码,所以会一定在一定程度上增加包体积。

2.不支持so文件和资源替换。

七、参考

Android热更新方案Robust
Android热补丁之Robust原理解析

makefile 复制代码
最后的最后:
我们是37手游移动客户端开发团队,致力于为游戏行业提供高质量的SDK开发服务。
欢迎关注我们,了解更多移动开发和游戏 SDK 技术动态~
技术问题/交流/进群等可以加官方微信 MobileTeam37
相关推荐
MavenTalk8 分钟前
前端技术选型之uniapp
android·前端·flutter·ios·uni-app·前端开发
坚定信念,勇往无前15 分钟前
uni-app运行 安卓模拟器 MuMu模拟器
android·uni-app
吾即是光3 小时前
[SWPUCTF 2021 新生赛]error
android
大耳猫3 小时前
Android 基于Camera2 API进行摄像机图像预览
android·kotlin·相机·camera
MYBOYER3 小时前
Kotlin DSL Gradle 指南
android·开发语言·kotlin
Mr_Xuhhh4 小时前
程序地址空间
android·java·开发语言·数据库
呆呆小雅6 小时前
C# 结构体
android·java·c#
ᥬ 小月亮8 小时前
Layui表格的分页下拉框新增“全部”选项
android·javascript·layui
sunly_17 小时前
Flutter:启动屏逻辑处理02:启动页
android·javascript·flutter
Sgq丶18 小时前
Android Studio 配置 proto
android·ide·android studio