ASM

2,765 阅读11分钟

当前使用的Androidstudio版本为4.1gradle版本为6.5\color{blue}{当前使用的Android studio版本为4.1,gradle版本为6.5。}

ASM是什么?

ASM is an all purpose Java bytecode manipulation and analysis framework.

ASM官网的描述:一个java字节码的处理和解析框架。

在Android中.java文件在编译成.class文件后还需要转换成Android 虚拟机能识别的.dex文件。针对这种编译流程,我们可以在.class文件到.dex之前,对.class文件进行增改。由于是直接操作.class文件,所以可以通过AOP的思想,简单的实现java源码中很繁琐的功能,比如说统计所有的点击事件等。这些对于字节码的处理和解析都可以通过[ASM](https://asm.ow2.io/)实现。

在使用ASM之前,还有个问题:我们如何拿到编译好的字节码文件?

Starting with 1.5.0-beta1, the Gradle plugin includes a Transform API allowing 3rd party plugins to manipulate compiled class files before they are converted to dex files. (The API existed in 1.4.0-beta2 but it's been completely revamped in 1.5.0-beta1)

从1.5.0-beta1开始,Gradle插件包含一个Transform API,允许第三方插件在将已编译的类文件转换为dex文件之前对其进行操作。 (该API已存在于1.4.0-beta2中,但已在1.5.0-beta1中进行了彻底修改)

所以我们要先了解gradle transform如何使用

1. Gradle Transform

除了创建新project 默认自带的app module外,我们单独创建一个frc-pluginmodule用来实现Gradle Transform逻辑(以及后续的ASM逻辑)。

1.1 生成Transform

先创建一个 Android Library module,名称为frc-plugin

image-20210207143524356

将frc-plugin module的build.gradle清空,然后替换成下面代码:

plugins {
    id 'groovy'
    id 'maven'
}

dependencies {
    implementation gradleApi()
    implementation localGroovy()
    implementation 'com.android.tools.build:gradle:4.1.2'
}

repositories {
    jcenter()
}

//后面会用到这个task 来生成maven依赖
uploadArchives {
    repositories.mavenDeployer {
        //本地仓库路径
        repository(url: uri('../rep'))

        //下面就是一种格式,具体参数自己定义 最后组成:'com.rongcheng:frc_plugin:1.0.0'
        pom.groupId = 'com.rongcheng'
        pom.artifactId = 'frc_plugin'

        pom.version = '1.0.0'
    }
}

如下图:

image-20210207144456544

在src/main/目录下创建一个 groovy包,并删除自动生成的java包:

image-20210207144808952

在groovy包下创建了rongcheng.plugin目录,并创建了FrcTransform.groovy类:

image-20210207145251538
package rongcheng.plugin

import com.android.build.api.transform.Context;
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.TransformOutputProvider
import com.android.build.gradle.internal.pipeline.TransformManager;


/**
 * @author: frc* @description: 用于测试 gradle transform 功能
 * @date: 2/7/21 2:49 PM
 */
class FrcTransform extends Transform {
    /**
     *  transform 最后会转换成 gradle task 执行,这里返回个任务名
     * @return task name
     */
    @Override
    String getName() {
        return 'frc_transform'
    }

    /**
     * 指定当前transform 要处理的数据类型
     * @return .class类型的数据
     */
    @Override
    Set<QualifiedContent.ContentType> getInputTypes() {
        return TransformManager.CONTENT_CLASS
    }

    /**
     * 指定当前 transform 的作用范围
     * @return
     */
    @Override
    Set<? super QualifiedContent.Scope> getScopes() {
        return TransformManager.SCOPE_FULL_PROJECT
    }

    /**
     * 是否增量更新
     * @return false
     */
    @Override
    boolean isIncremental() {
        return false
    }

   
     /**
      * 正在执行 transform逻辑的方法,在这里能拿到.class文件,操作后再写出去
      * @param transformInvocation
      * @throws TransformException
      * @throws InterruptedException
      * @throws IOException
      */
     @Override
     void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
         super.transform(transformInvocation)
         println '》》》》》》》》》》》》》》this is frc transform》》》》》》》》》》》'
     }
}

每个方式上都有详细注释,可以看下。

现在我们在transform中打印个日志。看下能不能输出。

不过又引入另外一个问题,自定义的Transform怎么被执行呢?

我们可以通过gradle plugin 来使用这个Transform,然后再让app依赖该plugin。

1.2 自定义Gradle Plugin

如何自定义一个gradle plugin?只要实现Plugin接口,并依赖就行,如下就是在app module的build.gradle中写的一个简单的FrcPlugin

plugins {
    id 'com.android.application'
    id 'kotlin-android'
    id 'kotlin-kapt'
}

class FrcPlugin implements Plugin<Project>{

    @Override
    void apply(Project project) {
         println '》》》》》》 this is frc plugin 》》》》'
    }
}

apply plugin:FrcPlugin.class

...

日志成功输出:

image-20210207154152369

当然这种写法太死了,所以我们可以将这个FrcPlugin放到frc-plugin中:

package rongcheng.plugin

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

/**
 * @author: frc* @description:
 * @date: 2/7/21 3:43 PM
 */
class FrcPlugin implements Plugin<Project> {
    @Override
    void apply(Project project) {
        def appExtension = project.extensions.findByType(AppExtension.class)
        //注册
        appExtension.registerTransform(new FrcTransform())
    }
}

apply中奖FrcTransform进行了注册。

然后在src/main目录下创建一个resources目录:

image-20210207154720656

再在resources中创建一个/META-INF/gradle-plugins/xxxx.properites文件:

image-20210207163530819

注意com.rongcheng.frc-plugin.properties文件名com.rongcheng.frc-plugin就是之后要在build.gradle中apply的。

文件中implementation-class=rongcheng.plugin.FrcPlugin指定了依赖的类,会被触发执行。

1.3 使用Gradle Plugin

找到frc-compliermodule的uploadArchivestask,点击执行:

image-20210207164646748

如果成功会在根目录下生成rep文件夹:

image-20210207164744387

现在让app module中使用frc-plugin。

首先需要在project的build.gradle中配置如下:

image-20210207164900561

然后在app module的build.gradle中依赖

image-20210207165010847

然后编译app项目,会有日志输出。这说明FrcTransform被成功执行了:

image-20210207165333611

不过此时直接run app 是会有问题的,告诉你apk无法找到。

image-20210207165712353

问题出在:我们重写的transform方法

     /**
      * 正在执行 transform逻辑的方法,在这里能拿到.class文件,操作后再写出去
      * @param transformInvocation
      * @throws TransformException
      * @throws InterruptedException
      * @throws IOException
      */
     @Override
     void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
         super.transform(transformInvocation)
         println '》》》》》》》》》》》》》》this is frc transform》》》》》》》》》》》'
     }

