Android字节码插桩 - Go语言中文社区

Android字节码插桩


什么是字节码插桩

字节码插桩就是在构建的过程中,通过修改已经编译完成的字节码文件,也就是class文件,来实现功能的添加。

简单来讲,我们要实现无埋点对客户端的全量统计。这里的统计概括的范围比较广泛,常见的场景有:

  • 页面(Activity、Fragment)的打开事件
  • 各种点击事件的统计,包括但不限于Click LongClick TouchEvent
  • Debug期需要统计各个方法的耗时。注意这里的方法包括接入的第三方SDK的方法。
  • 待补充

要实现这些功能需要拥有哪些技术点呢?

  • 面向切面编程思想(AOP)
  • Android打包流程
  • 自定义Gradle插件
  • Java字节码
  • 字节码编织(ASM)
  • 结合自己的业务实现统计代码

面向切面编程思想(AOP)

AOP(Aspect Oriented Program)是一种面向切面编程的思想。这种编程思想是相对于OOP(ObjectOriented Programming)来说的。说破天,咱们要实现的功能还是统计嘛,大规模的重复统计行为是典型的AOP使用场景。所以搞懂什么是AOP以及为什么要用AOP变得很重要。

先来说一下大家熟悉的面向对象编程:面向对象的特点是继承、多态和封装。而封装就要求将功能分散到不同的对象中去,这在软件设计中往往称为职责分配。实际上也就是说,让不同的类设计不同的方法。这样代码就分散到一个个的类中去了。这样做的好处是降低了代码的复杂程度,使类可重用。

但是面向对象的编程天生有个缺点就是分散代码的同时,也增加了代码的重复性。比如我希望在项目里面所有的模块都增加日志统计模块,按照OOP的思想,我们需要在各个模块里面都添加统计代码,但是如果按照AOP的思想,可以将统计的地方抽象成切面,只需要在切面里面添加统计代码就OK了。

在这里插入图片描述

其实在服务端的领域AOP已经被各路大佬玩的风生水起,例如Spring这类跨时代的框架。我第一次接触AOP就是在学习Spring框架的的时候。最常见实现AOP的方式就是代理。

AOP 是一种编程思想,但是它的实现方式有很多,比如:Spring、AspectJ、JavaAssist、ASM 等。由于我是做 Android 开发的,所以会用 Android 中的一些例子。

  • JakeWharton 的 hugo 就是一个典型的应用,其利用了自定义 Gradle 插件 + AspectJ 的方式,将有特定注解的方法的参数、返回结果和执行时间打印到 Logcat 中,方便开发调试。
  • 最近在学习 Java 字节码和 ASM 方面的知识,所以也照猫画虎,写了一个TraceLog,实现了和 hugo同样的功能,将特定注解的方法的参数、返回结果和执行时间打印到 Logcat 中,方便开发调试,不过我使用的是 自定义 Gradle 插件 + ASM 的方式。后面会讲。

Android打包流程

详见 android Apk打包过程概述

自定义Gradle插件

详见 Gradle自定义插件

如何使用Transform API

因为是编译期间搞事情,所以首先要在编译期间找一个时间点,这也就是本节 Transform 的内容,找到“作案”地点后,接下来就是“作案对象”了,这里选择的是对编译后的 .class 字节码下手,要用到的工具就是后面要介绍的 ASM 了。
在这里插入图片描述
上面是官方出品的编译打包签名流程,我们要搞事情的位置就是 Java Compiler 编译成 .class Files 之到打包为 .dex Files 这之间。Google 官方在 Android Gradle 的 1.5.0 版本以后提供了 Transfrom API, 允许第三方自定义插件在打包 dex 文件之前的编译过程中操作 .class 文件,所以这里先要做的就是实现一个自定义的 Transform 进行.class文件遍历拿到所有方法,修改完成对原文件进行替换。

下面说一下如何引入 Transform 依赖,在 Android gradle 插件 1.5 版本以前,是有一个单独的 transform api 的;从 2.0 版本开始,就直接并入到 gradle api 中了。

Gradle 1.5:

Compile ‘com.android.tools.build:transfrom-api:1.5.0’

Gradle 2.0 开始:

implementation 'com.android.tools.build:gradle:3.5.2'

Transform是作用在.class编译后,打包成.dex前,可以对.class和resource进行再处理的部分。为了验证,我们建立一个项目Build的一次。
在这里插入图片描述
可以很清楚的看到,原生就带了一系列Transform供使用。那么这些Transform是怎么组织在一起的呢,我们用一张图表示:
在这里插入图片描述
每个Transform其实都是一个gradle task,Android编译器中的TaskManager将每个Transform串连起来,第一个Transform接收来自javac编译的结果,以及已经拉取到在本地的第三方依赖(jar. aar),还有resource资源,注意,这里的resource并非android项目中的res资源,而是asset目录下的资源。 这些编译的中间产物,在Transform组成的链条上流动,每个Transform节点可以对class进行处理再传递给下一个Transform。我们常见的混淆,Desugar等逻辑,它们的实现如今都是封装在一个个Transform中,而我们自定义的Transform,会插入到这个Transform链条的最前面。

