作者简介: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。
- 生成一个
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;
}
}
- 生成一个
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);
...
}
}
- 类中添加一个静态变量
ChangeQuickRedirect changeQuickRedirect
- 在办法前刺进一段代码,假如是需求修补的办法就履行补丁包中对应修正办法的相关逻辑,假如不是则履行原有逻辑。
- 美团 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支撑补丁自动化生成,详细操作如下:
- 在修正完的办法上添加@Modify注解;
- 新创立的办法或类添加@Add注解。
- 工程添加依赖 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(详细补丁办法的完结)
- 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;
}
}
- 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;
}
}
- 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 });
}
}
补丁包的生成逻辑:
- 反射获取
PatchesInfoImpl
中补丁包映射关系,如PatchedClassInfo(“com.meituan.sample.NeedModify”, “com.meituan.robust.patch.NeedModifyPatchControl”)。 - 反射获取
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