因为这里传给你的.class文件你没有将它传出去,所以最终在生成.dex文件时找不到对应文件,导致无法生成apk。

1.4 transform()

 /**
      * 正在执行 transform逻辑的方法,在这里能拿到.class文件,操作后再写出去
      * @param transformInvocation
      * @throws TransformException
      * @throws InterruptedException
      * @throws IOException
      */
     @Override
     void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
         super.transform(transformInvocation)
     }  

transform(TransformInvocation t)方法就给我们返回了个TransformInvocation对象,我们后续的操作都靠它了。

/**
 * An invocation object used to pass of pertinent information for a
 * {@link Transform#transform(TransformInvocation)} call.
 */
public interface TransformInvocation {

    /**
     * Returns the context in which the transform is run.
     * @return the context in which the transform is run.
     */
    @NonNull
    Context getContext();

    /**
     * Returns the inputs/outputs of the transform.
     * @return the inputs/outputs of the transform.
     */
    @NonNull
    Collection<TransformInput> getInputs();

    /**
     * Returns the referenced-only inputs which are not consumed by this transformation.
     * @return the referenced-only inputs.
     */
    @NonNull Collection<TransformInput> getReferencedInputs();
    /**
     * Returns the list of secondary file changes since last. Only secondary files that this
     * transform can handle incrementally will be part of this change set.
     * @return the list of changes impacting a {@link SecondaryInput}
     */
    @NonNull Collection<SecondaryInput> getSecondaryInputs();

