阅读 244

Android编译插桩操作字节码(java字节码插桩)

1. 概念

什么是编译插桩

顾名思义,所谓的编译插桩就是在代码编译期间修改已有的代码或者生成新代码。我们项目中的 Dagger、ButterKnife或者kotlin都用到了编译插桩技术。

要理解编译插桩,我们要先知道在Android中.java 文件是怎么编译的。

WechatIMG106.png

如上图所示,demo.java通过javac命令编译成demo.class文件,然后通过字节码文件编译器将class文件打包成.dex。

我们今天要说的插桩,就是在class文件转为.dex之前修改或者添加代码。

2. 场景

我们什么时候会用到它呢?

  • 日志埋点

  • 性能监控

  • 权限控制

  • 代码替换

  • 代码调试

  • 等等...

3. 插桩工具介绍

  • AspectJ

AspectJ 是老牌 AOP(Aspect-Oriented Programming)框架。其主要优势是成熟稳定,使用者也不需要对字节码文件有深入的理解。

  • ASM

ASM 最初起源于一个博士的研究项目,在 2002 年开源,并从 5.x 版本便开始支持 Java 8。并且,ASM 是诸多 JVM 语言钦定的字节码生成库,它在效率与性能方面的优势要远超其它的字节码操作库如 javassist、AspectJ。其主要优势是内存占用很小,运行速度快,操作灵活。但是上手难度大,需要对 Java 字节码有比较充分的了解。

本文使用 ASM 来实现简单的编译插桩效果,接下来我们是想一个小需求,

4. 实践

1. 创建AsmDemo项目,其中只有一个MainActivity

QQ20211224-135648@2x.png

2.创建自定义gradle插件

QQ20211224-135825@2x.png删除module中main文件夹下所有目录,新建groovy跟java目录。

2222.pnggradle插件是用groovy编写的,所以groovy文件存放.groovy文件,java目录中存放asm相关类。 清空build.gradle文件内容,改为如下内容:

plugins {
    id 'groovy'
    id 'maven'
}
dependencies {
    implementation gradleApi()
    implementation localGroovy()
    implementation 'com.android.tools.build:gradle:3.5.4'
}
group = "demo.asm.plugin"
version = "1.0.0"

uploadArchives {
    repositories {
        mavenDeployer {
            repository(url: uri("../asm_lifecycle_repo"))
        }
    }
}复制代码

3.创建LifeCyclePlugin文件

package demo.asm.plugin

import org.gradle.api.Plugin
import org.gradle.api.Project

public class LifeCyclePlugin implements Plugin<Project> {

    @Override
    void apply(Project target) {
        println("hello this is my plugin")
    }
}复制代码

LifeCyclePlugin实现了Plugin接口,但我们在app中使用此插件的时候,LifeCyclePlugin的apply插件会被调用。

接着创建properties文件: 首先在main下面创建resources/META-INF/gradle-plugins目录,然后在gradle-plugins中创建demo.asm.lifecycle.properties,并填入如下内容:

implementation-class=demo.asm.plugin.LifeCyclePlugin复制代码

其中文件名demo.asm.lifecycle就是我们插件的名称,后续我们需要在app的build.gradle文件中引用此插件。 好了,现在我们的插件已经写完了,我们把他部署到本地仓库中来测试一下。发布地址在上述build.grale文件中repository属性配置。我将其配置在asm_lifecycle_repo目录中。

我们在 Android Studio 的右边栏找到 Gradle 中点击 uploadArchives,执行 plugin 的部署任务,构建成功后,本地会出现一个repo目录,就是我们自定义的插件。

333.png

我们测试一下demo.asm.lifecycle。

首先在项目根目录的build.gradle文件中添加

buildscript {
    ext.kotlin_version = '1.4.32'
    repositories {
        google()
        mavenCentral()
        maven { url 'asm_lifecycle_repo' }   //需要添加的内容
    }
    dependencies {
        classpath "com.android.tools.build:gradle:3.5.4"
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.4.32"
        classpath 'demo.asm.plugin:asm_lifecycle_plugin:1.0.0' //需要添加的内容
        
    }
}复制代码

然后在app的build.gradle中添加

id 'demo.asm.lifecycle'复制代码

然后我们执行命令./gradlew clean assembleDebug,可以看到hello this is my plugin 正确输出,说明我们自定义的gradle插件可以使用。

444.png

然后我们来自定义transform,来遍历.class文件 这部分功能主要依赖 Transform API。

4.自定义transform

什么是 Transform ?

