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

318 篇文章 18 订阅
46 篇文章 2 订阅

作者简介: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层每个方法的前面都织入一段控制执行逻辑代码。

二、热修复方案的优劣势

技术方案TinkerQZoneAndFixRobust
类替换yesyesnono
So替换yesnonono
资源替换yesyesnono
即时生效nonoyesyes
性能损耗较小较小较小较小
补丁大小较小较大一般最小
复杂度较低较低复杂复杂
成功率较高较高一般最高(99.99%)

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

Robust 插件对APP应用Java层的每个方法都在编译打包阶段自动的插入了一段代码(备注:可以通过包名列表来配置需要插入代码的范围)。
通过判断if(changeQuickRedirect != null)来确定是否进行热修复,当changeQuickRedirect不为null时,调用补丁包中patch.dex中同名Patch类的同名方法达到 修复目的。

3.1 以Hotfix类为例

public class Hotfix {
    public int needToHotfix() {
        return 0;
    }
}

3.2 插桩后的Hotfix

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补丁包说明类,可以获取补丁对象;对象包含被修复类名及该类对应的补丁类。
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变量。
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 这个插件会自动遍历所有的类,并根据配置文件中指定的规则,对类进行以下操作:

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 操作字节码为例进行阐述:
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。
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.TYPE, ElementType.CONSTRUCTOR})
@Retention(RetentionPolicy.CLASS)
@Documented
public @interface Modify {
    String value() default "";
}

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

public class NeedModify {
    @Modify
    public String getNeedToModify() {
        return "ErrorText";
    }
}

生成补丁包环节结束…

4.2.2 补丁结构

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

  1. PatchesInfoImpl:补丁包说明类,可以获取所有补丁对象;每个对象包含被修复类名及该类对应的补丁类。
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 类, 模板类代码如下:
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:具体补丁方法的实现。该类中包含被修复类中需要热修的方法。
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实现类,需要实现拉取补丁、校验补丁等逻辑。

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时,可以推送的方式通知客户端拉取对应的补丁包,下载补丁包完成后,会开一个子线程执行以下操作: (同时建议:在应用启动时,也执行一次更新补丁包操作)

// 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类,调用则认为此方法需要修复。

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

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

@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 学习笔录

Android 性能优化篇:https://qr18.cn/FVlo89
Android 车载篇:https://qr18.cn/F05ZCM
Android 逆向安全学习笔记:https://qr18.cn/CQ5TcL
Android Framework底层原理篇:https://qr18.cn/AQpN4J
Android 音视频篇:https://qr18.cn/Ei3VPD
Jetpack全家桶篇(内含Compose):https://qr18.cn/A0gajp
Kotlin 篇:https://qr18.cn/CdjtAF
Gradle 篇:https://qr18.cn/DzrmMB
OkHttp 源码解析笔记:https://qr18.cn/Cw0pBD
Flutter 篇:https://qr18.cn/DIvKma
Android 八大知识体:https://qr18.cn/CyxarU
Android 核心笔记:https://qr21.cn/CaZQLo
Android 往年面试题锦:https://qr18.cn/CKV8OZ
2023年最新Android 面试题集:https://qr18.cn/CgxrRy
Android 车载开发岗位面试习题:https://qr18.cn/FTlyCJ
音视频面试题锦:https://qr18.cn/AcV6Ap

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值