    /**
     * Returns the output provider allowing to create content.
     * @return he output provider allowing to create content.
     */
    @Nullable
    TransformOutputProvider getOutputProvider();


    /**
     * Indicates whether the transform execution is incremental.
     * @return true for an incremental invocation, false otherwise.
     */
    boolean isIncremental();
}

如注释:一个用来传递Transform调用相关信息的对象。

其中最重要的是:

 		@NonNull
    Collection<TransformInput> getInputs();
  
    @Nullable
    TransformOutputProvider getOutputProvider();

分别获取TransformInputTransformOutputProvider对象

  • TransformInput: transform时输入的对象,包含两种类型:jar包和文件类型(.class)
  • TransformOutputProvider: 用来获取文件的输出地址

添加如下代码,运行看下输出:

  /**
      * 正在执行 transform逻辑的方法,在这里能拿到.class文件,操作后再写出去
      * @param transformInvocation
      * @throws TransformException
      * @throws InterruptedException
      * @throws IOException
      */
     @Override
     void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
         super.transform(transformInvocation)
         println '》》》》》》》》》》》》》》this is frc transform》》》》》》》》》》》'

         // Transform 的 inputs 有两种类型,一种是目录,一种是 jar 包,要分开遍历
         def inputs = transformInvocation.getInputs()
         def outputProvider = transformInvocation.outputProvider


         inputs.each { TransformInput input ->
             //遍历目录
             input.directoryInputs.each { DirectoryInput directoryInput ->

                 println "input.directoryInputs input path: ${ directoryInput.getFile().absolutePath}"

                 //获取 output 目录
                 def dest = outputProvider.getContentLocation(directoryInput.name,
                         directoryInput.contentTypes, directoryInput.scopes,
                         Format.DIRECTORY)
                 println "input.directoryInputs  output path: ${dest.absolutePath}"

             }

             //遍历 jar
             input.jarInputs.each { JarInput jarInput ->
//                 // 重命名输出文件(同目录copyFile会冲突)
                 def jarName = jarInput.name
                 println "jarName: ${jarName}"
                 def md5Name = DigestUtils.md5Hex(jarInput.file.getAbsolutePath())
                 println "md5Name: ${md5Name}"
//
                 if (jarName.endsWith(".jar")) {
                     jarName = jarName.substring(0, jarName.length() - 4)
                 }
//
                 File copyJarFile = jarInput.file
                 println "copyJarFile path : ${copyJarFile.getAbsoluteFile()}"

//                 生成输出路径
                 def dest = outputProvider.getContentLocation(jarName + md5Name,
                         jarInput.contentTypes, jarInput.scopes, Format.JAR)

                 println "input.jarInputs path: ${dest.absolutePath}"



                 println "》》》》》》》》》》》》》》"
             }
         }
     }

下面是我截取的一部分日志输出:

分为两部分:jar和class文件,

> Task :app:transformClassesWithFrc_transformForDebug
》》》》》》》》》》》》》》this is frc transform》》》》》》》》》》》
----jar 部分 ---