但其实,上面这幅图,只是展示Transform的其中一种情况。而Transform其实可以有两种输入,一种是消费型的,当前Transform需要将消费型型输出给下一个Transform,另一种是引用型的,当前Transform可以读取这些输入,而不需要输出给下一个Transform,比如Instant Run就是通过这种方式,检查两次编译之间的diff的。

Transform解读

class TraceTransform extends Transform {

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

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

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

    @Override
    boolean isIncremental() {
        return true
    }

    @Override
    void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
        super.transform(transformInvocation)
        ......
    }

我们一项项分析:

(1)

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

Name顾名思义,就是我们的Transform名称,再回到我们刚刚Build的流程里:
在这里插入图片描述
这个最终的名字是如何构成的呢?好像跟我们这边的定义的名字有区别。以transform开头,之后拼接ContentType,这个ContentType代表着这个Transform的输入文件的类型,类型主要有两种,一种是Classes,另一种是Resources,ContentType之间使用And连接,拼接完成后加上With,之后紧跟的就是这个Transform的Name,name在getName()方法中重写返回即可。

(2)

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

先来看代码注释,注释写的很清晰了,必须是CLASSES(0x01),RESOURCES(0x02)之一,相当于Transform需要处理的类型。

 /**
     * Returns the type(s) of data that is consumed by the Transform. This may be more than
     * one type.
     *
     * <strong>This must be of type {@link QualifiedContent.DefaultContentType}</strong>
     */
    @NonNull
    public abstract Set<ContentType> getInputTypes();
    
    ----------------------------------
    
     /**
     * The type of of the content.
     */
    enum DefaultContentType implements ContentType {
        /**
         * The content is compiled Java code. This can be in a Jar file or in a folder. If
         * in a folder, it is expected to in sub-folders matching package names.
         */
        CLASSES(0x01),

        /** The content is standard Java resources. */
        RESOURCES(0x02);

        private final int value;

        DefaultContentType(int value) {
            this.value = value;
        }

        @Override
        public int getValue() {
            return value;
        }
    }

(3)

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

先来看源码注释,这个的作用相当于用来Transform表明作用域

 /**
     * Returns the scope(s) of the Transform. This indicates which scopes the transform consumes.
     */
    @NonNull
    public abstract Set<Scope> getScopes();
开发一共可以选如下几种:

 /**
     * The scope of the content.
     *
     * <p>
     * This indicates what the content represents, so that Transforms can apply to only part(s)
     * of the classes or resources that the build manipulates.
     */
    enum Scope implements ScopeType {
        /** Only the project (module) content */
        PROJECT(0x01),
        /** Only the sub-projects (other modules) */
        SUB_PROJECTS(0x04),
        /** Only the external libraries */
        EXTERNAL_LIBRARIES(0x10),
        /** Code that is being tested by the current variant, including dependencies */
        TESTED_CODE(0x20),
        /** Local or remote dependencies that are provided-only */
        PROVIDED_ONLY(0x40),

        /**
         * Only the project's local dependencies (local jars)
         *
         * @deprecated local dependencies are now processed as {@link #EXTERNAL_LIBRARIES}
         */
        @Deprecated        PROJECT_LOCAL_DEPS(0x02),
        /**
         * Only the sub-projects's local dependencies (local jars).
         *
         * @deprecated local dependencies are now processed as {@link #EXTERNAL_LIBRARIES}
         */
        @Deprecated        SUB_PROJECTS_LOCAL_DEPS(0x08);

一般来说如果是要处理所有class字节码,Scope我们一般使用TransformManager.SCOPE_FULL_PROJECT。即

public static final Set<Scope> SCOPE_FULL_PROJECT =
            Sets.immutableEnumSet(
                    Scope.PROJECT,
                    Scope.SUB_PROJECTS,
                    Scope.EXTERNAL_LIBRARIES);

(4)

@Override
boolean isIncremental() {
    return true
}

增量编译开关。当我们开启增量编译的时候,相当input包含了changed/removed/added三种状态,实际上还有notchanged。需要做的操作如下:

  • NOTCHANGED: 当前文件不需处理,甚至复制操作都不用;
  • ADDED、CHANGED: 正常处理,输出给下一个任务;
  • REMOVED: 移除outputProvider获取路径对应的文件。

(5)

@Override
void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
    super.transform(transformInvocation)
    ......
}

