android 基于agp AsmClassVisitorF

android项目引用了大量第三方库后,做得好的库,会提供接口给调用方设置统一的线程池,差一点的库在内部使用统一的线程池,但是难免遇到这种库:不仅没有提供接口给调用方设置线程池,而且内部还不使用线程池,大量直接创建线程并运行。

今天就针对这种场景通过插桩实践下治理它们。

主角是agp 7.0之后提供的新api AsmClassVisitorFactory。

先定义插件

(为了方便调试,这里没有使用单独的工程来开发插件,直接将插件定义到buildSrc里)

public class NewThreadPlugin implements Plugin<Project> {
    @Override
    public void apply(Project target) {
        AndroidComponentsExtension comp = target.getExtensions().getByType(AndroidComponentsExtension.class);
        comp.onVariants(comp.selector().all(), new Action<Component>() {
            @Override
            public void execute(Component variant) {
                variant.transformClassesWith(NewThreadVisitorFactory.class, InstrumentationScope.ALL, v -> null);
                variant.setAsmFramesComputationMode(FramesComputationMode.COMPUTE_FRAMES_FOR_INSTRUMENTED_METHODS);
            }
        });
    }
}

创建注册文件 resources/META-INF/gradle-plugins/test.thread.properties

implementation-class=com.test.plugins.newthread.NewThreadPlugin

插件开发

在开发字节码插件时,对字节码不熟悉怎么办?

  1. 先了解下asm的api
  2. 通过其他工具得到目标代码的asm代码

我主要使用ide的ASM Bytecode Outline和ASM Bytecode Viewer。

如下面这段代码:

public void test7(Runnable runnable) {
    Thread thread = new Thread(runnable);
    thread.setName("test7");
    thread.start();
}

通过插件编译后的asm代码为:

{ methodVisitor = classWriter.visitMethod(ACC_PUBLIC, "test7", "(Ljava/lang/Runnable;)V", null, null); methodVisitor.visitCode(); Label label0 = new Label(); methodVisitor.visitLabel(label0); methodVisitor.visitLineNumber(68, label0); methodVisitor.visitTypeInsn(NEW, "java/lang/Thread"); methodVisitor.visitInsn(DUP); methodVisitor.visitVarInsn(ALOAD, 1); methodVisitor.visitMethodInsn(INVOKESPECIAL, "java/lang/Thread", "<init>", "(Ljava/lang/Runnable;)V", false); methodVisitor.visitVarInsn(ASTORE, 2); Label label1 = new Label(); methodVisitor.visitLabel(label1); methodVisitor.visitLineNumber(69, label1); methodVisitor.visitVarInsn(ALOAD, 2); methodVisitor.visitLdcInsn("test7"); methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Thread", "setName", "(Ljava/lang/String;)V", false); Label label2 = new Label(); methodVisitor.visitLabel(label2); methodVisitor.visitLineNumber(70, label2); methodVisitor.visitVarInsn(ALOAD, 2); methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Thread", "start", "()V", false); Label label3 = new Label(); methodVisitor.visitLabel(label3); methodVisitor.visitLineNumber(71, label3); methodVisitor.visitInsn(RETURN); Label label4 = new Label(); methodVisitor.visitLabel(label4); methodVisitor.visitLocalVariable("this", "Lsg/bigo/tanwei/asm/TestCode;", null, label0, label4, 0); methodVisitor.visitLocalVariable("runnable", "Ljava/lang/Runnable;", null, label0, label4, 1); methodVisitor.visitLocalVariable("thread", "Ljava/lang/Thread;", null, label1, label4, 2); methodVisitor.visitMaxs(3, 3); methodVisitor.visitEnd(); }

分析目标代码的结构,可以得出一些关键性的指令:

methodVisitor.visitMethodInsn(INVOKESPECIAL, "java/lang/Thread", "<init>", "(Ljava/lang/Runnable;)V", false);
methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Thread", "start", "()V", false);

关于Thread的其他方法调用类似,然后开始写插桩逻辑并测试

插桩逻辑

  1. 经过上面的步骤,我们知道了目标代码的指令特征,先遍历字节码,然后匹配到调用代码。
  2. 匹配到目标调用代码后,将与创建Runnable实例无关的指令删除掉:如new Thead,Thread.start等。
  3. 提取出Runnable实例,并将Runnable实例传给一个新的方法,在新的方法里会将这个Runnable实例丢给Executors来执行,当然也可以根据业务来决定,比如做个ab实验。
  4. 将3的指令替换掉Thread.start这句指令(当然在2里面删掉了,这里重新添加也行;在2保留是为了方便定位)
