大家好,我是瑞英。
本篇会使用具体实例详细分析一下,基于美团开源热修框架中,对base包的插桩方案所进行的优化。目的:减小包体。
名词解析:
- base包:宿主包,可能拉取热修并加载完成,最终使得问题被修复的线上包。
- patch包:热修包,基于base包打出的能够解决线上问题的包。
原始美团base包插桩方案解析
-
为类插入一个重定向属性,changeQuickRedirect
public interface ChangeQuickRedirect { Object accessDispatch(String methodName, Object[] paramArrayOfObject); boolean isSupport(String methodName, Object[] paramArrayOfObject); }
-
为方法体中插入PatchProxy.proxy()方法调用
/** * * @param paramsArray[] 参数数组 * @param current 当前类对象this * @param changeQuickRedirect 重定向对象 * @param isStatic 当前方法是否为静态方法 * @param methodNumber 方法编号 * @param paramsClassTypes 参数类型数组 * @param returnType 返回类型 * @return */ public class PatchProxy { public static PatchProxyResult proxy(Object[] paramsArray, Object current, ChangeQuickRedirect changeQuickRedirect, boolean isStatic, int methodNumber, Class[] paramsClassTypes, Class returnType) { PatchProxyResult patchProxyResult = new PatchProxyResult(); if (PatchProxy.isSupport(paramsArray, current, changeQuickRedirect, isStatic, methodNumber, paramsClassTypes, returnType)) { patchProxyResult.isSupported = true; patchProxyResult.result = PatchProxy.accessDispatch(paramsArray, current, changeQuickRedirect, isStatic, methodNumber, paramsClassTypes, returnType); } return patchProxyResult; } }
上述两个步骤在一个Test1示例上,字节码插桩后,结果如下:
public final class Test1 {
public static ChangeQuickRedirect changeQuickRedirect; // 热修重定向属性
private Test1() {
}
public final boolean test(boolean r8, String string) {
// PatchProxy.proxy调用
PatchProxyResult proxy = PatchProxy.proxy(new Object[]{new Byte(r8 ? (byte) 1 : (byte) 0), string}, this, changeQuickRedirect, false, 237, Boolean.TYPE);
if (proxy.isSupported) {
return ((Boolean) proxy.result).booleanValue();
}
return r8;
}
}
如果对上述Test1类进行热修
- 首先会打出一个Test1PatchControl用于将原方法调用转发到热修方法中
public class Test1PatchControl implements ChangeQuickRedirect {
/**
methodName格式:【className + ":" + methodName + ":" + isStatic + ":" + methodNumber】
根据方法名,将当前调用分发到对应的修复方法
*/
public Object accessDispatch(String methodName, Object[] paramArrayOfObject) {
// 调用匹配的修复方法
Tes1Patch.test()
return null;
}
public boolean isSupport(String methodName, Object[] paramArrayOfObject) {
// 根据方法编号,判定当前方法是否被热修
}
}
-
其次会打出一个Test1Patch类,包含了被修复的test()方法
-
最后会打出PatchesInfo类,记录被热修的类信息
public class PatchesInfoImpl implements PatchesInfo { public List getPatchedClassesInfo() { ArrayList var1 = new ArrayList(); PatchedClassInfo var2 = new PatchedClassInfo("com.test.Test1", "com.test.Test1PatchControl"); } }
加载热修包时,会通过反射对被修复类的静态属性changeQuickRedirect注入值,实现的效果是:
Test1.changeQuickRedirect = new Test1PatchControl()
优化后的base插桩
对上述的插桩方案进行仔细分析,进行优化
- 不再对每个base包中的类插入一个静态changeQuickRedirect属性,同时proxy方法中也无需传递这个参数
加载热修包时,被修复类的信息可以直接存在一个静态map中,proxy()方法中,直接读取这个map的信息来判断当前类有无对应修复类
- 精简proxy方法传递的参数
根据被插桩方法的参数和返回值特性,调用不同的proxy方法.
目的:使得base插入的指令尽可能的少
-
根据返回值类型划分
- 无返回值,则proxy方法直接返回true/false,表示当前方法是否被热修,如此被插桩方法中不需要出现result.isSupport的判断语句,再决定当前方法是否被热修
fun proxyWithoutRes(...省略): Boolean { return proxy(...).isSupported }
- 有返回值,则proxy方法需要返回result
fun proxy(...省略): Result { return result }
-
根据被插桩方法的参数个数划分
只有5个以上的参数方法被插桩时,需要采用Object[]数组传递所有的参数。因为构建数组并且初始化数组元素,需要的指令较多。
若方法只有一个参数,那么直接传递object对象只需要1条指令,如果通过Object[]传递该对象需要6条指令,如下所示:
//有一个参数str:String,存放与局部变量表中 index = 1
//直接传递该object对象
mv.visitMethodInsn(ALOAD, 1)
//利用object数组进行传递
mv.visitInsn(1)//数组大小
mv.visitTypeInsn(Opcodes.ANEWARRAY, "java/lang/Object")
mv.visitInsn(Opcodes.DUP)// 创建数组object[]
mv.visitInsn(Opcodes.ICONST_0)// 下标索引
mv.visitVarInsn(Opcodes.ALOAD, 1) //获取局部变量表中该object对象
mv.visitInsn(Opcodes.AASTORE) //存入数组中
-
去掉原有传递的参数:isStatic
直接根据传入的当前类对象current:Object,如果为空,就可确定为被插桩方法是静态方法
-
去掉原有传递的参数:被插桩方法的参数类型数组和返回值类型
Class[] paramsClassTypes, Class returnType
热修插桩生成Test1PatchControl.accessDispatch()方法中,会对每个参数进行类型转换(编译时就能够拿到参数的各个类型,不需要特意传递),returnType同理
-
ChangeQuickRedirect接口中定义接口方法参数变动[为了适配PatchProxy.proxy方法参数的变动]
最终PatchProxy.proxy方法被精简为下图所示:
上述Test1使用优化后的base插桩方案生成的字节码,如下:[明显指令数减少]
public final class Test1 {
private Test1() {
}
public final boolean test(boolean r8, String string) {
PatchProxyResult proxyPara2 = PatchProxy.proxyPara2(new Boolean(r8), string, this, Test1.class, 138);
if (proxy2Para.isSupported) {
return ((Boolean) proxy2Para.result).booleanValue();
}
return r8;
}
}