先来看一下源码注释,它是Transform处理文件的核心代码:

 /**
     * Executes the Transform.
     *
     * <p>The inputs are packaged as an instance of {@link TransformInvocation}
     * <ul>
     *     <li>The <var>inputs</var> collection of {@link TransformInput}. These are the inputs
     *     that are consumed by this Transform. A transformed version of these inputs must
     *     be written into the output. What is received is controlled through
     *     {@link #getInputTypes()}, and {@link #getScopes()}.</li>
     *     <li>The <var>referencedInputs</var> collection of {@link TransformInput}. This is
     *     for reference only and should be not be transformed. What is received is controlled
     *     through {@link #getReferencedScopes()}.</li>
     * </ul>
     *
     * A transform that does not want to consume anything but instead just wants to see the content
     * of some inputs should return an empty set in {@link #getScopes()}, and what it wants to
     * see in {@link #getReferencedScopes()}.
     *
     * <p>Even though a transform's {@link Transform#isIncremental()} returns true, this method may
     * be receive <code>false</code> in <var>isIncremental</var>. This can be due to
     * <ul>
     *     <li>a change in secondary files ({@link #getSecondaryFiles()},
     *     {@link #getSecondaryFileOutputs()}, {@link #getSecondaryDirectoryOutputs()})</li>
     *     <li>a change to a non file input ({@link #getParameterInputs()})</li>
     *     <li>an unexpected change to the output files/directories. This should not happen unless
     *     tasks are improperly configured and clobber each other's output.</li>
     *     <li>a file deletion that the transform mechanism could not match to a previous input.
     *     This should not happen in most case, except in some cases where dependencies have
     *     changed.</li>
     * </ul>
     * In such an event, when <var>isIncremental</var> is false, the inputs will not have any
     * incremental change information:
     * <ul>
     *     <li>{@link JarInput#getStatus()} will return {@link Status#NOTCHANGED} even though
     *     the file may be added/changed.</li>
     *     <li>{@link DirectoryInput#getChangedFiles()} will return an empty map even though
     *     some files may be added/changed.</li>
     * </ul>
     *
     * @param transformInvocation the invocation object containing the transform inputs.
     * @throws IOException if an IO error occurs.
     * @throws InterruptedException
     * @throws TransformException Generic exception encapsulating the cause.
     */
    public void transform(@NonNull TransformInvocation transformInvocation)
            throws TransformException, InterruptedException, IOException {
        // Just delegate to old method, for code that uses the old API.
        //noinspection deprecation
        transform(transformInvocation.getContext(), transformInvocation.getInputs(),
                transformInvocation.getReferencedInputs(),
                transformInvocation.getOutputProvider(),
                transformInvocation.isIncremental());
    }

大致意思如下,具体大家一定要仔细看注释:

  • 如果拿取了getInputs()的输入进行消费,则transform后必须再输出给下一级
  • 如果拿取了getReferencedInputs()的输入,则不应该被transform。
  • 是否增量编译要以transformInvocation.isIncremental()为准。

在 transform 方法中主要做的事情就是把 Inputs 保存到 outProvider 提供的位置去。生成的位置见下图:
在这里插入图片描述
后面会讲到代码,主要有两个 transform 方法,一个 transformJar 就是简单的拷贝,另一个 transformDirectory,我们就是在这里用 ASM 对字节码进行修改的。

Transform注册和使用

在gradle插件中注册

class TracePlugin implements Plugin<Project>{

    @Override
    void apply(Project project) {
        println "------trace plugin begin-------"
        def android = project.extensions.findByType(AppExtension.class)
        android.registerTransform(new TraceTransform(project))
        println "------trace plugin end-------"
    }

}

参考 Transform详解

Java字节码

详见 《深入理解Java虚拟机》第6章 类文件结构

Java字节码编织框架——ASM

什么是ASM?

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

为什么选择ASM来进行字节码编织?

有前人做了实验。参考网易乐得团队的实验结果:
在这里插入图片描述

通过上表可见,ASM的效率更高。不过效率高的前提是该库的语法更接近字节码层面。所以上面的虚拟机相关知识显得更加重要。

ASM 库是一款基于 Java 字节码层面的代码分析和修改工具。ASM 可以直接生产二进制的 class 文件,也可以在类被加载入 JVM 之前动态修改类行为。
ASM 库的结构如下所示:
在这里插入图片描述

  • Core:为其他包提供基础的读、写、转化Java字节码和定义的API,并且可以生成Java字节码和实现大部分字节码的转换,在 访问者模式和
    ASM 中介绍的几个重要的类就在 Core API 中:ClassReader、ClassVisitor 和 ClassWriter 类.
  • Tree:提供了 Java 字节码在内存中的表现
  • Commons:提供了一些常用的简化字节码生成、转换的类和适配器
  • Util:包含一些帮助类和简单的字节码修改类,有利于在开发或者测试中使用
  • XML:提供一个适配器将XML和SAX-comliant转化成字节码结构,可以允许使用XSLT去定义字节码转化