public class NewThreadClassVisitor extends ClassNode {

    public NewThreadClassVisitor(ClassVisitor visitor) {
        super(Opcodes.ASM7);
        classVisitor = visitor;
    }

    @Override
    public void visitEnd() {

        methods.forEach(new Consumer<MethodNode>() {
            @Override
            public void accept(MethodNode methodNode) {
                handleMethodInsn(methodNode);
            }
        });

        if (classVisitor != null) {
            accept(classVisitor);
        }
    }

    private void handleMethodInsn(MethodNode methodNode) {
        InsnList insnList = methodNode.instructions;
        ListIterator<AbstractInsnNode> iterator = insnList.iterator();
        while (iterator.hasNext()) {
            AbstractInsnNode insnNode = iterator.next();
            if (insnNode instanceof MethodInsnNode) {
                MethodInsnNode methodInsn = (MethodInsnNode) insnNode;
                if (checkIsTargetInvoke(methodInsn)) {

                    removeUnusedInsn(methodNode, insnList, methodInsn);

                    MethodInsnNode newMethodInsn = new MethodInsnNode(Opcodes.INVOKESTATIC,
                            HANDLER_CLASS_NAME, HANDLER_METHOD_NAME, HANDLER_METHOD_DESC);
                    insnList.set(methodInsn, newMethodInsn);

                    System.out.println(TAG + "after handle");
                    for (AbstractInsnNode node : methodNode.instructions) {
                        System.out.println("   insn:  type=" + node.getType() + ",opCode=" + node.getOpcode() + ", toStr=" + node);
                    }
                }
            }
        }
    }

    // 仅针对Thread调用包含Runnable参数的构造方法
    private boolean checkIsTargetInvoke(MethodInsnNode methodInsn) {
        int opcode = methodInsn.getOpcode();
        boolean isTarget = (opcode == Opcodes.INVOKESPECIAL && THRED_CLASS_NAME.equals(methodInsn.owner)
                && CONSTRUCTOR_METHOD.equals(methodInsn.name));
        if (isTarget) {
            AbstractInsnNode next = methodInsn.getNext();
            if (next instanceof FieldInsnNode && next.getOpcode() == Opcodes.PUTFIELD) {
                FieldInsnNode fieldNode = (FieldInsnNode) next;
                if (THRED_TYPE_DESC.equals(fieldNode.desc)) {
                    System.out.println(TAG + "checkIsTargetInvoke ignore: next is PUTFIELD: name=" + fieldNode.name + ",desc=" + fieldNode.desc);
                    return false;
                }
            } else {
                // 仅构造Thread但是未调用start,例如只是创建对象并返回
                if (next.getOpcode() == Opcodes.ARETURN) {
                    return false;
                }
                boolean callStart = false;
                while (next != null) {
                    if (next.getOpcode() >= Opcodes.IRETURN && next.getOpcode() <= Opcodes.RETURN) {
                        break;
                    }
                    if (next instanceof MethodInsnNode) {
                        MethodInsnNode node = (MethodInsnNode) next;
                        if (THRED_CLASS_NAME.equals(node.owner) && START_METHOD_NAME.equals(node.name)) {
                            callStart = true;
                            break;
                        }
                    }
                    next = next.getNext();
                }
                if (!callStart) {
                    System.out.println(TAG + "checkIsTargetInvoke ignore: not call start");
                    return false;
                }
            }
            String desc = methodInsn.desc;
            System.out.println("\n");
            if (desc.contains(RUNNABLE_CLASS_NAME)) {
                System.out.println(TAG + "checkIsTargetInvoke desc " + desc);
                return true;
            } else {
                System.out.println(TAG + "checkIsTargetInvoke ignore: no Runnable param");
                return false;
            }
        }
        return false;
    }

    // 找出当前指令前后的无效指令并删除
    private void removeUnusedInsn(MethodNode methodNode, InsnList list, MethodInsnNode anchor) {
        List<AbstractInsnNode> removeList = new ArrayList<>();

        removeList.addAll(findConstructParamInsn(list,anchor));
        int size = removeList.size();
        System.out.println(TAG + "remove pre insn size " + size);

        removeList.addAll(findLocalVarUsageInsn(list, anchor));
        System.out.println(TAG + "remove next insn size " + (removeList.size() - size));

        for (AbstractInsnNode node : removeList) {
            list.remove(node);
        }
    }

