作者简介: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。
- 生成一个
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;
}
}
- 生成一个
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);
...
}
}
- 类中增加一个静态变量
ChangeQuickRedirect changeQuickRedirect
- 在方法前插入一段代码,如果是需要修补的方法就执行补丁包中对应修复方法的相关逻辑,如果不是则执行原有逻辑。
- 美团 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支持补丁自动化生成,具体操作如下:
- 在修复完的方法上添加@Modify注解;
- 新创建的方法或类添加@Add注解。
- 工程添加依赖 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(具体补丁方法的实现)
- 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;
}
}
- 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;
}
}
- 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 });
}
}
补丁包的生成逻辑:
- 反射获取
PatchesInfoImpl
中补丁包映射关系,如PatchedClassInfo("com.meituan.sample.NeedModify", "com.meituan.robust.patch.NeedModifyPatchControl")。 - 反射获取
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