作者简介: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类为例

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热更新计划Robust
Android热补丁之Robust原理解析

最终的最终:
咱们是37手游移动客户端开发团队,致力于为游戏行业供给高质量的SDK开发服务。
欢迎关注咱们,了解更多移动开发和游戏 SDK 技术动态~
技术问题/交流/进群等能够加官方微信 MobileTeam37