Transform 可以被看作是 Gradle 在编译项目时的一个 task,在 .class 文件转换成 .dex 的流程中会执行这些 task,对所有的 .class 文件(可包括第三方库的 .class)进行转换,转换的逻辑定义在 Transform 的 transform 方法中。实际上平时我们在 build.gradle 中常用的功能都是通过 Transform 实现的,比如混淆(proguard)、分包(multi-dex)、jar 包合并(jarMerge)。 创建LifeCycleTransfrom文件,内容如下:

package demo.asm.plugin

import com.android.build.api.transform.QualifiedContent
import com.android.build.api.transform.Transform
import com.android.build.api.transform.TransformException
import com.android.build.api.transform.TransformInput
import com.android.build.api.transform.TransformInvocation
import com.android.build.gradle.internal.pipeline.TransformManager
import joptsimple.internal.Classes

/**
 * Transform 主要作用是检索项目编译过程中的所有文件。通过这几个方法,我们可以对自定义 Transform 设置一些遍历规则,
 */
public class LifeCycleTransform extends Transform {

    /**
     * 设置我们自定义的 Transform 对应的 Task 名称。Gradle 在编译的时候,会将这个名称显示在控制台上。
     * 比如:Task :app:transformClassesWithXXXForDebug。
     * @return
     */
    @Override
    String getName() {
        return "LifeCycleTransform"
    }
    /**
     * 在项目中会有各种各样格式的文件,通过 getInputType 可以设置 LifeCycleTransform 接收的文件类型,
     * 此方法返回的类型是 Set<QualifiedContent.ContentType> 集合。
     * 此方法有俩种取值
     * 1.CLASSES:代表只检索 .class 文件;
     * 2.RESOURCES:代表检索 java 标准资源文件。
     * @return
     */
    @Override
    Set<QualifiedContent.ContentType> getInputTypes() {
        return TransformManager.CONTENT_CLASS
    }
    /**
     * 这个方法规定自定义 Transform 检索的范围,具体有以下几种取值:
     * EXTERNAL LIBRARIES 只有外部库
     * PROJECT 只有项目内容
     * PROJECT LOCAL DEPS 只有项目的本地依赖(本地jar )
     * PROVIDED ONLY 只提供本地或远程依赖项
     * SUB PROJECTS 只有子项目。
     * SUB PROJECTS LOCAL DEPS 只有子项目的本地依赖项(本地jar)。
     * TESTED CODE 由当前变量(包括依赖项)测试的代码
     * @return
     */
    @Override
    Set<? super QualifiedContent.Scope> getScopes() {
        return TransformManager.PROJECT_ONLY
    }
    /**
     * isIncremental() 表示当前 Transform 是否支持增量编译,我们不需要增量编译,所以直接返回 false 即可。
     * @return
     */
    @Override
    boolean isIncremental() {
        return false
    }
    /**
     * 最重要的方法,在这个方法中,可以获取到俩个数据的流向
     * inputs:inputs 中是传过来的输入流,其中有两种格式,一种是 jar 包格式,一种是 directory(目录格式)。
     * outputProvider:outputProvider 获取到输出目录,最后将修改的文件复制到输出目录,这一步必须做,否则编译会报错。
     *
     * @param transformInvocation
     * @throws TransformException* @throws InterruptedException* @throws IOException
     */
    @Override
    void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
        Collection<TransformInput> tis = transformInvocation.inputs
        tis.forEach(ti -> {
            ti.directoryInputs.each {
                File file = it.file
                if (file) {
                    file.traverse {
                        println("find class:" + it.name)
                    }
                }
            }
        })

    }
}复制代码

然后将我们将自定义的transform注册到我们定义好的plugin中,LifeCyclePlugin代码修改如下:

package demo.asm.plugin

import com.android.build.gradle.AppExtension
import org.gradle.api.Plugin
import org.gradle.api.Project

public class LifeCyclePlugin implements Plugin<Project> {

    @Override
    void apply(Project target) {
        println("hello this is my plugin")
        def android = target.extensions.getByType(AppExtension)
        println "======register transform ========"
        LifeCycleTransform transform = new LifeCycleTransform()
        android.registerTransform(transform)

    }
}复制代码

然后再次执行./gradlew clean assembleDebug,可以看到项目中所有的.class文件都被输出了

555.png

5.使用 ASM,插入字节码到 Activity 文件

ASM 是一套开源框架,其中几个常用的 API 如下:

ClassReader:负责解析 .class 文件中的字节码,并将所有字节码传递给 ClassWriter。

