前言

  • 前一篇文章,我们讲了gradle插件从创建到执行的整体链路。
  • 接下来我们来过一过字节码插桩的一些必备的知识点。
  • 本系列的ASM插桩教程,是根据笔者自己探索得来的一些总结。因为也是在摸索中学习,所以有说的不对的地方,请大家评论指正。
  • 或者在学习之余,大家一起交流交流。

ASM插桩基础知识

字节码

  • 因为ASM是操作Java字节码的库,所以我们先介绍下Java字节码
  • Java字节码是Java虚拟机(JVM)的指令集。每一条指令由一个字节的操作码和0个或者多个操作数组成。
  • 简单说来,字节码就是javac编译后生成的.class文件中的字节指令。
  • Java虚拟机解释执行的就是这些字节码文件。
  • 具体的细节知识点,在这里就先不细讲了,毕竟目前笔者也还停留在使用(ASM)阶段,等后续有了更深了解,我们在单开文章来讨论。
  • 具体的字节码文件,会是如下代码样式:
// class version 51.0 (51)
// access flags 0x21
public class com/muxi/gradleasmrock/RockApplication extends android/app/Application  {

  // compiled from: RockApplication.java

  // access flags 0x1
  public <init>()V
   L0
    LINENUMBER 5 L0
    ALOAD 0
    INVOKESPECIAL android/app/Application.<init> ()V
    RETURN
   L1
    LOCALVARIABLE this Lcom/muxi/gradleasmrock/RockApplication; L0 L1 0
    MAXSTACK = 1
    MAXLOCALS = 1

  // access flags 0x1
  public onCreate()V
   L0
    LINENUMBER 9 L0
    ALOAD 0
    INVOKESPECIAL android/app/Application.onCreate ()V
   L1
    LINENUMBER 10 L1
    RETURN
   L2
    LOCALVARIABLE this Lcom/muxi/gradleasmrock/RockApplication; L0 L2 0
    MAXSTACK = 1
    MAXLOCALS = 1
}

  • 其源码十分简单,如下:
package com.muxi.gradleasmrock;

import android.app.Application;

public class RockApplication extends Application {

    @Override
    public void onCreate() {
        super.onCreate();
    }
}

常用概念

类名

  • 字节码中对应类名,和java文件对应的类名是不一致的:

  • Java:

    com.muxi.gradleasmrock.RockApplication

  • 字节码:

    com/muxi/gradleasmrock/RockApplication

变量

  • 字节码中对应的基本数据类型和对象,也有不同的替代,如下表:
