作者简介: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%) |
Robust 插件对APP应用Java层的每个方法都在编译打包阶段自动的插入了一段代码(备注:可以通过包名列表来配置需要插入代码的范围)。
通过判断if(changeQuickRedirect != null)
来确定是否进行热修复,当changeQuickRedirect
不为null
时,调用补丁包中patch.dex
中同名Patch类的同名方法达到 修复目的。
Hotfix
类为例public class Hotfix {
public int needToHotfix() {
return 0;
}
}
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;
}
}
主要包含两个class:PatchesInfoImpl.java和HotfixPatch.java。
PatchesInfoImpl
补丁包说明类,可以获取补丁对象;对象包含被修复类名及该类对应的补丁类。public class PatchesInfoImpl implements PatchesInfo {
public List getPatchedClassesInfo() {
List patchedClassesInfos = new ArrayList();
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 通过配置文件 robust.xml
来指定是否开启插桩、哪些包下需要插桩、哪些包下不需要插桩,在编译 Release 包时,RobustTransform 这个插件会自动遍历所有的类,并根据配置文件中指定的规则,对类进行以下操作:
class RobustTransform extends Transform implements Plugin {
@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
class JavaAssistInsertImpl {
@Override
protected void insertCode(List 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());
}
}
}
}
@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";
}
}
生成补丁包环节结束…
每个补丁包含以下三个部分:PatchesInfoImpl(补丁包说明类)、PatchControl(补丁类)、xxPatch(具体补丁方法的实现)
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;
}
}
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;
}
}
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
。jar2dex工具
将jar包
转换成dex包
。自定义PatchManipulate实现类,需要实现拉取补丁、校验补丁等逻辑。
public abstract class PatchManipulate {
/**
* 获取补丁列表
* @return 相应的补丁列表
*/
protected abstract List fetchPatchList(Context context);
/**
* 努力确保补丁文件存在,验证md5是否一致。
* 如果不存在,则动态下载
* @return 是否存在
*/
protected abstract boolean ensurePatchExist(Patch patch);
/**
* 验证补丁文件md5是否一致
* 如果不存在,则动态下载
* @return 校验结果
*/
protected abstract boolean verifyPatch(Context context, Patch patch);
}
当线上应用出现bug时,可以推送的方式通知客户端拉取对应的补丁包,下载补丁包完成后,会开一个子线程执行以下操作: (同时建议:在应用启动时,也执行一次更新补丁包操作)
// 1. 拉取补丁列表
List 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 性能优化篇: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