ClassVisitor:负责访问 .class 文件中各个元素,还记得上一课时我们介绍的 .class 文件结构吗?ClassVisitor 就是用来解析这些文件结构的,当解析到某些特定结构时(比如类变量、方法),它会自动调用内部相应的 FieldVisitor 或者 MethodVisitor 的方法,进一步解析或者修改 .class 文件内容。

ClassWriter:继承自 ClassVisitor,它是生成字节码的工具类,负责将修改后的字节码输出为 byte 数组。

添加 ASM 依赖 在asm_demo_plugin的build.gradle中添加asm依赖

dependencies {
    implementation gradleApi()
    implementation localGroovy()
    implementation 'com.android.tools.build:gradle:3.5.4'
    implementation 'org.ow2.asm:asm:8.0.1'//需要添加的依赖
    implementation 'org.ow2.asm:asm-commons:8.0.1'//需要添加的依赖
}复制代码

在main/java下面创建包 demo/asm/asm目录并添加如下代码:

import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;


/**
 * Created by zhangzhenrui 
 */

public class LifeCycleClassVisitor extends ClassVisitor {

    private String className = "";
    private String superName = "";

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


    public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
        System.out.println("classVisitor methodName" + name + ",supername" + superName);
        MethodVisitor mv = cv.visitMethod(access, name, descriptor, signature, exceptions);
        if (superName.equals("android/support/v7/app/AppCompatActivity")) {
            if (name.equals("onCreate")) {
                return new LifeCycleMethodVisitor(className, name, mv);
            }
        }
        return mv;
    }


    public void visitEnd() {
        super.visitEnd();
    }

    public LifeCycleClassVisitor(ClassVisitor classVisitor) {
        super(Opcodes.ASM5, classVisitor);
    }
}复制代码
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;

/**
 * Created by zhangzhenrui 
 */

class LifeCycleMethodVisitor extends MethodVisitor {
    private String className;
    private String methodName;

    public LifeCycleMethodVisitor(String className, String methodName, MethodVisitor methodVisitor) {
        super(Opcodes.ASM5, methodVisitor);
        this.className = className;
        this.methodName = methodName;
    }

    public void visitCode() {
        super.visitCode();
        System.out.println("methodVistor visitorCode");
        mv.visitLdcInsn("TAG");
        mv.visitLdcInsn(className + "------>" + methodName);
        mv.visitMethodInsn(Opcodes.INVOKESTATIC, "android/util/Log", "e", "(Ljava/lang/String;Ljava/lang/String;)I", false);
        mv.visitInsn(Opcodes.POP);

    }

}复制代码

然后修改LifeCycleTransformtransform函数如下:

@Override
void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
    Collection<TransformInput> transformInputs = transformInvocation.inputs
    TransformOutputProvider outputProvider = transformInvocation.outputProvider
    transformInputs.each { TransformInput transformInput ->
        transformInput.directoryInputs.each { DirectoryInput directoryInput ->
            File file = directoryInput.file
            if (file) {
                file.traverse(type: FileType.FILES, namefilter: ~/.*.class/) { File item ->
                    ClassReader classReader = new ClassReader(item.bytes)
                    if (classReader.itemCount != 0) {
                        System.out.println("find class:" + item.name + "classReader.length:" + classReader.getItemCount())
                        ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS)
                        ClassVisitor classVisitor = new LifeCycleClassVisitor(classWriter)
                        classReader.accept(classVisitor, ClassReader.EXPAND_FRAMES)
                        byte[] bytes = classWriter.toByteArray()
                        FileOutputStream outputStream = new FileOutputStream(item.path)
                        outputStream.write(bytes)
                        outputStream.close()
                    }
                }
            }
            def dest = outputProvider.getContentLocation(directoryInput.name, directoryInput.contentTypes, directoryInput.scopes, Format.DIRECTORY)
            FileUtils.copyDirectory(directoryInput.file, dest)
        }

    }
}复制代码

重新部署我们的插件后,重新运行主项目,可以看到:

MainActivity------>onCreate复制代码

但是我们没有在MainActivity中写一行代码,这样就实现了动态注入日志的功能

5.总结

本篇文章主要讲述了在Android中使用asm动态操作字节码的流程,其中涉及到的技术点有

  • 自定义gradle插件

  • transform的使用

  • asm的使用


作者:良辰公子
链接:https://juejin.cn/post/7047026102925508622

文章分类
ANDROID
版权声明:本站是系统测试站点,无实际运营。本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 XXXXXXo@163.com 举报,一经查实,本站将立刻删除。
相关推荐