Core API 介绍

(1)ClassVisitor 抽象类

如下所示,在 ClassVisitor 中提供了和类结构同名的一些方法,这些方法会对类中相应的部分进行操作,而且是有顺序的:visit [ visitSource ] [ visitOuterClass ] ( visitAnnotation | visitAttribute )* (visitInnerClass | visitField | visitMethod )* visitEnd
public abstract class ClassVisitor {

    ......

public void visit(int version, int access, String name, String signature, String superName, String[] interfaces);
public void visitSource(String source, String debug);
public void visitOuterClass(String owner, String name, String desc);
public AnnotationVisitor visitAnnotation(String desc, boolean visible);
public AnnotationVisitor visitTypeAnnotation(int typeRef, TypePath typePath, String desc, boolean visible);
public void visitAttribute(Attribute attr);
public void visitInnerClass(String name, String outerName, String innerName, int access);
public FieldVisitor visitField(int access, String name, String desc, String signature, Object value);
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions);
public void visitEnd();}
  • void visit(int version, int access, String name, String signature, String superName, String[] interfaces) 该方法是当扫描类时第一个调用的方法,主要用于类声明使用。下面是对方法中各个参数的示意:visit( 类版本 , 修饰符 , 类名 ,泛型信息 , 继承的父类 , 实现的接口)
  • AnnotationVisitor visitAnnotation(String desc, boolean visible) 该方法是当扫描器扫描到类注解声明时进行调用。下面是对方法中各个参数的示意:visitAnnotation(注解类型 , 注解是否可以在JVM 中可见)。
  • FieldVisitor visitField(int access, String name, String desc, String signature, Object value) 该方法是当扫描器扫描到类中字段时进行调用。下面是对方法中各个参数的示意:visitField(修饰符 , 字段名 , 字段类型 , 泛型描述 , 默认值)
  • MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) 该方法是当扫描器扫描到类的方法时进行调用。下面是对方法中各个参数的示意:visitMethod(修饰符 , 方法名 , 方法签名 ,泛型信息 , 抛出的异常)
  • void visitEnd() 该方法是当扫描器完成类扫描时才会调用,如果想在类中追加某些方法

(2)ClassReader 类

这个类会将 .class 文件读入到 ClassReader 中的字节数组中,它的 accept 方法接受一个 ClassVisitor 实现类,并按照顺序调用 ClassVisitor 中的方法。

(3)ClassWriter 类

ClassWriter 是一个 ClassVisitor 的子类,是和 ClassReader 对应的类,ClassReader 是将 .class 文件读入到一个字节数组中,ClassWriter 是将修改后的类的字节码内容以字节数组的形式输出。

(4)MethodVisitor & AdviceAdapter

MethodVisitor 是一个抽象类,当 ASM 的 ClassReader 读取到 Method 时就转入 MethodVisitor 接口处理。
AdviceAdapter 是 MethodVisitor 的子类,使用 AdviceAdapter 可以更方便的修改方法的字节码。其中比较重要的几个方法如下:

  • void visitCode():表示 ASM 开始扫描这个方法
  • void onMethodEnter():进入这个方法
  • void onMethodExit():即将从这个方法出去
  • void onVisitEnd():表示方法扫码完毕

(5)FieldVisitor 抽象类

FieldVisitor 是一个抽象类,当 ASM 的 ClassReader 读取到 Field 时就转入 FieldVisitor 接口处理。和分析 MethodVisitor 的方法一样,也可以查看源码注释进行学习,这里不再详细介绍。

操作流程

  1. 需要创建一个 ClassReader 对象,将 .class 文件的内容读入到一个字节数组中
  2. 然后需要一个 ClassWriter 的对象将操作之后的字节码的字节数组回写
  3. 需要事件过滤器 ClassVisitor。在调用 ClassVisitor 的某些方法时会产生一个新的 XXXVisitor
    对象,当我们需要修改对应的内容时只要实现自己的 XXXVisitor 并返回就可以了
