Android ASM 字节码插桩 实战

字节码插桩:就是由class到dex之前修改class文件,达到增强现有类的功能。

1.Android工程的构建过程

  • 1.Android Resources–>通过aapt–>R.java

  • 2.aidl Files–>通过aidl–>java interface

  • 3.(R.java、Android Resouce code、java interface)–>java compile–>.class Files

  • 4.(.class Files、3rd Party Libraries and class Files)–>dex 编译器–>.dex Files

  • 5.(dex Files、Other Resources)–>Apk Builder–>Android Package(.apk)–>jar signer–>Signed Apk

以上就是构建的整个过程,其中字节码插桩就是.class Files 经过dex编译器之前的操作,对class的增强。

2.Transform

Transform是Android 官方插件提供给开发者在项目构建阶段由Class到Dex转换之前修改Class文件的一套API。目前典型的应用场景就是字节码插桩。

通过Transform可以得到所有的class字节码,我们自定义的Transfrom会先执行,执行的结果做为参数进行传递。

3.新建一个插件类

public class APMPlugin implements Plugin<Project> {
    
    

    @Override
    public void apply(Project project) {
    
    
        BaseExtension android = project.getExtensions().getByType(BaseExtension.class);
        /**
         * 注册一个Transform
         */
        android.registerTransform(new ASMTransfrom());
    }
}

android 插件能够获得所有的class,并通过接口的形式暴露出来。

4.创建一个ASM

public class ASMTransform extends Transform {
    
    

    @Override
    public String getName() {
    
    
        return "ms_asm";
    }

    /**
     * 处理所有class
     *
     * @return
     */
    @Override
    public Set<QualifiedContent.ContentType> getInputTypes() {
    
    
        return TransformManager.CONTENT_CLASS;
    }

    /**
     * 范围仅仅是主项目所有的类
     *
     * @return
     */
    @Override
    public Set<? super QualifiedContent.Scope> getScopes() {
    
    
        return TransformManager.PROJECT_ONLY;
    }

    /**
     * 不使用增量
     * @return
     */
    @Override
    public boolean isIncremental() {
    
    
        return false;
    }

    /**
     * android插件将所有的class通过这个方法告诉给我们
     *
     * @param transformInvocation
     * @throws TransformException
     * @throws InterruptedException
     * @throws IOException
     */
    @Override
    public void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
    
    
        super.transform(transformInvocation);

        TransformOutputProvider outputProvider = transformInvocation.getOutputProvider();
        //清理上次缓存的文件信息
        outputProvider.deleteAll();


        //得到所有的输入
        Collection<TransformInput> inputs = transformInvocation.getInputs();
        for (TransformInput input : inputs) {
    
    
            // 处理class目录
            for (DirectoryInput directoryInput : input.getDirectoryInputs()) {
    
    
                // 直接复制输出到对应的目录
                String dirName = directoryInput.getName();
                File src = directoryInput.getFile();
                System.out.println("输出class文件:" + src);
                String md5Name = DigestUtils.md5Hex(src.getAbsolutePath());
                //得到输出class文件的目录
                File dest = outputProvider.getContentLocation(dirName + md5Name,
                        directoryInput.getContentTypes(), directoryInput.getScopes(),
                        Format.DIRECTORY);
                //执行插桩操作
                processInject(src, dest);
            }
            // 处理jar(依赖)的class
            for (JarInput jarInput : input.getJarInputs()) {
    
    
                String jarName = jarInput.getName();
                File src = jarInput.getFile();
                System.out.println("输出jar包:" + src);
                String md5Name = DigestUtils.md5Hex(src.getAbsolutePath());
                if (jarName.endsWith(".jar")) {
    
    
                    jarName = jarName.substring(0, jarName.length() - 4);
                }
                File dest = outputProvider.getContentLocation(jarName + md5Name,
                        jarInput.getContentTypes(), jarInput.getScopes(), Format.JAR);
                FileUtils.copyFile(src, dest);
            }
        }
    }

    private void processInject(File src, File dest) throws IOException {
    
    
        String dir = src.getAbsolutePath();
        FluentIterable<File> allFiles = FileUtils.getAllFiles(src);
        for (File file : allFiles) {
    
    
            //得到文件输入流
            FileInputStream fis = new FileInputStream(file);
            //得到字节码Reader
            ClassReader cr = new ClassReader(fis);
            //得到写出器
            ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
            //将注入的时间信息,写入
            cr.accept(new ClassInjectTimeVisitor(cw,file.getName()),
                    ClassReader.EXPAND_FRAMES);

            byte[] newClassBytes = cw.toByteArray();
            String absolutePath = file.getAbsolutePath();
            String fullClassPath = absolutePath.replace(dir, "");
            //将得到的字节码信息 写如输出目录
            File outFile = new File(dest, fullClassPath);
            FileUtils.mkdirs(outFile.getParentFile());
            FileOutputStream fos = new FileOutputStream(outFile);
            fos.write(newClassBytes);
            fos.close();
        }

    }
}

