Robust 源代码分析之gradle-plugin

本文深入解析热修复技术的工作原理,包括代码插桩、热修复代码加载及运行时处理。介绍了RobustTransform在编译期间如何通过Gradle插件对代码进行插桩,以及运行时如何通过自定义ClassLoader加载热修复Dex。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

工作原理简介

  1. 编译期间通过代码插桩,在每个方法的最前面插入一段代。这段代码的功能是:如果有热修复代码就走热修逻辑返回,不再走原方法逻辑。
  2. 运行期间,通过新建ClassLoader的方式,加载包含热修复代码的Dex。

编译期做了哪些工作

gradle脚本插件的入口方法在RobustTransform的apply方法中

void apply(Project target) {
    project = target
    robust = new XmlSlurper().parse(new File("${project.projectDir}/${Constants.ROBUST_XML}"))
    logger = project.logger
    initConfig()
    //isForceInsert 是true的话,则强制执行插入
    if (!isForceInsert) {
        if (!isDebugTask) {
            project.android.registerTransform(this)
            project.afterEvaluate(new RobustApkHashAction())
            logger.quiet "Register robust transform successful !!!"
        }
    } else {
        project.android.registerTransform(this)
        project.afterEvaluate(new RobustApkHashAction())
    }
}
复制代码

这个方法做了这些事情:

  1. 读取配置文件,初始化配置
  2. 如果是强制插入,则插入代码
  3. 如果是非强制插入,则Debug打包模式下不插入代码

初始化配置的代码如下:

def initConfig() {
    hotfixPackageList = new ArrayList<>()
    hotfixMethodList = new ArrayList<>()
    exceptPackageList = new ArrayList<>()
    exceptMethodList = new ArrayList<>()
    isHotfixMethodLevel = false;
    isExceptMethodLevel = false;
    /*对文件进行解析*/
    for (name in robust.packname.name) {
        hotfixPackageList.add(name.text());
    }
    for (name in robust.exceptPackname.name) {
        exceptPackageList.add(name.text());
    }
    for (name in robust.hotfixMethod.name) {
        hotfixMethodList.add(name.text());
    }
    for (name in robust.exceptMethod.name) {
        exceptMethodList.add(name.text());
    }

    if (null != robust.switch.filterMethod && "true".equals(String.valueOf(robust.switch.turnOnHotfixMethod.text()))) {
        isHotfixMethodLevel = true;
    }

    if (null != robust.switch.useAsm && "false".equals(String.valueOf(robust.switch.useAsm.text()))) {
        useASM = false;
    }else {
        //默认使用asm
        useASM = true;
    }

    if (null != robust.switch.filterMethod && "true".equals(String.valueOf(robust.switch.turnOnExceptMethod.text()))) {
        isExceptMethodLevel = true;
    }

    if (robust.switch.forceInsert != null && "true".equals(String.valueOf(robust.switch.forceInsert.text())))
        isForceInsert = true
    else
        isForceInsert = false

}
复制代码

分别读取了如下配置

  • packname 需要插入代码的包名或者类名
  • exceptPackname 不需要插入代码的包名或者类名
  • hotfixMethod 需要插入代码的方法名
  • exceptMethod 不需要插入代码的方法名
  • filterMethod与turnOnHotfixMethod 与hotfixMethod配合插桩
  • useAsm 是否使用Asm进行插入
  • filterMethod与turnOnExceptMethod 与exceptMethod配合过滤方法
  • forceInsert 强制插入,Debug是否也进行插入代码

如果需要插入代码,则将RobustTransform与RobustApkHashAction注册

RobustTransform

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);
    }else {
        insertcodeStrategy=new JavaAssistInsertImpl(hotfixPackageList,hotfixMethodList,exceptPackageList,exceptMethodList,isHotfixMethodLevel,isExceptMethodLevel);
    }
    insertcodeStrategy.insertCode(box, jarFile);
    writeMap2File(insertcodeStrategy.methodMap, Constants.METHOD_MAP_OUT_PATH)
}
复制代码

这个Transform如下工作

  1. 把所有需要打包的类放在一个列表里
  2. 根据配置对这些类遍历进行选择性插桩
  3. 将插桩完成的类输出到Transform的输出jar包中
  4. 输出插桩的方法与此方法的id(这个id就是自增1生成的),到一个Map中
  5. 将这个Map输出到 /outputs/robust/methodsMap.robust中

除了第二步骤,相对都比较简单,我们忽略其他步骤,主要看下第2步骤的实现

根据配置对这些类遍历进行选择性插桩

