ASM字节码插桩

个人博客

http://www.milovetingting.cn

ASM字节码插桩

前言

热修复的多Dex加载方案中,对于5.0以下的系统存在CLASS_ISPREVERIFIED的问题,而解决这个问题的一个方案是:通过ASM插桩,在类的构造方法里引入一个其它dex里的类,从而避免被打上CLASS_ISPREVERIFIED标签。热修复可以参考其它资料或者前面写的一篇文章。本文主要介绍ASM插桩,主要参考 https://juejin.im/post/5c6eaa066fb9a049fc042048

ASM框架

ASM是一个可以分析和操作字节码的框架,通过它可以动态地修改字节码内容。使用ASM可以实现无埋点统计、性能监控等。

什么是字节码插桩

Android编译过程中,往字节码插入自定义的字节码。

插桩时机

Android打包要经过:java文件--class文件--dex文件,通过Gradle提供的Transform API,可以在编译成dex文件前,得到class文件,然后通过ASM修改字节码,即字节码插桩。

实现

下面通过自定义Gradle插件来处理class文件来实现插桩。

自定义Gradle插件

具体自定义Gradle插件的步骤,这里不再详细介绍,可以参考之前的一篇文章或者自行查阅其它资料。

处理Class

插件分为插件部分(src/main/groovy)、ASM部分(src/main/java)

ASMPlugin类继承自Transform并实现Plugin接口,在apply的方法里注册,transform里回调并处理class。

class ASMPlugin extends Transform implements Plugin<Project> {

    @Override
    void apply(Project project) {
        def android = project.extensions.getByType(AppExtension)
        android.registerTransform(this)
    }

    @Override
    String getName() {
        return "ASMPlugin"
    }

    @Override
    Set<QualifiedContent.ContentType> getInputTypes() {
        return TransformManager.CONTENT_CLASS
    }

    @Override
    Set<? super QualifiedContent.Scope> getScopes() {
        return TransformManager.SCOPE_FULL_PROJECT
    }

    @Override
    boolean isIncremental() {
        return false
    }

    @Override
    void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
        //处理class
    }
}

主要的逻辑处理都在transform方法里

@Override
    void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
        println('--------------------ASMPlugin transform start--------------------')
        def startTime = System.currentTimeMillis()
        Collection<TransformInput> inputs = transformInvocation.inputs
        TransformOutputProvider outputProvider = transformInvocation.outputProvider
        //删除旧的输出
        if (outputProvider != null) {
            outputProvider.deleteAll()
        }
        //遍历inputs
        inputs.each { input ->
            //遍历directoryInputs
            input.directoryInputs.each {
                directoryInput -> handleDirectoryInput(directoryInput, outputProvider)
            }
            //遍历jarInputs
            input.jarInputs.each {
                jarInput -> handleJarInput(jarInput, outputProvider)
            }
        }
        def time = (System.currentTimeMillis() - startTime) / 1000
        println('-------------------- ASMPlugin transform end --------------------')
        println("ASMPlugin cost $time s")
    }