input.directoryInputs.each { DirectoryInput directoryInput ->
    if (directoryInput.file.isDirectory()) {
        directoryInput.file.eachFileRecurse { File file ->
            def name = file.name
            if (name.endsWith(".class") && !(name == ("R.class"))
                    && !name.startsWith("R$") && !(name == ("BuildConfig.class"))) {

                ClassReader reader = new ClassReader(file.bytes)
                ClassWriter writer = new ClassWriter(reader, ClassWriter.COMPUTE_MAXS)
                ClassVisitor visitor = new TraceVisitor(writer)
                reader.accept(visitor, ClassReader.EXPAND_FRAMES)

                byte[] code = writer.toByteArray()
                def classPath = file.parentFile.absolutePath + File.separator + name
                FileOutputStream fos = new FileOutputStream(classPath)
                fos.write(code)
                fos.close()
            }
        }
    }

这个库也没什么可展开描述的,值得参考的资源:
AOP 的利器:ASM 3.0 介绍
ASM 库的介绍和使用

虽然有了ASM这种框架,可以很方便的修改class文件,但是如果不熟悉框架的使用,写起来还是有点吃力
人类总是懒惰的,试图找出一些捷径,于是有了一款Idea插件——ASM Bytecode Outline

ASM Bytecode Outline

详见 【我的Android进阶之旅】Android Studio 使用 ASM Bytecode Outline 插件来研究Java字节码
插件ASM Bytecode Outline,可以把java代码转为ASM框架的代码,那么我们可以先修改好一个类的代码,把代码转为ASM框架的代码,然后把需要的代码复制到插件中,这样就可以在自定义的gradle plugin中批量自动去修改目标类了。

TraceLog

使用自定义 Gradle 插件 + ASM 的方式实现了和 JakeWharton 的 hugo 库同样的功能的库,将特定注解的方法的传入参数、返回结果和执行时间打印到 Logcat 中,方便开发调试。
在这里插入图片描述

整个工程分3个模块,主模块是调用方,就是使用@TraceLog的地方。plugin模块是自定义的gradle插件。因为打印日志和业务无关性,这里把打印日志的功能单独拆分成一个模块tracelibrary。plugin模块依赖traceLibrary,在字节码插桩时调用traceLibrary里面的方法打印日志。

自定义gradle plugin

build.gradle

apply plugin: 'groovy'
//使用该插件,才能使用uploadArchives
apply plugin: 'maven'

repositories {
    jcenter()
}

dependencies {
    //使用gradle sdk
    compile gradleApi()
    //使用groovy sdk
    compile localGroovy()
    implementation 'com.android.tools.build:gradle:3.5.2'
    implementation 'org.ow2.asm:asm-all:5.2'
}

sourceCompatibility = "1.8"
targetCompatibility = "1.8"

uploadArchives {
    repositories.mavenDeployer {
        pom.version = '1.0.0'
        pom.artifactId = 'tracePlugin'
        pom.groupId = 'com.example.watson.plugin'
        repository(url: "file:///D:/repository/")
    }
}

TracePlugin.groovy

class TracePlugin implements Plugin<Project>{

    @Override
    void apply(Project project) {
        println "------trace plugin begin-------"
        def android = project.extensions.findByType(AppExtension.class)
        android.registerTransform(new TraceTransform(project))
        println "------trace plugin end-------"
    }

}

TraceTransform.groovy

class TraceTransform extends Transform {

    Project project

    TraceTransform(Project project) {
        this.project = project
    }

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

    @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 {
        transformInvocation.inputs.each { TransformInput input ->
            input.directoryInputs.each { DirectoryInput directoryInput ->
                if (directoryInput.file.isDirectory()) {
                    directoryInput.file.eachFileRecurse { File file ->
                        def name = file.name
                        if (name.endsWith(".class") && !(name == ("R.class"))
                                && !name.startsWith("R$") && !(name == ("BuildConfig.class"))) {

                            ClassReader reader = new ClassReader(file.bytes)
                            ClassWriter writer = new ClassWriter(reader, ClassWriter.COMPUTE_MAXS)
                            ClassVisitor visitor = new TraceVisitor(writer)
                            reader.accept(visitor, ClassReader.EXPAND_FRAMES)

                            byte[] code = writer.toByteArray()
                            def classPath = file.parentFile.absolutePath + File.separator + name
                            FileOutputStream fos = new FileOutputStream(classPath)
                            fos.write(code)
                            fos.close()
                        }
                    }
                }

                def dest = transformInvocation.outputProvider.getContentLocation(directoryInput.name,
                        directoryInput.contentTypes, directoryInput.scopes,
                        Format.DIRECTORY)


                FileUtils.copyDirectory(directoryInput.file, dest)
            }

            input.jarInputs.each { JarInput jarInput ->
                def jarName = jarInput.name
                def md5Name = DigestUtils.md5Hex(jarInput.file.getAbsolutePath())
                if (jarName.endsWith(".jar")) {
                    jarName = jarName.substring(0, jarName.length() - 4)
                }

                def dest = transformInvocation.outputProvider.getContentLocation(jarName + md5Name,
                        jarInput.contentTypes, jarInput.scopes, Format.JAR)

                FileUtils.copyFile(jarInput.file, dest)
            }
        }
    }
}

TraceVisitor.groovy

class TraceVisitor extends ClassVisitor {

    private String mClassName

    TraceVisitor(ClassVisitor classVisitor) {
        super(Opcodes.ASM5, classVisitor)
    }

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

    @Override
    MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
        MethodVisitor methodVisitor = super.visitMethod(access, name, desc, signature, exceptions)
        methodVisitor = new TraceMethodVisitor(Opcodes.ASM5, methodVisitor, access, mClassName, name, desc)
        return methodVisitor
    }
}

