android 基于agp AsmClassVisitorFactory插桩实践

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

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

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

先定义插件

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

typescript 复制代码
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

ini 复制代码
implementation-class=com.test.plugins.newthread.NewThreadPlugin

插件开发

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

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

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

如下面这段代码:

arduino 复制代码
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(); }

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

arduino 复制代码
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保留是为了方便定位)
scss 复制代码
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然后挨个分析其他可行的处理方式。

更完整的代码及测试 参考

相关推荐
我科绝伦(Huanhuan Zhou)15 分钟前
MySQL一键升级脚本(5.7-8.0)
android·mysql·adb
怪兽20141 小时前
Android View, SurfaceView, GLSurfaceView 的区别
android·面试
龚礼鹏2 小时前
android 图像显示框架二——流程分析
android
消失的旧时光-19432 小时前
kmp需要技能
android·设计模式·kotlin
帅得不敢出门3 小时前
Linux服务器编译android报no space left on device导致失败的定位解决
android·linux·服务器
雨白4 小时前
协程间的通信管道 —— Kotlin Channel 详解
android·kotlin
TimeFine5 小时前
kotlin协程 容易被忽视的CompletableDeferred
android
czhc11400756637 小时前
Linux1023 mysql 修改密码等
android·mysql·adb
GOATLong7 小时前
MySQL内置函数
android·数据库·c++·vscode·mysql
onthewaying9 小时前
Android SurfaceTexture 深度解析
android·opengl