继承Transform 重写父类的方法(com.android.build.api.transform.Transform)是这个别弄错了。

  • getName() 返回transfrom的方法名,这个随便定义
  • getInputTypes() 得到需要处理的内容类型,TransformManager.CONTENT_CLASS 这个表示字节码。
  • getScopes() 返回的是处理范围,比如是整个项目还是仅仅主app等
  • isIncremental() 是否增量
  • transform() 这个方法会回调我们需要的所有类信息

5.创建字节码注入时间方法器

public class ClassInjectTimeVisitor extends ClassVisitor {
    
    

    /**
     * 得到类名
     */
    private String mClassName;

    public ClassInjectTimeVisitor(ClassVisitor cv, String fileName) {
    
    
        super(Opcodes.ASM5, cv);
        mClassName = fileName.substring(0, fileName.lastIndexOf("."));
    }


    /**
     * 访问方法
     * @param access 方法的访问flag
     * @param name 方法名
     * @param desc 描述信息
     * @param signature 签名信息
     * @param exceptions
     * @return
     */
    @Override
    public MethodVisitor visitMethod(int access, String name, String desc, String signature,
                                     String[] exceptions) {
    
    

        MethodVisitor mv = super.visitMethod(access, name, desc, signature,
                exceptions);
        return new MethodAdapterVisitor(mv, access, name, desc, mClassName);
    }

}

visitMethod这个方法负责拦截所有的方法,并初始化一个方法适配器MethodAdapterVisitor

MethodAdapterVisitor,负责具体的插桩代码逻辑。

5.准备需要插入的class信息

public static void main(String[] args) throws InterruptedException {
    
    
    long start = System.currentTimeMillis();

    Thread.sleep(1000);

    long end = System.currentTimeMillis();
    System.out.println("execute:"+(end-start)+" ms.");
}

插入方法耗时统计:

  • 需要在方法开始插入这行代码:long start = System.currentTimeMillis();

  • 结尾处插入:

    ​ long end = System.currentTimeMillis();
    ​ System.out.println(“execute:”+(end-start)+" ms.");

将上面的java信息转成ASM Bytecode,以便方法的进行插桩注入

 {
    
    
            methodVisitor = classWriter.visitMethod(ACC_PUBLIC | ACC_STATIC, "main", "([Ljava/lang/String;)V", null, new String[]{
    
    "java/lang/InterruptedException"});
            methodVisitor.visitCode();
     
     		//long start = System.currentTimeMillis();
            Label label0 = new Label();
            methodVisitor.visitLabel(label0);
            methodVisitor.visitLineNumber(7, label0);
            methodVisitor.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
            methodVisitor.visitVarInsn(LSTORE, 1);
     
     
            // 下面是执行的 Thread.sleep(1_000); 
            Label label1 = new Label();
            methodVisitor.visitLabel(label1);
            methodVisitor.visitLineNumber(9, label1);
            methodVisitor.visitLdcInsn(new Long(1000L));
            methodVisitor.visitMethodInsn(INVOKESTATIC, "java/lang/Thread", "sleep", "(J)V", false);
           
            //下面是执行的 long end = System.currentTimeMillis();
            //        System.out.println("execute:"+(end-start)+" ms.");
            Label label2 = new Label();
            methodVisitor.visitLabel(label2);
            methodVisitor.visitLineNumber(11, label2);
            methodVisitor.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
            methodVisitor.visitVarInsn(LSTORE, 3);
            Label label3 = new Label();
            methodVisitor.visitLabel(label3);
            methodVisitor.visitLineNumber(12, label3);
            methodVisitor.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
            methodVisitor.visitTypeInsn(NEW, "java/lang/StringBuilder");
            methodVisitor.visitInsn(DUP);
            methodVisitor.visitMethodInsn(INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V", false);
            methodVisitor.visitLdcInsn("execute:");
            methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
            methodVisitor.visitVarInsn(LLOAD, 3);
            methodVisitor.visitVarInsn(LLOAD, 1);
            methodVisitor.visitInsn(LSUB);
            methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(J)Ljava/lang/StringBuilder;", false);
            methodVisitor.visitLdcInsn(" ms.");
            methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
            methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false);
            methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
     
     
            //其他内容不需要处理
            Label label4 = new Label();
            methodVisitor.visitLabel(label4);
            methodVisitor.visitLineNumber(13, label4);
            methodVisitor.visitInsn(RETURN);
            Label label5 = new Label();
            methodVisitor.visitLabel(label5);
            methodVisitor.visitLocalVariable("args", "[Ljava/lang/String;", null, label0, label5, 0);
            methodVisitor.visitLocalVariable("start", "J", null, label1, label5, 1);
            methodVisitor.visitLocalVariable("end", "J", null, label3, label5, 3);
            methodVisitor.visitMaxs(6, 5);
            methodVisitor.visitEnd();
        }

