Gradle Transform API简介

概述

动态编译技术在开源框架中的应用非常的广泛,现在市面上的插件化框架,热修复框架几乎都使用了动态编译技术,原理几乎都是在编译期间动态的在class文件中注入代码或者或修改。

Java的AOP 框架是一个比较出名的面向切面编程框架,这是一种通过预编译方式和运行期动态代理实现在不修改源代码的情况下给程序动态统一添加功能的技术,可以动态的修改Java的Class字节码的内容。而比较出名的AOP 框架是AspectJ,不管可惜的是这个框架是在gradle上需要一些变换才能使用,使用的时候也比较麻烦。AspectJ框架的命令有几个重要的参数: - classpath:class和source 的位置 - aspectpath: 定义了切面规则的class - d:指定输出的目录 - outjar:指定输出的jar上 - inpath:需要处理的.class

其中,classpath 的作用是在当解析一个类的时候,当这个类是不在inpath 中,会从classpath 中寻找。

不过Android官方从gradle 1.5版本开始,Android官方提供了Gradle Transform技术用于在项目构建阶段,即由class到dex转换期间修改class文件的一套api,借用这套api,开发者可以很容易的完成字节码插桩、代码注入技术等注入技术。 官方文档:http://google.github.io/android-gradle-dsl/javadoc/

API

在Transform 中有几个重要的概念需要特别注意:

public static final Set<scope> SCOPE_FULL_PROJECT = Sets.immutableEnumSet(
        Scope.PROJECT,
        Scope.PROJECT_LOCAL_DEPS,
        Scope.SUB_PROJECTS,
        Scope.SUB_PROJECTS_LOCAL_DEPS,
        Scope.EXTERNAL_LIBRARIES);
public static final Set<qualifiedcontent.scopetype> SCOPE_FULL_INSTANT_RUN_PROJECT =
        new ImmutableSet.Builder<qualifiedcontent.scopetype>()
                .addAll(SCOPE_FULL_PROJECT)
                .add(InternalScope.MAIN_SPLIT)
                .build();
public static final Set<scope> SCOPE_FULL_LIBRARY = Sets.immutableEnumSet(
        Scope.PROJECT,
        Scope.PROJECT_LOCAL_DEPS);

认识Project

根据Gradle官网的介绍,Project是Gradle交互的主接口,通过Project你可以通过代码使用所有的Gradle特性,Project与build.gradle是一对一的关系。 首先,先看一个通过Project访问的使用场景:Extension。读者可能对Extension不是很熟悉,但一定熟悉下面的脚本配置:

android {
    compileSdkVersion 28
    buildToolsVersion "28.0.0"

    defaultConfig {
        applicationId "com.hc.hcplugin"
        minSdkVersion 15
        targetSdkVersion 28
        versionCode 1
        versionName "1.0"
    }
    
}

或许,读者跟我一样,又一个疑惑:上面的android{}、compileSdkVersion、标签是如何被Android的Gradle插件读取到的呢?或许你已经想到了,没错,就是通过Extension方式。关于这方面的内容,读者可以自行查找相关的资料进行学习。

实战

自定义Gradle插件

众所周知,Android Studio并没有提供新建类似Gradle Plugin选项,那我们如何在Android Studio中编写Gradle插件呢?可行的办法是,选择一个Module类型(如Phone&Tablet Module或Android Librarty)来容纳我们的插件。所以,新建自定义插件可以参考下面的步骤:

  1. 新建一个Android Project;
  2. 然后,再新建一个Module,这个Module用于开发Gradle插件。因为Module里面并没有提供gradle plugin,所以我们需要提供一个“容器”来容纳我们的插件。即新建一个Module类型的库工程(如Phone&Tablet Module或Android Librarty)。
  3. 将Module里面的内容删除,只保留build.gradle和src/main目录。 由于gradle基于groovy实现的,因此,我们开发的gradle插件相当于一个groovy项目,所以需要在main目录下新建groovy目录;
  4. 在main目录下创建groovy文件夹,然后在groovy目录下就可以创建我们的包名和groovy文件了,记得后缀要已.groovy结尾。可以通过new->file->MyPlugin.groovy来新建名为MyPlugin的groovy文件。
  5. 为了让我们的groovy类申明为gradle的插件,新建的groovy需要实现org.gradle.api.Plugin接口。例如:

package  com.xzh.plugin

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


public class MyPlugin implements Plugin<project> {

  void apply(Project project) {
      System.out.println("------------------开始----------------------");
      System.out.println("这是自定义插件!");
      System.out.println("------------------结束----------------------->");
  }
}

  1. 在main目录下创建resources文件夹,在resources下创建META-INF文件夹,继续在META-INF文件夹下创建gradle-plugins文件夹,最后在gradle-plugins文件夹下创建一个xxx.properties文件。目录结构如下图所示: 在这里插入图片描述

  2. 因为要用到groovy以及后面打包要用到maven,所以打开build.gradle 删除里面所有的内容,并添加uploadArchives相关的配置。例如:

apply plugin: 'groovy'
apply plugin: 'maven'

dependencies {
    compile gradleApi()//gradle sdk
    compile localGroovy()//groovy sdk
}

repositories {
    mavenCentral()
}


group='com.xzh.plugin'
version='1.0.0'

