主要技术: gradle plugin, groovy, java代码插桩(ASM(默认使用asm), javassist(容易和其他字节码工具相互干扰)), DexClassLoader 补丁加载
美团热更插件Robust源码浅析
主要插件
autopatchbase 插件基类包
gradle-plugin 代码插桩插件
默认使用ASM插桩方案
一. 核心调用链:
- 脚本执行入口
RobustTransform.transform() - Class转换为javassist的CtClass资源
ConvertUtils.toCtClasses() - 创建inserCode策略
AsmInsertImpl(默认)或者JavassistInsertImpl - 使用inserCode策略 执行
insertCode(ctClassList, jarFile) - 将映射关系写入到map文件中
writeMap2File(methodMap, mapPath)核心transform代码如下:
@Override
void transform(Context context, Collection<TransformInput> inputs, Collection<TransformInput> referencedInputs, TransformOutputProvider outputProvider, boolean isIncremental) throws IOException, TransformException, InterruptedException {
logger.quiet '================robust start================'
def startTime = System.currentTimeMillis()
outputProvider.deleteAll()
File jarFile = outputProvider.getContentLocation("main", getOutputTypes(), getScopes(),
Format.JAR);
if(!jarFile.getParentFile().exists()){
jarFile.getParentFile().mkdirs();
}
if(jarFile.exists()){
jarFile.delete();
}
ClassPool classPool = new ClassPool()
project.android.bootClasspath.each {
classPool.appendClassPath((String) it.absolutePath)
}
def box = ConvertUtils.toCtClasses(inputs, classPool)
def cost = (System.currentTimeMillis() - startTime) / 1000
// logger.quiet "check all class cost $cost second, class count: ${box.size()}"
if (useASM) {
insertcodeStrategy = new AsmInsertImpl(hotfixPackageList, hotfixMethodList, exceptPackageList, exceptMethodList, isHotfixMethodLevel, isExceptMethodLevel, isForceInsertLambda);
} else {
insertcodeStrategy = new JavaAssistInsertImpl(hotfixPackageList, hotfixMethodList, exceptPackageList, exceptMethodList, isHotfixMethodLevel, isExceptMethodLevel, isForceInsertLambda);
}
insertcodeStrategy.insertCode(box, jarFile);
writeMap2File(insertcodeStrategy.methodMap, Constants.METHOD_MAP_OUT_PATH)
logger.quiet "===robust print id start==="
for (String method : insertcodeStrategy.methodMap.keySet()) {
int id = insertcodeStrategy.methodMap.get(method);
System.out.println("key is " + method + " value is " + id);
}
logger.quiet "===robust print id end==="
cost = (System.currentTimeMillis() - startTime) / 1000
logger.quiet "robust cost $cost second"
logger.quiet '================robust end================'
}
二. ASM代码插桩核心策略
- InsertCodeStrategy核心实现类:
AsmInsertImpl
@Override
protected void insertCode(List<CtClass> box, File jarFile) throws IOException, CannotCompileException {
ZipOutputStream outStream = new JarOutputStream(new FileOutputStream(jarFile));
//get every class in the box ,ready to insert code
for (CtClass ctClass : box) {
//change modifier to public ,so all the class in the apk will be public ,you will be able to access it in the patch
ctClass.setModifiers(AccessFlag.setPublic(ctClass.getModifiers()));
if (isNeedInsertClass(ctClass.getName()) && !(ctClass.isInterface() || ctClass.getDeclaredMethods().length < 1)) {
//only insert code into specific classes 插桩代码
zipFile(transformCode(ctClass.toBytecode(), ctClass.getName().replaceAll("\\.", "/")), outStream, ctClass.getName().replaceAll("\\.", "/") + ".class");
} else {
zipFile(ctClass.toBytecode(), outStream, ctClass.getName().replaceAll("\\.", "/") + ".class");
}
ctClass.defrost();
}
outStream.close();
}
核心代码在,这一行中zipFile(transformCode(ctClass.toBytecode(), ctClass.getName().replaceAll("\\.", "/")), outStream, ctClass.getName().replaceAll("\\.", "/") + ".class");
插桩实现在方法transformCode中, 包含两个参数: ctClass的字节码, ctClass的name; 源码如下:
public byte[] transformCode(byte[] b1, String className) throws IOException {
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
ClassReader cr = new ClassReader(b1);
ClassNode classNode = new ClassNode();
Map<String, Boolean> methodInstructionTypeMap = new HashMap<>();
// 1. 写入class 节点
cr.accept(classNode, 0);
final List<MethodNode> methods = classNode.methods;
// 2. 遍历方法节点
// methods 是当前类的所有方法节点列表。每个 MethodNode 表示一个方法的结构信息,包括方法名、描述符、指令列表等。
for (MethodNode m : methods) {
// 获取指令列表
InsnList inList = m.instructions;
// 定义一个布尔变量 isMethodInvoke,用于标记当前方法是否包含方法调用指令。
boolean isMethodInvoke = false;
for (int i = 0; i < inList.size(); i++) {
// 判断是否是调用指令
if (inList.get(i).getType() == AbstractInsnNode.METHOD_INSN) {
isMethodInvoke = true;
}
}
// 将方法的唯一标识(m.name + m.desc,即方法名和描述符)作为键,isMethodInvoke 作为值,存入 methodInstructionTypeMap 映射表。
// 这个映射表的作用是记录每个方法是否包含方法调用指令。
methodInstructionTypeMap.put(m.name + m.desc, isMethodInvoke);
}
// 初始化 InsertMethodBodyAdapter 对象,用于在方法体中插入代码。
InsertMethodBodyAdapter insertMethodBodyAdapter = new InsertMethodBodyAdapter(cw, className, methodInstructionTypeMap);
// cr.accept 方法用于读取类文件,并将读取到的信息传递给 InsertMethodBodyAdapter 对象。 具体实现代码在InsertMethodBodyAdapter的visitMethod方法中
cr.accept(insertMethodBodyAdapter, ClassReader.EXPAND_FRAMES);
return cw.toByteArray();
}
这种检查机制在字节码操作中非常常见,尤其是在需要对特定类型的方法进行插桩或修改时,可以用来过滤掉不需要处理的方法,从而提高效率和准确性。
执行完cr.accept(insertMethodBodyAdapter, ClassReader.EXPAND_FRAMES)之后最终的代码插桩代码实现在RobustAsmUtils.java的createInsertCode方法中
---
auto-patch-plugin 自动生成补丁插件
执行 assemble release 生成补丁包 主要生成类: AutoPatchTransform.groovy
- 利用
javassist使用ReflectUtils.toCtClasses()获取所有的class资源 - 利用
javassist使用ReadAnnotation.readAnnotation()读取所有的robust注解(@Modify @Add),并存储到对应的ArrayList集合中 modify添加到modifiedClassNameList中, add添加到newlyAddedClassNameList中 - 调用generatPatch()方法生成补丁文件
内联类(Inline Class)是一种优化技术,通常用于在编译时将某个类的实现直接嵌入到调用者类中。这种方式可以减少运行时的开销,避免类加载和方法调用的额外成本,同时也可以简化某些复杂的依赖关系。 在 Robust 框架中,内联类的概念被用来处理新增或修改的类,确保这些类能够在补丁生成过程中正确地嵌入到目标类中,从而实现热修复功能。
def generatPatch(List<CtClass> box,String patchPath){
if (!Config.isManual) {
if (Config.patchMethodSignatureSet.size() < 1) {
throw new RuntimeException(" patch method is empty ,please check your Modify annotation or use RobustModify.modify() to mark modified methods")
}
Config.methodNeedPatchSet.addAll(Config.patchMethodSignatureSet)
//1.处理inline类 和 inline方法
InlineClassFactory.dealInLineClass(patchPath, Config.newlyAddedClassNameList)
//2.处理父类方法
initSuperMethodInClass(Config.modifiedClassNameList);
//auto generate all class
for (String fullClassName : Config.modifiedClassNameList) {
CtClass ctClass = Config.classPool.get(fullClassName)
CtClass patchClass = PatchesFactory.createPatch(patchPath, ctClass, false, NameManger.getInstance().getPatchName(ctClass.name), Config.patchMethodSignatureSet)
patchClass.writeFile(patchPath)
patchClass.defrost();
createControlClass(patchPath, ctClass)
}
createPatchesInfoClass(patchPath);
if (Config.methodNeedPatchSet.size() > 0) {
throw new RuntimeException(" some methods haven't patched,see unpatched method list : " + Config.methodNeedPatchSet.toListString())
}
} else {
autoPatchManually(box, patchPath);
}
}
第一步: InlineClassFactory.dealInLineClass(patchPath, Config.newlyAddedClassNameList)处理inline类 和 inline方法
//该方法的核心
InlineClassFactory.dealInLineClass(patchPath, Config.newlyAddedClassNameList)
inlineClassFactory.dealInLineClass(patchPath)
inlineClassFactory.dealInLineMethodInNewAddClass(patchPath, list)
详细逻辑分解
-
dealInLineClass(patchPath)- 作用:处理所有需要内联的类。
- 具体步骤:
- 初始化一个集合
usedClass,包含所有新增的类名(Config.newlyAddedClassNameList)。 - 调用
getAllInlineClasses方法,递归查找所有可能的内联类及其方法。getAllInlineClasses会根据类的依赖关系,使用javassist的ExprEditor工具逐步扩展内联类的集合,直到没有新的内联类被发现为止。
- 创建钩子类(Hook Inline Class),用于捕获内联类的行为。
- 针对每个内联类,生成对应的补丁类(Patch Class),并将其写入到指定路径
patchPath。
- 初始化一个集合
-
dealInLineMethodInNewAddClass(patchPath, list)- 作用:处理新增类中的内联方法。
- 具体步骤:
- 遍历
list中的所有新增类。 - 对每个新增类的声明方法进行分析,查找其中的内联方法调用。
- 使用
javassist的ExprEditor工具,替换内联方法的调用为具体的实现代码。 - 将修改后的类写入到指定路径
patchPath。
- 遍历
总结
InlineClassFactory.dealInLineClass(patchPath, Config.newlyAddedClassNameList) 的主要功能是:
- 找出所有需要内联的类及其方法。
- 生成对应的补丁类,确保内联类的逻辑能够正确嵌入到目标类中。
- 处理新增类中的内联方法调用,替换为具体的实现代码。
这种设计的核心目的是为了优化热修复过程中的类加载和方法调用效率,同时保证补丁生成的完整性和正确性。
第二步: initSuperMethodInClass(Config.modifiedClassNameList)
- 找出所有需要内联的类及其方法。
- 生成对应的补丁类,确保内联类的逻辑能够正确嵌入到目标类中。
- 处理新增类中的内联方法调用,替换为具体的实现代码。
这种设计的核心目的是为了优化热修复过程中的类加载和方法调用效率,同时保证补丁生成的完整性和正确性。
initSuperMethodInClass 方法的作用是初始化并记录修改类中调用父类方法的列表。具体来说,它会遍历传入的类列表(originClassList),检查每个类中是否调用了父类方法(super 方法),并将这些方法记录到 Config.invokeSuperMethodMap 中。此外,该方法还会对修改类中的方法进行过滤,确保只处理需要打补丁的方法或内联方法。
第三步: 遍历所有需要打补丁的类(非内联)
//auto generate all class
for (String fullClassName : Config.modifiedClassNameList) {
CtClass ctClass = Config.classPool.get(fullClassName)
// 创建补丁类 (核心代码)
CtClass patchClass = PatchesFactory.createPatch(patchPath, ctClass, false, NameManger.getInstance().getPatchName(ctClass.name), Config.patchMethodSignatureSet)
// 写入补丁到磁盘
patchClass.writeFile(patchPath)
// 解冻后以便后续修改
patchClass.defrost();
// 创建控制类 用于管理补丁类的加载和执行
createControlClass(patchPath, ctClass)
}
// 创建补丁信息类
createPatchesInfoClass(patchPath);
packagePatchDex2Jar() 将生成的补丁类打包成jar 供运行时加载使用
patch 加载补丁java包 核心: DexClassLoader 加载补丁
DexClassLoader是Android提供的专门用于动态加载Dex文件的类加载器,与PathClassLoader(用于加载应用自身dex)不同,它可以从任意存储路径加载dex文件。
创建本地补丁加载线程 PatchExecutor 详细注释代码如下
/**
* 应用补丁到应用程序。
*
* 该方法首先通过 {@link patchManipulate#verifyPatch(Context, Patch)} 验证补丁信息。
* 如果验证失败,则记录错误日志并返回 false。
* 然后尝试加载补丁类并应用补丁,替换特定字段。
*
* @param context 应用程序的上下文,用于访问应用程序特定的资源。
* @param patch 包含补丁信息的对象,包括补丁文件路径、MD5 等。
* @return 如果补丁成功应用则返回 true,否则返回 false。
*/
protected boolean patch(Context context, Patch patch) {
// 验证补丁信息
if (!patchManipulate.verifyPatch(context, patch)) {
robustCallBack.logNotify("verifyPatch failure, patch info:" + "id = " + patch.getName() + ",md5 = " + patch.getMd5(), "class:PatchExecutor method:patch line:107");
return false;
}
ClassLoader classLoader = null;
try {
// 获取补丁缓存目录并创建 DexClassLoader 加载补丁
File dexOutputDir = getPatchCacheDirPath(context, patch.getName() + patch.getMd5());
classLoader = new DexClassLoader(patch.getTempPath(), dexOutputDir.getAbsolutePath(),
null, PatchExecutor.class.getClassLoader());
} catch (Throwable throwable) {
throwable.printStackTrace();
}
if (null == classLoader) {
// 如果无法创建 ClassLoader,则返回 false
return false;
}
Class patchClass, sourceClass;
Class patchesInfoClass;
PatchesInfo patchesInfo = null;
try {
// 加载补丁信息类并实例化
Log.d("robust", "patch patch_info_name:" + patch.getPatchesInfoImplClassFullName());
patchesInfoClass = classLoader.loadClass(patch.getPatchesInfoImplClassFullName());
patchesInfo = (PatchesInfo) patchesInfoClass.newInstance();
} catch (Throwable t) {
Log.e("robust", "patch failed 188 ", t);
}
if (patchesInfo == null) {
// 如果补丁信息为空,记录日志并返回 false
robustCallBack.logNotify("patchesInfo is null, patch info:" + "id = " + patch.getName() + ",md5 = " + patch.getMd5(), "class:PatchExecutor method:patch line:114");
return false;
}
// 获取需要修补的类列表
List<PatchedClassInfo> patchedClasses = patchesInfo.getPatchedClassesInfo();
if (null == patchedClasses || patchedClasses.isEmpty()) {
// 如果类列表为空,手写的补丁可能返回空列表,直接返回 true
return true;
}
boolean isClassNotFoundException = false;
for (PatchedClassInfo patchedClassInfo : patchedClasses) {
String patchedClassName = patchedClassInfo.patchedClassName;
String patchClassName = patchedClassInfo.patchClassName;
if (TextUtils.isEmpty(patchedClassName) || TextUtils.isEmpty(patchClassName)) {
// 如果类名为空,记录日志并跳过
robustCallBack.logNotify("patchedClasses or patchClassName is empty, patch info:" + "id = " + patch.getName() + ",md5 = " + patch.getMd5(), "class:PatchExecutor method:patch line:131");
continue;
}
Log.d("robust", "current path:" + patchedClassName);
try {
try {
// 加载需要修补的类
sourceClass = classLoader.loadClass(patchedClassName.trim());
} catch (ClassNotFoundException e) {
isClassNotFoundException = true;
continue;
}
// 获取类的所有字段
Field[] fields = sourceClass.getDeclaredFields();
Log.d("robust", "oldClass :" + sourceClass + " fields " + fields.length);
Field changeQuickRedirectField = null;
for (Field field : fields) {
// 查找 ChangeQuickRedirect 类型的字段
if (TextUtils.equals(field.getType().getCanonicalName(), ChangeQuickRedirect.class.getCanonicalName()) && TextUtils.equals(field.getDeclaringClass().getCanonicalName(), sourceClass.getCanonicalName())) {
changeQuickRedirectField = field;
break;
}
}
if (changeQuickRedirectField == null) {
// 如果未找到 ChangeQuickRedirect 字段,记录日志并跳过
robustCallBack.logNotify("changeQuickRedirectField is null, patch info:" + "id = " + patch.getName() + ",md5 = " + patch.getMd5(), "class:PatchExecutor method:patch line:147");
Log.d("robust", "current path:" + patchedClassName + " something wrong !! can not find:ChangeQuickRedirect in" + patchClassName);
continue;
}
Log.d("robust", "current path:" + patchedClassName + " find:ChangeQuickRedirect " + patchClassName);
try {
// 加载补丁类并实例化,设置 ChangeQuickRedirect 字段
patchClass = classLoader.loadClass(patchClassName);
Object patchObject = patchClass.newInstance();
changeQuickRedirectField.setAccessible(true);
changeQuickRedirectField.set(null, patchObject);
Log.d("robust", "changeQuickRedirectField set success " + patchClassName);
} catch (Throwable t) {
Log.e("robust", "patch failed! ");
robustCallBack.exceptionNotify(t, "class:PatchExecutor method:patch line:163");
}
} catch (Throwable t) {
Log.e("robust", "patch failed! ");
}
}
Log.d("robust", "patch finished ");
if (isClassNotFoundException) {
// 如果发生 ClassNotFoundException,返回 false
return false;
}
return true;
}
Robust中的特殊处理
在Robust框架中,对标准流程有以下增强:
-
类替换策略:
- 不是简单覆盖原有类,而是通过插入的检查逻辑决定是否使用补丁类
- 每个方法调用前会检查是否有补丁版本
-
多Dex支持:
- 处理Android的多Dex情况
- 确保补丁能正确应用到所有相关的Dex文件
-
安全验证:
- 校验补丁文件的签名和完整性
- 防止恶意补丁注入
底层实现细节
-
DexFile加载:
- 最终通过native方法
openDexFile()加载dex - 生成优化的odex文件(在optimizedDirectory中)
- 最终通过native方法
-
类查找顺序:
- 补丁DexClassLoader → 原应用PathClassLoader → BootClassLoader
- Robust通过修改这一顺序实现热修复
-
方法替换:
- 不是真正的类替换,而是通过方法调用重定向
- 使用Java反射或native hook技术
典型问题与解决方案
-
类验证问题:
- Android 5.0+对dex文件有严格验证
- Robust通过预先处理避免验证失败
-
资源冲突:
- 补丁中的资源ID需与主应用一致
- 通常资源修复需要单独处理
-
兼容性问题:
- 不同Android版本ClassLoader实现差异
- Robust通过版本判断采用不同策略
Java ASM 动态插桩与Javassist的区别
ASM和Javassist都是Java字节码操作工具,用于实现动态插桩(Instrumentation),但它们在设计理念和使用方式上有显著区别:
主要区别
| 特性 | ASM | Javassist |
|---|---|---|
| 抽象级别 | 低级字节码操作 | 高级源代码级别操作 |
| 学习曲线 | 较陡峭,需要了解JVM字节码 | 较平缓,使用类似Java的语法 |
| 性能 | 更高 | 相对较低 |
| 使用方式 | 直接操作字节码指令 | 通过字符串拼接Java代码 |
| 灵活性 | 极高,可以完成任何字节码修改 | 有限,受限于其API能表达的转换 |
| 典型应用场景 | 高性能需求、复杂字节码转换 | 快速原型开发、简单类修改 |
详细对比
1. 抽象级别
- ASM:直接操作JVM字节码,提供的是指令级别的控制
- Javassist:允许开发者以类似Java源代码的形式编写插入的代码
2. 代码示例对比
ASM示例(方法进入时插入代码):
MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions);
mv.visitCode();
mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitLdcInsn("Method entered");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
// 原始方法代码...
Javassist示例:
CtMethod method = ...;
method.insertBefore("{ System.out.println(\"Method entered\"); }");
3. 性能考虑
- ASM通常比Javassist快一个数量级,因为:
- 直接操作字节码,没有源代码解析步骤
- 更少的内存开销
- 更优化的内部实现
4. 功能完整性
- ASM可以处理所有合法的Java类文件,包括Java 8的lambda表达式和Java 11的嵌套类
- Javassist在某些高级字节码特性上可能有限制
5. 使用场景建议
选择ASM当:
- 需要最高性能
- 进行复杂的字节码转换
- 处理最新的Java特性
- 在大型项目或生产环境中使用
选择Javassist当:
- 快速原型开发
- 简单的类修改
- 不熟悉JVM字节码
- 开发时间比运行时性能更重要
两者在Java动态插桩领域都有广泛应用,选择取决于项目具体需求和团队熟悉程度。