    // 找出当前指令使用的aload参数指令
    private List<AbstractInsnNode> findConstructParamInsn(InsnList list, MethodInsnNode anchor) {
        List<AbstractInsnNode> removeList = new ArrayList<>();
        List<String> params = parseParamFromDesc(anchor.desc);
        int size = params.size();
        if (size > 0) {
            // 删除构造相关无效指令,如NEW,DUP
            System.out.println(TAG + "pre:remove insn");
            AbstractInsnNode pre = anchor.getPrevious();
            while (pre != null) {

                int opcode = pre.getOpcode();
                int type = pre.getType();

                // 起始指令
                if (opcode == Opcodes.NEW) {
                    if (pre instanceof TypeInsnNode && THRED_CLASS_NAME.equals(((TypeInsnNode) pre).desc)) {
                        removeList.add(pre);
                        AbstractInsnNode next = pre.getNext();
                        if (next != null && next.getOpcode() == Opcodes.DUP) {
                            removeList.add(next);
                        }
                        break;
                    }
                } else if (type == AbstractInsnNode.LDC_INSN) {
                    LdcInsnNode ldc = (LdcInsnNode) pre;
                    if (checkLDCParam(params, ldc.cst)) {
                        removeList.add(pre);
                    }
                }

                pre = pre.getPrevious();
            }

        } else {
            System.out.println(TAG + "pre:constructor use no params desc= " + anchor.desc);
        }
        return removeList;
    }

    private List<AbstractInsnNode> findLocalVarUsageInsn(InsnList list, MethodInsnNode anchor){
        List<AbstractInsnNode> removeList = new ArrayList<>();
        // 删除后面无效指令,如后续的调用
        AbstractInsnNode next = anchor.getNext();
        if (next == null) {
            return removeList;
        }

        if (next instanceof MethodInsnNode) {
            // new xxx().start()
            MethodInsnNode method = (MethodInsnNode) next;
            if (method.getOpcode() == Opcodes.INVOKEVIRTUAL && THRED_CLASS_NAME.equals(method.owner)) {
                removeList.add(method);
            }
        } else if (next instanceof VarInsnNode && next.getOpcode() == Opcodes.ASTORE) {
            // xxx = new XXX
            // xxx.start()
            removeList.add(next);
            VarInsnNode var = (VarInsnNode) next;
            int operand = var.var;

            next = next.getNext();
            boolean loadVar = false;
            while (next != null) {


                // 删除后续调用
                if (next instanceof VarInsnNode && next.getOpcode() == Opcodes.ALOAD && ((VarInsnNode) next).var == operand) {
                    loadVar = true;
                } else if (loadVar && next instanceof MethodInsnNode && THRED_CLASS_NAME.equals(((MethodInsnNode) next).owner)) {
                    AbstractInsnNode pre = next;
                    while (pre != null) {
                        removeList.add(pre);
                        if (pre.getType() == AbstractInsnNode.LABEL) {
                            break;
                        }
                        pre = pre.getPrevious();
                    }
                    loadVar = false;
                }

                next = next.getNext();
            }
        } else {
            // new xxx(new yyy()).start()
            if (next.getType() == AbstractInsnNode.LABEL) {
                ArrayList<AbstractInsnNode> tempList = new ArrayList<>();
                tempList.add(next);
                next = next.getNext();
                while (next != null) {
                    tempList.add(next);
                    if (next.getType() == AbstractInsnNode.METHOD_INSN) {
                        MethodInsnNode method = (MethodInsnNode) next;
                        if (method.getOpcode() == Opcodes.INVOKEVIRTUAL && THRED_CLASS_NAME.equals(method.owner)) {
                            break;
                        }
                    }
                    next = next.getNext();
                }
                removeList.addAll(tempList);
            }
        }
        return removeList;
    }

    private List<String> parseParamFromDesc(String desc) {
        int start = desc.indexOf('(');
        int end = desc.indexOf(')');
        String content = desc.substring(start + 1, end);
        System.out.println(TAG + "parse params from desc " + desc + ",param " + content);

        return parseParam(content);
    }
    
}

目前只是处理了局部变量的情形,如果将Thread对象定义为成员变量,会更加的复杂,并且容易引出更多问题,这里略过;可以在插件里通过日志记录这些case然后挨个分析其他可行的处理方式。

更完整的代码及测试 参考

全部评论

相关推荐

抱抱碍事梨a:三点建议,第一点是建议再做一个项目,把自我介绍部分顶了,第二点是中南大学加黑加粗,第三点是建议加v详细交流
点赞 评论 收藏
分享
刘湘_passion:出国旅游?那就小心你的腰子咯
点赞 评论 收藏
分享
评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客企业服务