TraceMethodVisitor.groovy

class TraceMethodVisitor extends AdviceAdapter {

    private static final String COST_ANNOTATION_DESC = "Lcom/example/tracelibrary/TraceLog;"

    private boolean isInjected = false

    private int startTimeId

    private int methodId

    private String className

    private String methodName

    private String desc

    private boolean isStaticMethod

    private Type[] argumentArrays

    TraceMethodVisitor(int api, MethodVisitor mv, int access, String className, String methodName, String desc) {
        super(api, mv, access, methodName, desc)
        this.className = className
        this.methodName = methodName
        this.desc = desc
        argumentArrays = Type.getArgumentTypes(desc)
        isStaticMethod = ((access & Opcodes.ACC_STATIC) != 0)
    }

    @Override
    AnnotationVisitor visitAnnotation(String desc, boolean visible) {
        if (COST_ANNOTATION_DESC.equals(desc)) {
            isInjected = true
        }
        return super.visitAnnotation(desc, visible)
    }

    @Override
    protected void onMethodEnter() {
        if (isInjected) {
            methodId = newLocal(Type.INT_TYPE)
            mv.visitMethodInsn(INVOKESTATIC, "com/example/tracelibrary/core/MethodCache", "request", "()I", false)
            mv.visitIntInsn(ISTORE, methodId)

            for (int i = 0; i < argumentArrays.length; i++) {
                Type type = argumentArrays[i]
                int index = isStaticMethod ? i : (i + 1)
                switch (type.getSort()) {
                    case Type.BOOLEAN:
                    case Type.CHAR:
                    case Type.BYTE:
                    case Type.SHORT:
                    case Type.INT:
                        mv.visitVarInsn(ILOAD, index)
                        box(type)
                        break
                    case Type.FLOAT:
                        mv.visitVarInsn(FLOAD, index)
                        box(type)
                        break
                    case Type.LONG:
                        mv.visitVarInsn(LLOAD, index)
                        box(type)
                        break
                    case Type.DOUBLE:
                        mv.visitVarInsn(DLOAD, index)
                        box(type)
                        break
                    case Type.ARRAY:
                    case Type.OBJECT:
                        mv.visitVarInsn(ALOAD, index)
                        box(type)
                        break
                }
                mv.visitVarInsn(ILOAD, methodId)
                visitMethodInsn(INVOKESTATIC, "com/example/tracelibrary/core/MethodCache", "addMethodArgument",
                        "(Ljava/lang/Object;I)V", false)
            }

            startTimeId = newLocal(Type.LONG_TYPE)
            mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false)
            mv.visitIntInsn(LSTORE, startTimeId)
        }
    }

    @Override
    protected void onMethodExit(int opcode) {
        if (isInjected) {
            if (opcode == RETURN) {
                visitInsn(ACONST_NULL)
            } else if (opcode == ARETURN || opcode == ATHROW) {
                dup()
            } else {
                if (opcode == LRETURN || opcode == DRETURN) {
                    dup2()
                } else {
                    dup()
                }
                box(Type.getReturnType(this.methodDesc))
            }
            mv.visitLdcInsn(className)
            mv.visitLdcInsn(methodName)
            mv.visitLdcInsn(desc)
            mv.visitVarInsn(LLOAD, startTimeId)
            mv.visitVarInsn(ILOAD, methodId)
            mv.visitMethodInsn(INVOKESTATIC, "com/example/tracelibrary/core/MethodCache", "updateMethodInfo",
                    "(Ljava/lang/Object;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;JI)V", false)

            mv.visitVarInsn(ILOAD, methodId)
            mv.visitMethodInsn(INVOKESTATIC, "com/example/tracelibrary/core/MethodCache",
                    "printMethodInfo", "(I)V", false)
        }
    }
}

第三方库文件

看到,自定义gradle插件的TraceMethodVisitor会在方法执行前后织入需要的功能,这些功能就是第三方库的内容。

build.gradle

apply plugin: 'com.android.library'
//使用该插件,才能使用uploadArchives
apply plugin: 'maven'

android {
    compileSdkVersion 29
    buildToolsVersion "29.0.2"


    defaultConfig {
        minSdkVersion 15
        targetSdkVersion 29
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
        consumerProguardFiles 'consumer-rules.pro'
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }

}

dependencies {
    compile 'org.ow2.asm:asm-all:5.2'
}