在transform里处理class文件和jar文件

    /**
     * 处理目录下的class文件
     * @param directoryInput
     * @param outputProvider
     */
    static void handleDirectoryInput(DirectoryInput directoryInput, TransformOutputProvider outputProvider) {
        //是否为目录
        if (directoryInput.file.isDirectory()) {
            //列出目录所有文件(包含子文件夹,子文件夹内文件)
            directoryInput.file.eachFileRecurse {
                file ->
                    def name = file.name
                    if (isClassFile(name)) {
                        println("-------------------- handle class file:<$name> --------------------")
                        ClassReader classReader = new ClassReader(file.bytes)
                        ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS)
                        ClassVisitor classVisitor = new ActivityClassVisitor(classWriter)
                        classReader.accept(classVisitor, org.objectweb.asm.ClassReader.EXPAND_FRAMES)
                        byte[] bytes = classWriter.toByteArray()
                        FileOutputStream fileOutputStream = new FileOutputStream(file.parentFile.absolutePath + File.separator + name)
                        fileOutputStream.write(bytes)
                        fileOutputStream.close()
                    }
            }
        }
        def dest = outputProvider.getContentLocation(directoryInput.name, directoryInput.contentTypes, directoryInput.scopes, Format.DIRECTORY)
        FileUtils.copyDirectory(directoryInput.file, dest)
    }

    /**
     * 处理Jar中的class文件
     * @param jarInput
     * @param outputProvider
     */
    static void handleJarInput(JarInput jarInput, TransformOutputProvider outputProvider) {
        if (jarInput.file.getAbsolutePath().endsWith(".jar")) {
            //重名名输出文件,因为可能同名,会覆盖
            def jarName = jarInput.name
            def md5Name = DigestUtils.md5Hex(jarInput.file.absolutePath)
            if (jarName.endsWith(".jar")) {
                jarName = jarName.substring(0, jarName.length() - 4)
            }
            JarFile jarFile = new JarFile(jarInput.file)
            Enumeration enumeration = jarFile.entries()
            File tempFile = new File(jarInput.file.parent + File.separator + "temp.jar")
            //避免上次的缓存被重复插入
            if (tempFile.exists()) {
                tempFile.delete()
            }
            JarOutputStream jarOutputStream = new JarOutputStream(new FileOutputStream(tempFile))
            //保存
            while (enumeration.hasMoreElements()) {
                JarEntry jarEntry = enumeration.nextElement()
                String entryName = jarEntry.name
                ZipEntry zipEntry = new ZipEntry(entryName)
                InputStream inputStream = jarFile.getInputStream(zipEntry)
                if (isClassFile(entryName)) {
                    println("-------------------- handle jar file:<$entryName> --------------------")
                    jarOutputStream.putNextEntry(zipEntry)
                    ClassReader classReader = new ClassReader(IOUtils.toByteArray(inputStream))
                    ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS)
                    ClassVisitor classVisitor = new ActivityClassVisitor(classWriter)
                    classReader.accept(classVisitor, org.objectweb.asm.ClassReader.EXPAND_FRAMES)
                    byte[] bytes = classWriter.toByteArray()
                    jarOutputStream.write(bytes)
                } else {
                    jarOutputStream.putNextEntry(zipEntry)
                    jarOutputStream.write(IOUtils.toByteArray(inputStream))
                }
                jarOutputStream.closeEntry()
            }
            jarOutputStream.close()
            jarFile.close()
            def dest = outputProvider.getContentLocation(jarName + "_" + md5Name, jarInput.contentTypes, jarInput.scopes, Format.JAR)
            FileUtils.copyFile(tempFile, dest)
            tempFile.delete()
        }
    }

    /**
     * 判断是否为需要处理class文件
     * @param name
     * @return
     */
    static boolean isClassFile(String name) {
        return (name.endsWith(".class") && !name.startsWith("R\$")
                && "R.class" != name && "BuildConfig.class" != name && name.contains("Activity"))
    }

在handleDirectoryInput和handleJarInput调用了我们自己定义在src/main/java里的ClassVisitor,

class ActivityClassVisitor extends ClassVisitor implements Opcodes {

    private String mClassName;

    private static final String CLASS_NAME_ACTIVITY = "androidx/appcompat/app/AppCompatActivity";

    private static final String METHOD_NAME_ONCREATE = "onCreate";

    private static final String METHOD_NAME_ONDESTROY = "onDestroy";

    public ActivityClassVisitor(ClassVisitor cv) {
        super(Opcodes.ASM5, cv);
    }

    @Override
    public void visit(int version, int access, String name, String signature, String superName,
                      String[] interfaces) {
        mClassName = name;
        super.visit(version, access, name, signature, superName, interfaces);
    }

    @Override
    public MethodVisitor visitMethod(int access, String name, String desc, String signature,
                                     String[] exceptions) {
        MethodVisitor methodVisitor = cv.visitMethod(access, name, desc, signature, exceptions);
        if (CLASS_NAME_ACTIVITY.equals(mClassName)) {
            if (METHOD_NAME_ONCREATE.equals(name)) {
                System.out.println("-------------------- ActivityClassVisitor,visit method:" + name +
                        " --------------------");
                return new ActivityOnCreateMethodVisitor(Opcodes.ASM5, methodVisitor);
            } else if (METHOD_NAME_ONDESTROY.equals(name)) {
                System.out.println("-------------------- ActivityClassVisitor,visit method:" + name +
                        " --------------------");
                return new ActivityOnDestroyMethodVisitor(Opcodes.ASM5, methodVisitor);
            }
        }
        return methodVisitor;
    }
}