if(useASM){
    insertcodeStrategy=new AsmInsertImpl(hotfixPackageList,hotfixMethodList,exceptPackageList,exceptMethodList,isHotfixMethodLevel,isExceptMethodLevel);
}else {
    insertcodeStrategy=new JavaAssistInsertImpl(hotfixPackageList,hotfixMethodList,exceptPackageList,exceptMethodList,isHotfixMethodLevel,isExceptMethodLevel);
}
insertcodeStrategy.insertCode(box, jarFile);
复制代码

这里的插桩逻辑可以根据配置选择是使用asm还是javaassit,我们详细的看下ASM的实现

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");

        }
    }
    outStream.close();
}
复制代码
  1. 这里把所有类的修饰符都变成了public
  2. 如果类需要插桩(isNeedInsertClass返回true的基础上还要求不能是接口以及累的方法数大于1),则把插桩后的类输出到jar包中,否则把原始类输出到jar包中

类是否需要插桩的判断 isNeedInsertClass

protected boolean isNeedInsertClass(String className) {

    //这样子可以在需要埋点的剔除指定的类
    for (String exceptName : exceptPackageList) {
        if (className.startsWith(exceptName)) {
            return false;
        }
    }
    for (String name : hotfixPackageList) {
        if (className.startsWith(name)) {
            return true;
        }
    }
    return false;
}
复制代码

代码十分简单,如果这个类的名字在exceptPackageList中,则返回false;如果这个类的名字在hotfixPackageList中,则返回true

对类插桩 transformCode

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<>();
    cr.accept(classNode, 0);
    final List<MethodNode> methods = classNode.methods;
    for (MethodNode m : methods) {
        InsnList inList = m.instructions;
        boolean isMethodInvoke = false;
        for (int i = 0; i < inList.size(); i++) {
            if (inList.get(i).getType() == AbstractInsnNode.METHOD_INSN) {
                isMethodInvoke = true;
            }
        }
        methodInstructionTypeMap.put(m.name + m.desc, isMethodInvoke);
    }
    InsertMethodBodyAdapter insertMethodBodyAdapter = new InsertMethodBodyAdapter(cw, className, methodInstructionTypeMap);
    cr.accept(insertMethodBodyAdapter, ClassReader.EXPAND_FRAMES);
    return cw.toByteArray();
}
复制代码

这段代码是把这个类中的所有方法列出来,然后把方法名字和方法的descriptor拼接起来作为key值,这个方法内部是否有方法调用指令做为value值存储起来,传递给InsertMethodBodyAdapter去对方法进行插桩

InsertMethodBodyAdapter是这样事儿的

public InsertMethodBodyAdapter(ClassWriter cw, String className, Map<String, Boolean> methodInstructionTypeMap) {
    super(Opcodes.ASM5, cw);
    this.classWriter = cw;
    this.className = className;
    this.methodInstructionTypeMap = methodInstructionTypeMap;
    //insert the field
    classWriter.visitField(Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC, Constants.INSERT_FIELD_NAME, Type.getDescriptor(ChangeQuickRedirect.class), null, null);
}

@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
    if (isProtect(access)) {
        access = setPublic(access);
    }
    MethodVisitor mv = super.visitMethod(access, name,
            desc, signature, exceptions);
    if (!isQualifiedMethod(access, name, desc, methodInstructionTypeMap)) {
        return mv;
    }
    //record method number
    methodMap.put(className.replace('/', '.') + "." + name + "(" + parameters.toString() + ")", insertMethodCount.incrementAndGet());
    return new MethodBodyInsertor(mv, className, desc, isStatic(access), String.valueOf(insertMethodCount.get()), name, access);
}
复制代码

InsertMethodBodyAdapter显示对类插入了一个静态的ChangeQuickRedirect类型的字段changeQuickRedirect,然后就开始了对类的每个方法的访问,访问前判断如果方法是protected 那么就把方法修改为public 接着去判断是否方法需要插桩,下列情况的方法不进行插桩(isQualifiedMethod方法的逻辑,此处代码就不贴了)

  1. 方法是synthetic并且不是private的
  2. 方法是abstract,native,interface,decprecated的
  3. 方法在exceptMethodList中的
  4. 方法内没有调用其他方法的

如果需要插桩,用 MethodBodyInsertor对这个方法进行插桩

对方法插桩 transformCode

@Override
public void visitCode() {
    //insert code here
    RobustAsmUtils.createInsertCode(this, className, paramsTypeClass, returnType, isStatic, Integer.valueOf(methodId));
}