uploadArchives {
    repositories.mavenDeployer {
        pom.version = '1.0.0'
        pom.artifactId = 'traceLibrary'
        pom.groupId = 'com.example.watson.library'
        repository(url: "file:///D:/repository/")
    }
}

MethodCache.java

public class MethodCache {

    /**
     * 方法缓存默认大小
     */
    private static final int INIT_CACHE_SIZE = 1024;
    /**
     * 方法名缓存
     */
    private static Vector<MethodInfo> mCacheMethods = new Vector<>(INIT_CACHE_SIZE);

    /**
     * 占位并生成方法ID
     *
     * @return 返回 方法 Id
     */
    public static int request() {
        mCacheMethods.add(new MethodInfo());
        return mCacheMethods.size() - 1;
    }

    public static void addMethodArgument(Object argument, int id) {
        MethodInfo methodInfo = mCacheMethods.get(id);
        methodInfo.addArgument(argument);
    }

    public static void updateMethodInfo(Object result, String className, String methodName, String methodDesc, long startTime, int id) {
        MethodInfo methodInfo = mCacheMethods.get(id);
        methodInfo.setCost((System.currentTimeMillis() - startTime));
        methodInfo.setResult(result);
        methodInfo.setMethodDesc(methodDesc);
        methodInfo.setClassName(className);
        methodInfo.setMethodName(methodName);
    }

    public static void printMethodInfo(int id) {
        MethodInfo methodInfo = mCacheMethods.get(id);
        Printer.printMethodInfo(methodInfo);
    }
}

MethodInfo.java

public class MethodInfo {

    private static final String OUTPUT_FORMAT = "The method's name is %s ,the cost is %dms and the result is ";

    private String mClassName;              // 类名
    private String mMethodName;             // 方法名
    private String mMethodDesc;             // 方法描述符
    private Object mResult;                 // 方法执行结果
    private long mCost;                     // 方法执行耗时
    private List<Object> mArgumentList;     // 方法参数列表

    MethodInfo() {
        mArgumentList = new ArrayList<>();
    }

    @Override
    public String toString() {
        return String.format(Locale.CHINA, OUTPUT_FORMAT, getMethodName(), mCost) + mResult;
    }

    /**
     * @param className 设置类名
     */
    public void setClassName(String className) {
        mClassName = className;
    }

    /**
     * @return 返回类名
     */
    public String getClassName() {
        mClassName = mClassName.replace("/", ".");
        return mClassName;
    }

    /**
     * @param methodName 设置方法名
     */
    public void setMethodName(String methodName) {
        mMethodName = methodName;
    }

    /**
     * @return 返回方法名
     */
    public String getMethodName() {
        StringBuilder msg = new StringBuilder();
        Type[] argumentTypes = Type.getArgumentTypes(mMethodDesc);
        msg.append('(');
        for (int i = 0; i < argumentTypes.length; i++) {
            msg.append(argumentTypes[i].getClassName());
            if (i != argumentTypes.length - 1) {
                msg.append(", ");
            }
        }
        msg.append(')');
        mMethodName = mMethodName + msg.toString();
        return mMethodName;
    }

    /**
     * @param cost 设置方法执行耗时
     */
    public void setCost(long cost) {
        this.mCost = cost;
    }

    /**
     * @return 返回方法执行耗时
     */
    public long getCost() {
        return mCost;
    }

    /**
     * @param result 设置方法执行结果
     */
    public void setResult(Object result) {
        this.mResult = result;
    }

    /**
     * @return 返回方法执行结果
     */
    public Object getResult() {
        return mResult;
    }

    /**
     * @param methodDesc 设置方法描述符
     */
    public void setMethodDesc(String methodDesc) {
        this.mMethodDesc = methodDesc;
    }

    /**
     * 添加方法参数
     *
     * @param argument 方法参数
     */
    public void addArgument(Object argument) {
        mArgumentList.add(argument);
    }

    /**
     * @return 得到方法参数列表
     */
    public List<Object> getArgumentList() {
        return mArgumentList;
    }
}

Printer.java

public class Printer {
    private static final String TAG = "TraceLog";

    private static final char TOP_LEFT_CORNER = '┌';
    private static final char BOTTOM_LEFT_CORNER = '└';
    private static final char HORIZONTAL_LINE = '│';
    private static final String DOUBLE_DIVIDER = "───────────────────────────────────------";
    private static final String TOP_BORDER = TOP_LEFT_CORNER + DOUBLE_DIVIDER + DOUBLE_DIVIDER;
    private static final String BOTTOM_BORDER = BOTTOM_LEFT_CORNER + DOUBLE_DIVIDER + DOUBLE_DIVIDER;