这里为简化操作,只处理了Activity的onCreate和onDestroy方法。在visitMethod方法里又调用了具体的MethodVisitor。如果对字节码不是特别了解的,可以通过在Android Studio中安装ASM Bytecode Outline插件来辅助。

具体使用:

安装完成ASM Bytecode Outline后,重启Android Studio,然后在相应的Java文件中右键,选择Show Bytecode outline

稍待一会后,会生成相应的字节码,在打开的面板中选择ASMified标签

public class ActivityOnCreateMethodVisitor extends MethodVisitor {

    public ActivityOnCreateMethodVisitor(int api, MethodVisitor mv) {
        super(api, mv);
    }

    @Override
    public void visitCode() {
         mv.visitLdcInsn("ASMPlugin");
        mv.visitLdcInsn("-------------------- MainActivity onCreate --------------------");
        mv.visitMethodInsn(INVOKESTATIC, "android/util/Log", "i", "(Ljava/lang/String;" +
                "Ljava/lang/String;)I", false);
        mv.visitInsn(POP);

        super.visitCode();
    }

    @Override
    public void visitInsn(int opcode) {
        super.visitInsn(opcode);
    }
}

public class ActivityOnDestroyMethodVisitor extends MethodVisitor {

    public ActivityOnDestroyMethodVisitor(int api, MethodVisitor mv) {
        super(api, mv);
    }

    @Override
    public void visitCode() {
        super.visitCode();

        mv.visitLdcInsn("ASMPlugin");
        mv.visitLdcInsn("-------------------- MainActivity onDestroy --------------------");
        mv.visitMethodInsn(INVOKESTATIC, "android/util/Log", "i", "(Ljava/lang/String;" +
                "Ljava/lang/String;)I", false);
        mv.visitInsn(POP);
    }

    @Override
    public void visitInsn(int opcode) {
        super.visitInsn(opcode);
    }
}

在visitCode和visitInsn方法里执行具体的操作。

在处理Class过程中,可能会出现各种问题,可以通过调试插件来定位问题。可以参考上一篇文章来调试插件。

引用插件

在app模块引用插件,这里不再详细介绍,可以参考前面的文章

将应用运行在手机上,打开后,可以看到日志输出:

02-25 17:29:45.885 31237 31237 I ASMPlugin: -------------------- MainActivity onCreate --------------------
02-25 17:29:50.646 31237 31237 I ASMPlugin: -------------------- MainActivity onDestroy --------------------

结语

这篇文章只是实现了简单的ASM插桩。可以查阅其它资料,了解更多关于字节码、ASM相关的内容。

源码地址:https://github.com/milovetingting/Samples/tree/master/ASM

原文地址:https://www.cnblogs.com/milovetingting/p/12364333.html

时间: 2024-11-08 17:03:50

ASM字节码插桩的相关文章

字节码技术在模块依赖分析中的应用

背景 近年来,随着手机业务的快速发展,为满足手机端用户诉求和业务功能的迅速增长,移动端的技术架构也从单一的大工程应用,逐步向模块化.组件化方向发展.以高德地图为例,Android 端的代码已突破百万行级别,超过100个模块参与最终构建. 试想一下,如果没有一套标准的依赖检测和监控工具,用不了多久,模块的依赖关系就可能会乱成一锅粥. 从模块 Owner 的角度看,为什么依赖分析这么重要? 作为模块 Owner,我首先想知道“谁依赖了我?依赖了哪些接口”.唯有如此才能评估本模块改动的影响范围,以及暴

关于java字节码框架ASM的学习

一.什么是ASM ASM是一个java字节码操纵框架,它能被用来动态生成类或者增强既有类的功能.ASM 可以直接产生二进制 class 文件,也可以在类被加载入 Java 虚拟机之前动态改变类行为.Java class 被存储在严格格式定义的 .class文件里,这些类文件拥有足够的元数据来解析类中的所有元素:类名称.方法.属性以及 Java 字节码(指令).ASM从类文件中读入信息后,能够改变类行为,分析类信息,甚至能够根据用户要求生成新类.asm字节码增强技术主要是用来反射的时候提升性能的,