uploadArchives {
    repositories {
        mavenDeployer {
            //提交到远程服务器:
            // repository(url: "http://www.xxx.com/repos") {
            //    authentication(userName: "admin", password: "admin")
            // }
            repository(url: uri('/Users/xiangzhihong029/Documents/Transform'))
        }
    }
}

然后,重新执行一遍编译操作,将看到最终的项目编译成功,且项目的结构如下图: 在这里插入图片描述 然后,执行我们自己插件myplugin的upload操作,就会在我们先前配置文件com.xzh.gradle.properties的上传的地址中看到打包好的插件。例如,我配的我本地的地址: 在这里插入图片描述

应用自定义gradle插件

在项目中使用自定义gradle插件和使用系统提供的插件类似,只不过使用系统提供的插件是从jcent,maven等地方下载的。添加自定义的gradle插件只需要在build.gradle(也可以在module中)中的repositories模块中添加自定义gradle插件地址即可。例如:

buildscript {
    repositories {
        maven {
            url uri('/Users/xiangzhihong029/Documents/Transform')
        }
    }
    dependencies {
        classpath 'com.xzh.plugin:myplugin:1.0.0'
    }
}
apply plugin: 'com.xzh.gradle'

然后,clean一下工程,然后再Make一下 ,就可以在Gradle Console中看到我们的插件得到了执行。 在这里插入图片描述 当然,上面介绍的是将自定义Gradle插件并在项目中使用的例子。其实,我们还可以也可以将它作为一个Gradle插件库来使用,使用的时候只需要添加Gradle插件即可。例如:

apply plugin: com.xzh.second.SecondPlugin

Transform实战

前面说过,gradle从1.5开始,gradle插件包含了一个叫Transform的API,这个API允许第三方插件在class文件转为为dex文件前操作编译好的class文件。利用这一特性,我们可以很容易在代码编译阶段实现注入代码或者代码修改,这也是很多埋点技术和热修复技术的基础。

1,首先,在我们自定义的gradle插件的build.gradle中引入transform的包,配置的脚本文件如下:

dependencies {
    compile 'com.android.tools.build:gradle:3.1.2'

    compile gradleApi()
    compile localGroovy()

    compile 'com.android.tools.build:transform-api:1.5.0'
    compile 'javassist:javassist:3.12.1.GA'
    compile 'commons-io:commons-io:2.5'
}

2,然后,创建一个类继承Transform 并实现相关方法。例如:

package com.xzh.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;
import org.gradle.api.Project;

import java.io.IOException;
import java.util.Collection;
import java.util.Set;

public class MyClassTransform extends Transform {

    private Project mProject;

    public MyClassTransform(Project p) {
        this.mProject = p;
    }

    
    @Override
    public String getName() {
        return "MyClassTransform";
    }

    //需要处理的数据类型,有两种枚举类型:CLASSES和RESOURCES,CLASSES代表处理的java的class文件,RESOURCES代表要处理java的资源
    @Override
    public Set<qualifiedcontent.contenttype> getInputTypes() {
        return TransformManager.CONTENT_CLASS;
    }

    //    指Transform要操作内容的范围,官方文档Scope有7种类型:
//
//    EXTERNAL_LIBRARIES        只有外部库
//    PROJECT                       只有项目内容
//    PROJECT_LOCAL_DEPS            只有项目的本地依赖(本地jar)
//    PROVIDED_ONLY                 只提供本地或远程依赖项
//    SUB_PROJECTS              只有子项目。
//    SUB_PROJECTS_LOCAL_DEPS   只有子项目的本地依赖项(本地jar)。
//    TESTED_CODE                   由当前变量(包括依赖项)测试的代码
    @Override
    public Set<qualifiedcontent.scope> getScopes() {
        return TransformManager.SCOPE_FULL_PROJECT;
    }

    
    @Override
    public boolean isIncremental() {
        //指明当前Transform是否支持增量编译
        return false;
    }

    //    Transform中的核心方法,
//    inputs中是传过来的输入流,其中有两种格式,一种是jar包格式一种是目录格式。
//    outputProvider 获取到输出目录,最后将修改的文件复制到输出目录,这一步必须做不然编译会报错
    @Override
    public void transform(Context context,
                          Collection<transforminput> inputs,
                          Collection<transforminput> referencedInputs,
                          TransformOutputProvider outputProvider,
                          boolean isIncremental) throws IOException, TransformException, InterruptedException {

    }

}

3,接下来,定义一个类并实现Plugin接口,在apply方法中注册自定义的Transform。例如:


class MyPlugin implements Plugin<project> {
    @Override
    void apply(Project project) {
 
        def android = project.extensions.findByType(AppExtension.class)
        android.registerTransform(new MyClassTransform(project))
 
    }

Javassist

Javassist是一款实现在编译的过程中操作.class文件,动态注入代码Javassist是一个可以用来分析、编辑和创建Java字节码的开源类库。 关于Javassist代码注入相关的知识可以参考Javassist代码注入示例代码

参考:面向切面编程AspectJ在Android埋点的实践 Plugin Transform Javassist操作Class文件 Gradle的Transform配合ASM实战路由框架 Javassist 使用指南(一) Javassist 使用指南(二) Javassist 使用指南(三) Android热补丁动态修复技术

</qualifiedcontent.scope></qualifiedcontent.contenttype></qualifiedcontent.scopetype></qualifiedcontent.scopetype>