jarName: androidx.core:core-ktx:1.3.2
md5Name: b3193180d0f251aee2907fdab0b4c786
copyJarFile path : /Users/frc/.gradle/caches/transforms-2/files-2.1/9612c86e7c70e163e9cd77fb9435bfd7/jetified-core-ktx-1.3.2-runtime.jar
input.jarInputs path: /Users/frc/Code/android/Study/app/build/intermediates/transforms/frc_transform/debug/45.jar
》》》》》》》》》》》》》》
jarName: androidx.lifecycle:lifecycle-common:2.1.0
md5Name: 8000dbc5e16924134f6474ec97a8bd71
copyJarFile path : /Users/frc/.gradle/caches/modules-2/files-2.1/androidx.lifecycle/lifecycle-common/2.1.0/c67e7807d9cd6c329b9d0218b2ec4e505dd340b7/lifecycle-common-2.1.0.jar
input.jarInputs path: /Users/frc/Code/android/Study/app/build/intermediates/transforms/frc_transform/debug/78.jar
...
》》》》》》》》》》》》》》

---class 目录部分---

input.directoryInputs input path: /Users/frc/Code/android/Study/app/build/intermediates/javac/debug/classes
input.directoryInputs  output path: /Users/frc/Code/android/Study/app/build/intermediates/transforms/frc_transform/debug/84
input.directoryInputs input path: /Users/frc/Code/android/Study/app/build/tmp/kotlin-classes/debug
input.directoryInputs  output path: /Users/frc/Code/android/Study/app/build/intermediates/transforms/frc_transform/debug/85
input.directoryInputs input path: /Users/frc/Code/android/Study/app/build/tmp/kapt3/classes/debug
input.directoryInputs  output path: /Users/frc/Code/android/Study/app/build/intermediates/transforms/frc_transform/debug/86

先输出的是jar和这些class文件原来所在的位置,然后输出是编译后他们应该去的位置。可以看到很多三方库原来的位置都是在/.gradle/caches目录下,想要编译当前apk,必须先要copy到默认目录,该默认目录通过outputProvider.getContentLocation()获取。不能随便传,不然编译器找不到,也没法编译。

之前报的异常,其实就是没有将需要的jarclass文件拷贝到默认位置,导致编译器无法完成编译造成\color{red}{之前报的异常,其实就是没有将需要的jar和class文件拷贝到默认位置,导致编译器无法完成编译造成}

以class文件为例,/Users/frc/Code/android/Study/app/build/intermediates/javac/debug/classes目录下有如下两个class文件。

image-20210209103631235

但是/Users/frc/Code/android/Study/app/build/intermediates/transforms/frc_transform/debug/84目录却是空的。上面代码只是读取,没有copy

image-20210209103728889

所以,在对class文件操作后,还需要将这些文件拷贝到默认路径。下面是完整代码,可以成功编译apk了。

 /**
      * 正在执行 transform逻辑的方法,在这里能拿到.class文件,操作后再写出去
      * @param transformInvocation
      * @throws TransformException
      * @throws InterruptedException
      * @throws IOException
      */
     @Override
     void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
         super.transform(transformInvocation)
         println '》》》》》》》》》》》》》》this is frc transform》》》》》》》》》》》'

         // Transform 的 inputs 有两种类型,一种是目录,一种是 jar 包,要分开遍历
         def inputs = transformInvocation.getInputs()
         def outputProvider = transformInvocation.outputProvider


         inputs.each { TransformInput input ->
             //遍历目录
             input.directoryInputs.each { DirectoryInput directoryInput ->

                 //获取 output 目录
                 def dest = outputProvider.getContentLocation(directoryInput.name,
                         directoryInput.contentTypes, directoryInput.scopes,
                         Format.DIRECTORY)

                 // 将 input 的目录复制到 output 指定目录
                 FileUtils.copyDirectory(directoryInput.file, dest)
             }

             //遍历 jar
             input.jarInputs.each { JarInput jarInput ->
                 // 重命名输出文件(同目录copyFile会冲突)
                 def jarName = jarInput.name
                 def md5Name = DigestUtils.md5Hex(jarInput.file.getAbsolutePath())
                 if (jarName.endsWith(".jar")) {
                     jarName = jarName.substring(0, jarName.length() - 4)
                 }
                 File copyJarFile = jarInput.file

                 //生成输出路径
                 def dest = outputProvider.getContentLocation(jarName + md5Name,
                         jarInput.contentTypes, jarInput.scopes, Format.JAR)

                 // 将 input 的目录复制到 output 指定目录
                 FileUtils.copyFile(copyJarFile, dest)


             }
         }
     }