    private static final String CLASS_NAME_FORMAT = "%s The class's name: %s";
    private static final String METHOD_NAME_FORMAT = "%s The method's name: %s";
    private static final String ARGUMENT_FORMAT = "%s The arguments: ";
    private static final String RESULT_FORMAT = "%s The result: ";
    private static final String COST_TIME_FORMAT = "%s The cost time: %dms";

    public static void printMethodInfo(MethodInfo methodInfo) {
        Log.i(String.valueOf(0) + TAG, TOP_BORDER);
        Log.i(String.valueOf(1) + TAG, String.format(CLASS_NAME_FORMAT, HORIZONTAL_LINE, methodInfo.getClassName()));
        Log.i(String.valueOf(2) + TAG, String.format(METHOD_NAME_FORMAT, HORIZONTAL_LINE, methodInfo.getMethodName()));
        Log.i(String.valueOf(3) + TAG, String.format(ARGUMENT_FORMAT, HORIZONTAL_LINE) + methodInfo.getArgumentList());
        Log.i(String.valueOf(4) + TAG, String.format(RESULT_FORMAT, HORIZONTAL_LINE) + methodInfo.getResult());
        Log.i(String.valueOf(5) + TAG, String.format(Locale.CHINA, COST_TIME_FORMAT, HORIZONTAL_LINE, methodInfo.getCost()));
        Log.i(String.valueOf(6) + TAG, BOTTOM_BORDER);
    }
}

最后是注解的定义:

@Target(ElementType.METHOD)
public @interface TraceLog {
}

主Module

主Module是使用方,使用方式:

(1)项目工程的gradle.build添加gradle编译脚本依赖:

buildscript {
    repositories {
        maven {
            url uri('D:/repository')
        }
    }
    dependencies {
        classpath 'com.example.watson.plugin:tracePlugin:1.0.0'
    }
}

这里我使用的仓库是本地文件夹,以后可以上传服务器,做到远程依赖

(2)在需要使用的 module 中的 build.gradle 中应用插件:

apply plugin: com.example.watson.plugin.TracePlugin

同时添加第三方库依赖,这里同样使用的仓库是本地文件夹,以后可以上传服务器,做到远程依赖

repositories {
    maven {
        url uri('D:/repository')
    }
}
...
implementation 'com.example.watson.library:traceLibrary:1.0.0'

(3)添加注解

在需要被hook的方法上添加@TraceLog注解

public class MainActivity extends AppCompatActivity {
    private static final String TAG = "MainActivity";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        findViewById(R.id.btn).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                printPerson(new Person(66, "watson"), 100, true, (byte) 0, 'W');
            }
        });

    }

    @TraceLog
    private Person printPerson(Person person, int x, boolean flag, byte time, char temp) {
        Log.i(TAG, "flag is " + flag);
        Log.i(TAG, "time is " + time);
        Log.i(TAG, "temp is " + temp);
        person.setName("jack");
        person.setAge(x);
        try {
            Thread.sleep(1000);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return person;
    }
}

在编译后,我们可以找到被织入代码后的类文件方法:

D:projectplugin-masterappbuildintermediatestransformsTraceLogdebug28comexamplewatsonMainActivity.class

@TraceLog
private Person printPerson(Person person, int x, boolean flag, byte time, char temp) {
    int var6 = MethodCache.request();
    MethodCache.addMethodArgument(person, var6);
    MethodCache.addMethodArgument(new Integer(x), var6);
    MethodCache.addMethodArgument(new Boolean(flag), var6);
    MethodCache.addMethodArgument(new Byte(time), var6);
    MethodCache.addMethodArgument(new Character(temp), var6);
    long var7 = System.currentTimeMillis();
    Log.i("MainActivity", "flag is " + flag);
    Log.i("MainActivity", "time is " + time);
    Log.i("MainActivity", "temp is " + temp);
    person.setName("jack");
    person.setAge(x);

    try {
        Thread.sleep(1000L);
    } catch (Exception var10) {
        var10.printStackTrace();
    }

    MethodCache.updateMethodInfo(person, "com/example/watson/MainActivity", "printPerson", "(Lcom/example/watson/Person;IZBC)Lcom/example/watson/Person;", var7, var6);
    MethodCache.printMethodInfo(var6);
    return person;
}

点击按钮,测试结果:
在这里插入图片描述
DEMO下载地址

参考:
Android字节码插桩——详细讲解 附带Demo
从 Java 字节码到 ASM 实践

版权声明:本文来源CSDN,感谢博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/huaxun66/article/details/103497729
站方申明:本站部分内容来自社区用户分享,若涉及侵权,请联系站方删除。
  • 发表于 2021-05-16 12:55:42
  • 阅读 ( 725 )
  • 分类:

0 条评论

请先 登录 后评论

官方社群

GO教程

推荐文章

猜你喜欢