上边的内容我对 相关的内容 做了注释,分别在方法的执行最前面和最后面插入相关的代码逻辑就好,这部分是辅助内容。

6.创建方法访问者适配器

public class MethodAdapterVisitor extends AdviceAdapter {
    
    

    private String mClassName;
    private String mMethodName;
    private boolean mInject;
    private int mStart, mEnd;

    protected MethodAdapterVisitor(MethodVisitor mv, int access, String name, String desc,
                                   String className) {
    
    
        super(Opcodes.ASM5, mv, access, name, desc);
        mMethodName = name;
        this.mClassName = className;
    }


    /**
     * 拦截注解方法
     *
     * @param desc
     * @param visible
     * @return
     */
    @Override
    public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
    
    
        if ("Lcom/meishe/ms_asminject/MSTimeAnalysis;".equals(desc)) {
    
    
            mInject = true;
        }
        return super.visitAnnotation(desc, visible);
    }


    /**
     * 方法进入
     */
    @Override
    protected void onMethodEnter() {
    
    
        if (mInject) {
    
    
            //执行方法currentTimeMillis 得到startTime
            mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
            mStart = newLocal(Type.LONG_TYPE);
            mv.visitVarInsn(LSTORE, mStart);

        }
    }


    /**
     * 方法结束
     *
     * @param opcode
     */
    @Override
    protected void onMethodExit(int opcode) {
    
    
        if (mInject) {
    
    

            //执行 currentTimeMillis 得到end time
            mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
            mEnd =newLocal(Type.LONG_TYPE);
            mv.visitVarInsn(LSTORE, mEnd);

            //得到静态成员 out
            mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
            //new  //class java/lang/StringBuilder
            mv.visitTypeInsn(NEW, "java/lang/StringBuilder");
            //引入类型 分配内存 并dup压入栈顶让下面的INVOKESPECIAL 知道执行谁的构造方法
            mv.visitInsn(DUP);

            //执行init方法 (构造方法)
            mv.visitMethodInsn(INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V", false);
            //把常量压入栈顶
            mv.visitLdcInsn("execute "+ mMethodName +" :");
            //执行append方法,使用栈顶的值作为参数
            mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);

            //  获得存储的本地变量
            mv.visitVarInsn(LLOAD, mEnd);
            mv.visitVarInsn(LLOAD, mStart);
            // lsub   减法指令
            mv.visitInsn(LSUB);

            //把减法结果append
            mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(J)Ljava/lang/StringBuilder;", false);
            //拼接常量
            mv.visitLdcInsn(" ms.");
            mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);

            //执行StringBuilder 的toString方法
            mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false);
            //执行println方法
            mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);

        }
    }

}

继承AdviceAdapter,并重写相关的方法(org.objectweb.asm.commons.AdviceAdapter)是这个别继承错了。

  • visitAnnotation() 这个会输出方法拥有的注解信息,这里我们只对我们添加注解的方法进行注入操作。

  • onMethodEnter() 方法进入的时候会回调,在这里插入long start = System.currentTimeMillis();

  • onMethodExit(int opcode) 方法结束的时候回调,在这里插入:

    long end = System.currentTimeMillis();
    System.out.println(“execute:”+(end-start)+" ms.");

7.上面用的注解

@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.CLASS)
public @interface MSTimeAnalysis {
    
    
}

声明一个编译器注解即可,由于上边用到了,贴出来。

这样就完成了字节码插桩的全部工作。

8.进行测试工作

public class MainActivity extends AppCompatActivity {
    
    

    @Override
    protected void onCreate(Bundle savedInstanceState) {
    
    
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        testApp();
    }

    @MSTimeAnalysis
    public void testApp(){
    
    
        try {
    
    
            Thread.sleep(1000);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
    }
}

声明一个testApp()方法,增加@MSTimeAnalysis方法,并调用。运行就可以看到输出:

2022-05-03 11:08:10.824 27788-27788/com.meishe.ms_asminject I/System.out: execute testApp :1000 ms.

在build/intermediates/transforms/ms_asm/debug/1/com/meishe/ms_asminject/MainActivity.class可以查看编译生成字节码:

protected void onCreate(Bundle savedInstanceState) {
    
    
    super.onCreate(savedInstanceState);
    this.setContentView(2131427356);
    this.testApp();
}

@MSTimeAnalysis
public void testApp() {
    
    
    long var1 = System.currentTimeMillis();

    try {
    
    
        Thread.sleep(1000L);
    } catch (InterruptedException var6) {
    
    
        var6.printStackTrace();
    }

    long var4 = System.currentTimeMillis();
    System.out.println("execute testApp :" + (var4 - var1) + " ms.");
}

证明确实插桩成功了。

源码链接

猜你喜欢

转载自blog.csdn.net/u014078003/article/details/124552933