public static void createInsertCode(GeneratorAdapter mv, String className, List<Type> args, Type returnType, boolean isStatic, int methodId) {
    prepareMethodParameters(mv, className, args, returnType, isStatic, methodId);
    //开始调用
    mv.visitMethodInsn(Opcodes.INVOKESTATIC,
            PROXYCLASSNAME,
            "proxy",
            "([Ljava/lang/Object;Ljava/lang/Object;" + REDIRECTCLASSNAME + "ZI[Ljava/lang/Class;Ljava/lang/Class;)Lcom/meituan/robust/PatchProxyResult;",
            false);

    int local = mv.newLocal(Type.getType("Lcom/meituan/robust/PatchProxyResult;"));
    mv.storeLocal(local);
    mv.loadLocal(local);

    mv.visitFieldInsn(Opcodes.GETFIELD, "com/meituan/robust/PatchProxyResult", "isSupported", "Z");

    // if isSupported
    Label l1 = new Label();
    mv.visitJumpInsn(Opcodes.IFEQ, l1);

    //判断是否有返回值,代码不同
    if ("V".equals(returnType.getDescriptor())) {
        mv.visitInsn(Opcodes.RETURN);
    } else {
        mv.loadLocal(local);
        mv.visitFieldInsn(Opcodes.GETFIELD, "com/meituan/robust/PatchProxyResult", "result", "Ljava/lang/Object;");
        //强制转化类型
        if (!castPrimateToObj(mv, returnType.getDescriptor())) {
            //这里需要注意,如果是数组类型的直接使用即可,如果非数组类型,就得去除前缀了,还有最终是没有结束符;
            //比如:Ljava/lang/String; ==》 java/lang/String
            String newTypeStr = null;
            int len = returnType.getDescriptor().length();
            if (returnType.getDescriptor().startsWith("[")) {
                newTypeStr = returnType.getDescriptor().substring(0, len);
            } else {
                newTypeStr = returnType.getDescriptor().substring(1, len - 1);
            }
            mv.visitTypeInsn(Opcodes.CHECKCAST, newTypeStr);
        }

        //这里还需要做返回类型不同返回指令也不同
        mv.visitInsn(getReturnTypeCode(returnType.getDescriptor()));
    }

    mv.visitLabel(l1);
}
复制代码

终于来到对方法进行插桩的位置 RobustAsmUtils.createInsertCode,不过这段代码很ASM,在这里就不讲怎么使用ASM了,这段代码的大概意思是

  1. 把参数入操作栈,调用PatchProxy的proxy方法,这个方法会检测是否有热修代码,有的话则执行热修代码,没有则返回,这个方法的返回值PatchProxyResult,PatchProxyResult有两个字段,isSupported与result, result在isSupported为true的情况下为热修代码的返回值

  2. 检查PatchProxyResult的isSupported的值,如果为false,则走原方法代码逻辑,如果为true,分两种情况

  3. 无返回值,直接返回

  4. 有返回值,从PatchProxyResult的result取出结果返回

最终代码就是下面这种样子

//不带返回值的
private void sayHello() {
    if (!PatchProxy.proxy(new Object[0], this, n, false, 3, new Class[0], Void.TYPE).isSupported) {
        delegate.sayHello((Activity) this, 1);
    }
}
//带返回值的
private boolean isSpeakEnable() {
    PatchProxyResult proxy = PatchProxy.proxy(new Object[0], this, n, false, 2, new Class[0], Boolean.TYPE);
    return proxy.isSupported ? ((Boolean) proxy.result).booleanValue() : delegate.isSpeakEnable();
}
复制代码

至此插桩就完成了,回到最开始的RobustApkHashAction

RobustApkHashAction

这段代码就不贴了,因为真的是很简单,就是把代码res文件,dex文件,javaResource文件,jni文件,assets文件添加到一个zip包中,计算一下这个zip文件的hash值,然后在assets文件中创建一个robust.apkhash文件,把这个值写入即可

至此在打我们的应用包时,robust所做的工作就完成了,整体来看是比较简单的,流程也很清晰,唯一代码的阅读难点可能就是在方法插入代码时,需要一些ASM相关的知识以及需要懂一些jvm运行时的一些知识,才能读懂在方法中插入的指令有哪些作用以及为什么如此插入。

后续

后续还会针对robust写一篇解析文章,主要是针对打patch包时,robust做了些什么,robust加载patch包的代码非常简单,就不做分析了,另外,后续还会写一下字节码插桩相关内容的文章

转载于:https://juejin.im/post/5cc7f0915188252da4250717

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值