[译]深入字节码操作:使用ASM和Javassist创建审核日志

深入字节码操作:使用ASM和Javassist创建审核日志 原文链接:https://blog.newrelic.com/2014/09/29/diving-bytecode-manipulation-creating-audit-log-asm-javassist/ 在堆栈中使用Spring和Hibernate,您的应用程序的字节码可能会在运行时被增强或处理. 字节码是Java虚拟机(JVM)的指令集,所有在JVM上运行的语言都必须最终编译为字节码. 操作字节码原因如下: 程序分析: 查找应用

zorka源码解读之通过beanshell进行插桩的流程

zorka中插桩流程概述 1.在SpyDefinition中配置插桩属性,将SpyDefinition实例提交给插桩引擎.2.SpyDefinition实例中包含了插桩探针probes,probe插入到方法中,对方法的执行进行监控.方法的插入阶段主要包括三个:开始阶段(entry),返回阶段(return),异常阶段(error).每个probe会根据其指定的阶段对方法插桩,捕获其中的数据,比如当前时间.方法参数.方法内部的变量等.probe捕获的数据会封装为SpyRecord实例(Symbol

字节码增强技术探索

1.字节码 1.1 什么是字节码? Java之所以可以“一次编译,到处运行”,一是因为JVM针对各种操作系统.平台都进行了定制,二是因为无论在什么平台,都可以编译生成固定格式的字节码(.class文件)供JVM使用.因此,也可以看出字节码对于Java生态的重要性.之所以被称之为字节码,是因为字节码文件由十六进制值组成,而JVM以两个十六进制值为一组,即以字节为单位进行读取.在Java中一般是用javac命令编译源代码为字节码文件,一个.java文件从编译到运行的示例如图1所示. 图1 Java运

Javassist进行方法插桩

javassist官网 http://jboss-javassist.github.io/javassist/ javassist API网 http://jboss-javassist.github.io/javassist/html/index.html javassist参考博客 https://www.ibm.com/developerworks/cn/java/j-dyn0916/  Ⅰ插桩 自动用例生成(使用Randoop) 评价(对用例筛选冗余)>功能覆盖.语句覆盖(一般用后者)

java字节码忍者禁术

Java语言本身是由Java语言规格说明(JLS)所定义的,而Java虚拟机的可执行字节码则是由一个完全独立的标准,即Java虚拟机规格说明(通常也被称为VMSpec)所定义的. JVM字节码是通过javac对Java源代码文件进行编译后生成的,生成的字节码与原本的Java语言存在着很大的不同.比方说,在Java语言中为人熟知的一些高级特性,在编译过程中会被移除,在字节码中完全不见踪影. 这方面最明显的一个例子莫过于Java中的各种循环关键字了(for.while等等),这些关键字在编译过程中会

JVM——字节码增强技术简介

Java字节码增强指的是在Java字节码生成之后,对其进行修改,增强其功能,这种方式相当于对应用程序的二进制文件进行修改.Java字节码增强主要是为了减少冗余代码,提高性能等. 实现字节码增强的主要步骤为: 1.修改字节码 在内存中获取到原来的字节码,然后通过一些工具(如 ASM,Javaasist)来修改它的byte[]数组,得到一个新的byte数组. 2.使修改后的字节码生效 有两种方法: 1) 自定义ClassLoader来加载修改后的字节码: 2)替换掉原来的字节码:在JVM加载用户的C

JAVAssist字节码操作

Java动态性的两种常见实现方式 字节码操作 反射 运行时操作字节码可以让我们实现如下功能: 动态生成新的类 动态改变某个类的结构(添加/删除/修改  新的属性/方法) 优势: 比反射开销小,性能高 JAVAasist性能高于反射,低于ASM 常见的字节码操作类库 BCEL 这是Apache Software Fundation的jakarta项目的一部分.BCEL是javaclassworking广泛使用的一种跨级啊,它可以让你深入JVM汇编语言进行类的操作的细节.BCEL与javassist