熟悉Java注解处理器的朋友,肯定会了解如butterknife,dagger之类的框架,这类框架都是在编译阶段处理注解来生成辅助类,从而不需要再写很多机械的代码。这里我们换一种思路,不使用Java的注解处理器,直接使用Gradle来处理注解并生成类。注意:这篇文章仅提供一种注解处理的思路,不会考虑太多程序的健壮性。话不多说,直接开始吧。
为了方便起见,我就直接使用buildSrc来进行插件的构建了。这个目录会直接引入java以及groovy的api,如果单独简历module,我们就需要把插件提交到本地仓库,或者JCenter之类的才能使用了。
我们随便新建一个Android项目,并在目录中添加一个buildSrc目录以及groovy的目录。最终项目结构:
我们在buildSrc目录下添加一个插件类。准备好对应的包名,注意文件的后缀需要是groovy。
接下来我们来添加插件的代码。在这之前,首先整理一下思路。
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的源文件打包路径中。
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
对于注解处理,套路基本上都差不多,除了使用java的注解处理器来进行处理,使用Gradle插件同样也可以分析源文件来进行注解处理。这篇文章的代码虽然写了比较多的注释,但是要看明白除了要有gradle插件的基本知识以外,还需要有对注解处理有一定认识,如果有什么问题或者错误,可以在评论区进行评论。