java 字节码
byte B
char C
double D
float F
int I
long J
short S
boolean Z
void V
Object Ljava/lang/Object;
int[] [I
Object[][] [[Ljava/lang/Object;

函数

  • 函数也对应不同的表示:
java 字节码
void m(int i, float f) (IF)V
int m(Object o) (Ljava/lang/Object;)I
int[] m(int i, String s) (ILjava/lang/String;)[I
Object m(int[] i) ([I)Ljava/lang/Object;
  • 上述概念,其实笔者也记不住,需要用的时候再查一查,用久了自然熟能生巧。

Gradle Transform初探

  • 上面介绍完了字节码的常用概念,我们来看下如果需要进行ASM插桩的时候,需要提前做的一些准备工作

引用ASM

  • ASM也是第三方库,使用dependencies依赖,就能很方便地引入。
  • 我们在asmrock的module中,找到build.gradle,加入以下代码,就可以直接使用asm插桩库了:
dependencies {
    implementation gradleApi()
    implementation localGroovy()
    //asm库之一
    implementation 'org.ow2.asm:asm:7.2'
    //asm库之一
    implementation 'org.ow2.asm:asm-util:7.2'
    //asm库之一
    implementation 'org.ow2.asm:asm-commons:7.2'
    //hunter是一个在asm基础上做了封装的三方库,能更方便我们使用asm插桩
    implementation 'com.quinn.hunter:hunter-transform:0.9.3'
    //gradle插件中需要访问Android工程相关的gradle配置,所以引入android的gradle插件版本
    implementation 'com.android.tools.build:gradle:3.6.2'
    implementation 'com.android.tools.build:gradle-api:3.6.2'
}

创建插件实现

  • 引入asm库后,我们需要针对性地接管gradle的task,让gradle执行过程中,字节码生成后的task能流经我们自己的gradle插件。
  • 因此,我们需要把我们的gradle插件注册到当前工程的编译Task任务中去。
  • 这一系列操作,是gradle提供好的,我们只需要实现对应接口就好。
实现
  • gradle提供了一个接口类:com.android.build.api.trasform.Transform
  • Android编译过程中,实现了Transform接口的类,只要调用registerTransform()函数,就能将这个实现类注册到现有工程的gradle编译Task中。
  • 此时,执行gradle编译,task任务就会流经这个Transform的实现类。

有点装饰器的意思。

  • 我们先来创建一个Transform接口的实现类:
public class AsmRockCodeTransform implements Transform {

    @Override
    public String getName() {
        return getClass().getName();//获取Transform的名称
    }

    @Override
    public Set<QualifiedContent.ContentType> getInputTypes() {
        return TransformManager.CONTENT_CLASS;//获取Transform消费的数据,比如Class,比如jar包
    }

    @Override
    public Set<? super QualifiedContent.Scope> getScopes() {
        return TransformManager.SCOPE_FULL_PROJECT;//获取Transform消耗的范围(整个project包含sub和三方内容 、或者当前project等)
    }

    @Override
    public boolean isIncremental() {
        return false;//是否增量任务,比如add/remove/change files,当有其他增量任务控制时,也会被影响,一般为false
    }
}
  • AsmRockCodeTransform需要实现三个函数,分别控制Transform的名称,作用数据、作用范围以及是否增量任务。
  • 我们需要做的是遍历所有class文件,所以就按照上面注释所写,进行配置就好。届时,编译过程中所有的class文件都会流经我们的Transform。
  • 当然,我们引入了Hunter库,其实Hunter库为我们实现Transform做了一些额外的封装,可以简化我们很多的操作。
  • 因此,我们的AsmRockCodeTransform最终会使用Hunter库中的HunterTransform
public class AsmRockCodeTransform extends HunterTransform {

    public AsmRockCodeTransform(Project project) {
        super(project);
    }
}
  • HunterTransform其实就是实现了Transform接口,为我们封装了一层Abstract继承,使用还是基本一致的,感兴趣的同学可以自行撸源码,这里就不赘述了。
注册
  • 创建好了Transform,接下来的工作就是将Transform注册到gradle的Task中了。
  • 因为Transform本来就是Android的gradle编译提供的接口,所以我们注册也是使用Android的gradle编译提供的接口进行注册。
  • 现在,我们回到之前写好的AsmRock类中。还记得这个类吧?我们前面说过在asm_rock.properties中指定的类就是插件的入口。
  • AsmRock我们之前的实现是这样的:
public class AsmRock implements Plugin<Project> {
    @Override
    public void apply(Project project) {

        System.out.println("Hello world!AsmRock!");
    }
}
  • 接下来,我们需要在插件被调用时,将我们需要做的Transform操作实现类,注册到Project中。
public class AsmRock implements Plugin<Project> {
    @Override
    public void apply(Project project) {

        //AppExtension 就是 Android项目中的build.gradle下android对应的配置类:
        // android {
        //    compileSdkVersion 29
        //    buildToolsVersion "29.0.2"
        //    defaultConfig {
        //        applicationId "com.muxi.gradleasmrock"
        //        minSdkVersion 23
        //        targetSdkVersion 29
        //        versionCode 1
        //        versionName "1.0"
        //    }
        //}
        // AppExtension是Android的gradle插件提供的,这就是为什么我们要在asm_rock的module中引用:
        // implementation 'com.android.tools.build:gradle:3.6.2'
        // implementation 'com.android.tools.build:gradle-api:3.6.2'
        AppExtension appExtension = (AppExtension) project.getProperties().get("android");//获取android属性相关的配置类
        if (appExtension != null) {
            appExtension.registerTransform(new AsmRockCodeTransform(project));//获取到Android项目的配置类,Transform就是注册到AppExtension中的。
        }
    }
}
  • 上述代码,注册完毕,在Android的编译过程中,就会自动执行AsmRockCodeTransform中的相应逻辑。
执行
  • 注册成功以后,gradle编译task执行到AsmRockCodeTransform的时候,会自动回调AsmRockCodeTransform。
  • 回调的函数,就是transform()函数
  • 在Transform接口中,有两个transform函数:
public void transform(
            @NonNull Context context,
            @NonNull Collection<TransformInput> inputs,
            @NonNull Collection<TransformInput> referencedInputs,
            @Nullable TransformOutputProvider outputProvider,
            boolean isIncremental) throws IOException, TransformException, InterruptedException ;

public void transform(@NonNull TransformInvocation transformInvocation)
            throws TransformException, InterruptedException, IOException ;
  • 第一个已经被Deprecated了,但是其实还是会被回调的。ASM新版本推荐使用第二个transform(),其实逻辑上来说,两者都会被回调,所以使用谁都可以。
  • HunterTransform实现的是Deprecated的transform,所以如果使用了HunterTransform的同学,覆写的就是第一个transform()。
  • 当gradle回调transform的时候,就表示AsmRockCodeTransform到了执行时机,此时我们就可以在AsmRockCodeTransform中,做字节码插桩处理了。
  • 照例,我们先来实际运行下看看效果:
public class AsmRockCodeTransform extends HunterTransform {

    public AsmRockCodeTransform(Project project) {
        super(project);
    }

    @Override
    public void transform(Context context, Collection<TransformInput> inputs, Collection<TransformInput> referencedInputs, TransformOutputProvider outputProvider, boolean isIncremental) throws IOException, TransformException, InterruptedException {
        System.out.println("AsmRockCodeTransform execute");
    }
}
  • gradle插件,还是要被apply到Android的app工程中才能执行。
  • 我们变更了gradle插件的代码,因此需要重新发布到本地的maven仓库,才能真正被执行起来。

./gradlew uploadArchives

  • 执行上述命令,重新发布gradle插件代码,此时再去执行:

./gradlew assembleDebug

  • 执行完毕后,就能看到AsmRockCodeTransform的输出:

小结

  • 到这里,ASM操作字节码之前的工作基本都准备就绪了。下一步就是要使用ASM库具体的访问操作字节码。

 评论