Android自定义Gradle插件来处理注解

熟悉Java注解处理器的朋友,肯定会了解如butterknife,dagger之类的框架,这类框架都是在编译阶段处理注解来生成辅助类,从而不需要再写很多机械的代码。这里我们换一种思路,不使用Java的注解处理器,直接使用Gradle来处理注解并生成类。注意:这篇文章仅提供一种注解处理的思路,不会考虑太多程序的健壮性。话不多说,直接开始吧。

在Android Studio中创建Gradle插件

为了方便起见,我就直接使用buildSrc来进行插件的构建了。这个目录会直接引入java以及groovy的api,如果单独简历module,我们就需要把插件提交到本地仓库,或者JCenter之类的才能使用了。

1.新建项目并创建buildSrc目录

我们随便新建一个Android项目,并在目录中添加一个buildSrc目录以及groovy的目录。最终项目结构:
Android自定义Gradle插件来处理注解_第1张图片

2.添加Gradle插件类,并创建任务

我们在buildSrc目录下添加一个插件类。准备好对应的包名,注意文件的后缀需要是groovy。
Android自定义Gradle插件来处理注解_第2张图片

接下来我们来添加插件的代码。在这之前,首先整理一下思路。

  • 如何拿到项目中的src原文件目录
    ———在Gradle配置的时候,我们知道有个sourcesSet的配置选项,同理,我们可以通过这个属性拿到对应的源码目录。
  • 如何判断有哪些文件使用了我们的注解
    ———一般来说我们的注解以及辅助类都放在自己的包里,那么如果正式项目中要引用必然会导包,我们可以遍历所有源文件,逐行读取来进行包名的对比,如果有导入包的类我们就可以将绝对路径记录在某个临时文件中,那么接下来就单独对这些文件来进行注解处理。(为了方便起见,这一步后面的代码略过了,有兴趣的同学可以自己实现一下,逻辑并不复杂)
  • 如果获取对应源文件中的注解
    ———上一步的作用是为了排除调一些不需要处理注解的文件,提高效率,如果所有文件都去读取读取注解再来判断,效率会比较低。要拿到对应源文件的注解,我们使用了eclipse-astparser这个库,这个库就是对java文件进行分析的库,可以拿到一个源文件中的所有内容。
  • 按照怎样的模式来生成辅助类
    例如:
    MainActivity.java
public class MainActivity extends AppCompatActivity {

    @BindView(R.id.tv)
    TextView tv;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }
}

要生成的辅助类就是:

package com.example.ty.gplugin.activity;
import  com.example.ty.gplugin.R;

public class MainActivity_ViewBinding {

    public MainActivity_ViewBinding(MainActivity target) {
        target.tv=target.getWindow().getDecorView().findViewById(R.id.tv);

    }
}

那么思路已经整理完毕,接下来就只需要完善各个功能即可。首先我们在buildSrc中的build.gradle中添加依赖

repositories{
    jcenter()
    google()
    mavenCentral()

}

dependencies{
    compile  'com.android.tools.build:gradle:3.1.2'
    compile  'de.defmacro:eclipse-astparser:8.1'

}

接下来我们在AnnotationProcessPlugin添加代码。

package com.ty.annotationProcess

import com.android.build.gradle.api.BaseVariant
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.Task

class AnnotationProcessPlugin implements Plugin<Project> {

    Project project

    @Override
    void apply(Project project) {
        this.project=project
        //获取app插件中的变体,这里需要考虑library,还是app目录
        //如果是library目录,需要获取libraryVariants
        //不是重点,所以略过直接使用application的变体
        def variants = project.android.applicationVariants
        //在脚本分析完成之后执行
        project.afterEvaluate {
            //遍历变体
            variants.all { BaseVariant variant ->
                //获取java源文件目录
                def javaDirector = variant.sourceSets.get(0).getJavaDirectories().getAt(0)
                println "java源文件目录$javaDirector"

                //获取activity文件的绝对路径
                //我直接获取的activity包下的文件
                //这里和我之前说的,需要去遍历所有文件找到需要处理注解的文件,再进行处理
                //这里为了简单起见我就直接获取activity包下的文件了
                def absolutePackageDir="$javaDirector\\${variant.applicationId.replaceAll('\\.','\\\\')}\\activity"

                println '创建任务'
                //创建一个生成任务
                Task compile = project.tasks.create("generate${variant.name}",GeneratorTask)
                //为任务创建一个输入属性
                compile.inputs.property'package',variant.applicationId
                //为任务创建一个输出文件 这里是app\build\generated\atp\debug\com\example\ty\gplugin\activity
                compile.outputs.file("${project.buildDir}/generated/atp/$variant.name/${variant.applicationId.replaceAll('\\.','\\\\')}\\activity")
                println absolutePackageDir
                //遍历目录activity的所有文件,将文件加入任务的输入文件
                def activityPackage=new File(absolutePackageDir)
                activityPackage.eachFile {
                    compile.inputs.file(it)
                }

            }
            registerTask()

        }
    }

    def registerTask(){
        //注册源文件生成任务,将任务的文件以及对应的任务进行注册
        //这样打包的时候就会先将对应任务执行完成,并将生成的输出文件导包进入最后的apk
        project.android.applicationVariants.each{
            BaseVariant variant->
                def name="generate${variant.name}"
                variant.registerJavaGeneratingTask(project.tasks.getByName(name),
                        project.tasks.getByName(name).outputs.files.files)
        }
    }
}