现在``/Users/frc/Code/android/Study/app/build/intermediates/transforms/frc_transform/debug/`下就有文件了,也能成功编译了。

image-20210209104031787

至此,gradle transform 这块基本清楚了。下面要做的就是学会使用asm,然后在读取目标字节码,处理后再传给编译器。

ASM

ASM的内容很多,下面以一个简单的例子展示下使用的流程,不对具体的api进行讲解。具体api可以参考官网材料

官网

asm4-guide.pdf

2.1 添加依赖

ASM是个三方框架,所以我们首先需要在frc-plugin 中添加依赖。


dependencies {
    implementation gradleApi()
    implementation localGroovy()
    implementation 'com.android.tools.build:gradle:4.1.2'

    //包中定义类基于核心 API 的相关操作类
    //包中定义了核心 API 的类,其中 ClassVisitor、FieldVisitor、MethodVisitor 和 AnnotationVisitor
    // 四个抽象类,用于访问 .class 字节码文件中的 fields、 methods 和 annotations 相关的指令。
    implementation 'org.ow2.asm:asm:7.1'
    //包中提供了实用的类或方法的 Adapter 方法转换器;
    implementation 'org.ow2.asm:asm-commons:7.1'
    //包中提供了常见的类分析框架和分析器类;
    implementation 'org.ow2.asm:asm-analysis:7.1'
    //包中提供了基于核心 API 的常见工具类。
    implementation 'org.ow2.asm:asm-util:7.1'
    //包中定义了基于树 API 的类,以及一些用于事件和树 API 转换的工具类;
    implementation 'org.ow2.asm:asm-tree:7.1'
}

2.2 简单案例

现在希望通过ASM在MainActivityonCreate方法最前面添加System.out.println("<<< default message >>>")

package com.rong.cheng.study

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import com.rong.cheng.asm.PrintLog

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
      //类似动态添加了这行代码:         System.out.println("<<< default message >>>");
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

    }
}

首先我们要在transform的过程中找到MainActivity.class:

 @Override
    void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
        super.transform(transformInvocation)
        println '》》》》》》》》》》》》》》this is frc transform》》》》》》》》》》》'

        // Transform 的 inputs 有两种类型,一种是目录,一种是 jar 包,要分开遍历
        def inputs = transformInvocation.getInputs()
        def outputProvider = transformInvocation.outputProvider


        inputs.each { TransformInput input ->
            //遍历目录
            input.directoryInputs.each { DirectoryInput directoryInput ->
                //获取 output 目录
                def dest = outputProvider.getContentLocation(directoryInput.name,
                        directoryInput.contentTypes, directoryInput.scopes,
                        Format.DIRECTORY)
                //获取到class文件
                def dir = directoryInput.file
                if (dir) {
                    dir.traverse(type: FileType.FILES, nameFilter: ~/.*\.class/) {
                        File classFile ->
                            //过滤不需要修改的class
                            println "class file name : ${classFile.name}"
                            if (classFile.name == "MainActivity.class"){
                                println "get MainActivity class"
                            }
                    }
                }


                // 将 input 的目录复制到 output 指定目录
                FileUtils.copyDirectory(directoryInput.file, dest)
            }

            //遍历 jar
            ...
        }
    }

编译后日志输出如下,是可以找到对应的class文件的。

image-20210209152633216

获取到目标class文件后,使用 ClassReader来解析

if (classFile.name == "MainActivity.class") {
  ClassReader classReader = new ClassReader(classFile.bytes)
  ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS)
  ClassVisitor classVisitor = new FrcClassVisitor(Opcodes.ASM7, classWriter)
  classReader.accept(classVisitor, ClassReader.EXPAND_FRAMES)
	//覆盖原来的class文件
  byte[] code = classWriter.toByteArray()
  FileOutputStream fos = new FileOutputStream(classFile.parentFile.absolutePath + File.separator + classFile.name)
  fos.write(code)
  fos.close()
  println "get MainActivity class"
    }

上面我们使用了 ClassVisitor 来处理class文件

/**
 * @author: frc
 * @description:
 * @date: 2/9/21 3:55 PM
 */
public class FrcClassVisitor extends ClassVisitor implements Opcodes {
    public FrcClassVisitor(int api) {
        super(api);
    }

    public FrcClassVisitor(int api, ClassVisitor classVisitor) {
        super(api, classVisitor);
        System.out.println("FrcClassVisitor ----> start");
    }

    @Override
    public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
        super.visit(version, access, name, signature, superName, interfaces);
        System.out.println("FrcClassVisitor ----> visit");
        System.out.println("version: " + version);
        System.out.println("access: " + access);
        System.out.println("name: " + name);
        System.out.println("signature: " + signature);
        System.out.println("superName: " + superName);
        System.out.println("interfaces: " + Arrays.toString(interfaces));
    }

    @Override
    public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
        System.out.println("FrcClassVisitor ----> visitMethod");
        System.out.println("access: " + access);
        System.out.println("name: " + name);
        System.out.println("signature: " + signature);
        System.out.println("descriptor: " + descriptor);
        System.out.println("exceptions: " + Arrays.toString(exceptions));
        MethodVisitor mv = cv.visitMethod(access, name, descriptor, signature, exceptions);
        if (name.equals("onCreate")) {
            return new FrcMethodVisitor(Opcodes.ASM7, mv);
        }
        return mv;
    }

    @Override
    public void visitEnd() {
        super.visitEnd();
        System.out.println("FrcClassVisitor ----> end");
    }
}

visit是最先调用的方法,里面会返回一些类相关信息。

visitMethod 是当前class文件中方法的读取回调。

上面添加了日志,看下日志输出就知道了:

image-20210209174726121

可以看到当前class为com/rong/cheng/study/MainActivity,父类是androidx/appcompat/app/AppCompatActivity,没有实现接口等。

visitMethod执行了2次,说明com/rong/cheng/study/MainActivity有两个方法,一个是init,一个是onCreate()

现在我们需要在该onCreate()方法中添加System.out.println("<<< default message >>>");

//先判断方法名  
if (name.equals("onCreate")) {
            return new FrcMethodVisitor(Opcodes.ASM7, mv);
    }

方法的添加使用了MethodVisitor:

package rongcheng.plugin;

import org.objectweb.asm.MethodVisitor;

import static org.gradle.internal.impldep.bsh.org.objectweb.asm.Constants.GETSTATIC;
import static org.gradle.internal.impldep.bsh.org.objectweb.asm.Constants.INVOKEVIRTUAL;

/**
 * @author: frc
 * @description:
 * @date: 2/9/21 4:35 PM
 */
class FrcMethodVisitor extends MethodVisitor {
    public FrcMethodVisitor(int api) {
        super(api);
    }

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

    @Override
    public void visitCode() {
      	//System.out.println("<<< default message >>>");的字节码
        mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
        mv.visitLdcInsn("<<< default message >>>");
        mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
      //原方法之前
        super.visitCode();
			//原方法之后
    }

}

visitCode()中添加了具体逻辑的字节码。

最后编译看下MainActivity是否被修改:

image-20210209180630383

2.3 字节码辅助插件

如果能自己不能手写字节码的话,可以借助了插件show bytecode outline

不过该插件在android studio 4之后不兼容。所以我在 IDEA上使用了该插件。

image-20210209175545370

如下图使用java正常写需要的内容,然后右击选择show bytecode outline.

image-20210209175708907

选择需要的字节码,拷贝到MethodVistor中即可。

image-20210209175847645

本篇文章只是记录了一个简单的使用流程,为解决开发中的问题提供一个思路。Transform APIASM中的一些具体API的使用,需要在真实开发中逐步了解,多查文档即可。