代码中注释已经说的比较清楚了,总的来说就是生成一个任务,将需要处理的java文件全部加入任务的输入中,最后将任务以及任务的输出文件注册到java的源文件打包路径中。

3.输出生成的辅助类
package com.ty.annotationProcess

import org.eclipse.jdt.core.dom.*
import org.gradle.api.DefaultTask
import org.gradle.api.tasks.TaskAction

/*
 * Created by TY on 2018/5/4.
 */

class GeneratorTask extends DefaultTask {

    GeneratorTask() {
        group = 'assisclass'
        outputs.upToDateWhen { false }
    }

    @TaskAction
    def run() {
        //创建输出目录,如果存在则删除重新创建
        def outDir = outputs.files.singleFile
        outDir.deleteDir()
        outDir.mkdirs()
        //获取传入的属性包名
        def packageId = inputs.getProperties().'package'
        //遍历对应的输入文件
        inputs.files.each {
            println 'activity目录中的文件文件' + it.name
            if (it.file && it.name.endsWith('java')) {
                println '目标文件' + it.name
                //AST分析
                def parser = ASTParser.newParser(AST.JLS3) as ASTParser //initialize
                parser.setKind(ASTParser.K_COMPILATION_UNIT)     //to parse compilation unit
                parser.setSource(it.text.toCharArray())
                //content is a string which stores the java source
                parser.setResolveBindings(true)
                CompilationUnit result = (CompilationUnit) parser.createAST(null)
                //获取类名
                List types = result.types()
                TypeDeclaration typeDec = (TypeDeclaration) types.get(0);
                //获取成员变量
                FieldDeclaration[] fieldDec = typeDec.getFields();
                def fileCollection = [:]
                for (FieldDeclaration field : fieldDec) {
                    //这里只能拿到注解名字,不能拿到包名,所以说前面需要去判断是否有导入过注解的包
                    //这就和注解处理器的有差别了
                    System.out.println("Field fragment:" + field.fragments())
                    System.out.println("Field type:" + field.getType())
                    println field.modifiers().get(0).typeName
                    def value = field.modifiers().get(0).value
                    //找到有BindView注解的属性
                    if (field.modifiers().get(0).typeName.toString() == 'BindView') {
                        fileCollection.put(field.fragments().get(0).name,value)
                    }

                }

                //拿到注解的成员变量之后,我们就直接生成代码
                File assistFile = new File(outDir, "${typeDec.name}_ViewBinding.java")
                //拼接对应的内容
                def content = """
package ${packageId}.activity;
import  ${packageId}.R;

public class ${typeDec.name}_ViewBinding {

    public ${typeDec.name}_ViewBinding(${typeDec.name} target) {
        ${-> getBody(fileCollection)}

    }
}
"""
                assistFile.write(content)
            }
        }

    }

    def getBody(fileCollection) {
        def sb = new StringBuilder()
        fileCollection.each {
            key, value ->
                sb.append("target.${key}=target.getWindow().getDecorView().findViewById(${value.qualifier.qualifier}.${value.qualifier.name}.${value.name});")

        }
        sb.toString()
    }


}

这里的代码也比较简单,就是通过遍历源文件的成员变量,找到被BindView注解的所有成员变量。然后生成辅助类。接下来我们就在app模块下准备最后的代码。

代码测试

我们在app模块中建立一个activity包,一个annotation包。在activity中加入两个类:
MainActivity.java


public class MainActivity extends AppCompatActivity {

    @BindView(R.id.tv)
    TextView tv;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        BindUtils.bind(this);
        tv.setText("第一个activity");
        tv.setOnClickListener(v->{
            Intent i =new Intent(MainActivity.this,SecondActivity.class);
            startActivity (i);
        });
    }
}

SecondActivity.java

public class SecondActivity extends AppCompatActivity {

    @BindView(R.id.tv)
    TextView tv;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        BindUtils.bind(this);
        tv.setText("第二个activity");
    }
}

在annotation包中加入注解
BindView.java


@Retention(CLASS)
@Target(FIELD)
public @interface BindView {
     int value();
}

最后加入辅助类
BindUtils.java

public class BindUtils {
    public static void bind(Activity activity) {
        try {
            String className=activity.getClass().getName();
            Class c = Class.forName(className + "_ViewBinding");
            Constructor constructor = c.getConstructor(activity.getClass());
            constructor.newInstance(activity);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
    }
}

加入布局文件,activity_main.xml


<android.support.constraint.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".activity.MainActivity">

    <TextView
        android:id="@+id/tv"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World!"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent"/>

android.support.constraint.ConstraintLayout>

最后在build.gradle末尾添加插件

apply plugin:com.ty.annotationProcess.AnnotationProcessPlugin

然后直接编译运行
Android自定义Gradle插件来处理注解_第3张图片
tv成功设置文字,说明注解绑定成功了。

总结

对于注解处理,套路基本上都差不多,除了使用java的注解处理器来进行处理,使用Gradle插件同样也可以分析源文件来进行注解处理。这篇文章的代码虽然写了比较多的注释,但是要看明白除了要有gradle插件的基本知识以外,还需要有对注解处理有一定认识,如果有什么问题或者错误,可以在评论区进行评论。

你可能感兴趣的:(gradle,Gradle学习总结)