在上一篇文章解析了java Annotation的概念和基本用法,再简单回顾下注解的作用:
a.生成文档。这是最常见的,也是java 最早提供的注解。常用的有@see @param @return 等
b. 标记,用于告诉编译器一些信息,比如 @Override等,
c. 编译时动态处理,如动态生成代码
d. 运行时动态处理,如得到注解信息
上篇文章演示了处理运行时动态处理注解,这篇文件讲解注解的编译时动态处理机制,还是实现类似ButterKnife的功能,用注解代替findViewById()和setOnClickListener。
注解处理器
要使用编译时处理注解功能,首先的在android studio中添加注解处理器:android-apt
官方地址: https://bitbucket.org/hvisser/android-apt
Android Studio原本是不支持注解处理器的, 但是用这个插件后, 我们就可以使用注解处理器了, 这个插件可以自动的帮你为生成的代码创建目录, 让生成的代码编译到APK里面去, 而且它还可以让最终编译出来的APK里面不包含注解处理器本身的代码, 因为这部分代码只是编译的时候需要用来生成代码, 最终运行的时候是不需要的。
使用这个插件很简单, 首先在你项目顶层的build.gradle文件中添加依赖项, 如下:
java
dependencies {
classpath ‘com.android.tools.build:gradle:2.0.0’
classpath ‘com.neenbedankt.gradle.plugins:android-apt:1.8’
}
然后在app的build.gradle里面添加插件的引用以及需要依赖哪些库, 如下:
apply plugin: ‘com.android.application’
apply plugin: ‘com.neenbedankt.android-apt’
….
….
dependencies {
…
compile project(‘:bindannotation’)
apt project(‘:viewbindcompiler’)
compile project(‘:viewinject’)
}
注意上面同时添加了bindannotation,viewbindcompiler,viewinject 三个自定义模块:
- bindannotation : 这是个Java Library,,主要来定义注解。
- viewbindcompiler: 必须是java Library, 用于处理上面bindannotation定义的注解。
- viewinject:这个是Android Library,被android其他模块调用实现View的Bind。
代码结构如下:
使用注解
下面就来介绍怎么使用注解生成代码
定义bindannotation注解
1.定义ViewBind注解,处理findViewById方法:
@Documented
@Target(ElementType.FIELD) //用于类属性成员上
@Retention(RetentionPolicy.CLASS) //编译时注解
@Inherited
public @interface ViewBind {
int value() default 0;
}
2.定义BindClick注解,处理View onClickListener方法:
@Documented
@Target(ElementType.METHOD)//用于类方法上
@Retention(RetentionPolicy.CLASS)//编译时注解
@Inherited
public @interface BindClick {
int id() default 0;
}
定义注解产生代码调用者
在viewinject模块里先定义一个接口:
public interface ViewBinder<T> {
void viewBind(T target);
}
为什么要先定义这个接口呢,这个接口就是用于规范编译时自动产生的代码,后续我们编译时自动生成的代码都继承这个接口,并在viewBind方法里自动生成findViewById,setOnClickListener等代码,这样我们其他地方就可以方便统一调用这些自动生成的代码。
定义一个ViewBind类,调用自动生成的代码:
public class ViewBind {
private static final String BINDING_CLASS_SUFFIX = "$$ViewBinder";//生成类的后缀 以后会用反射去取
public static void init(Activity activity){
String bindClassName = activity.getClass().getName() + BINDING_CLASS_SUFFIX;
try {
Class bindClass = Class.forName(bindClassName);
ViewBinder viewBind = (ViewBinder) bindClass.newInstance();
viewBind.viewBind(activity);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
有了ViewBind类后,我们就只需在activity中,调用ViewBind.init(activity)后,就可以完成view的bind工作了。注意,后面编译时生成的类的类名,都是按使用注解的类的类名+$$ViewBinder。
比如MainActivity.class,会生成MainActivity$$ViewBinder.class 文件。
定义注解的处理器
注解处理器最核心的就是要有一个Processor, 它继承自AbstractProcessor,编译器在编译时自动查找所有继承自 AbstractProcessor 的类,然后调用他们的 process 方法去处理。
先在viewbindcompiler模块里添加所需的依赖:
apply plugin: 'java'
sourceCompatibility = 1.7
targetCompatibility = 1.7
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
compile 'com.google.auto.service:auto-service:1.0-rc2'
compile 'com.squareup:javapoet:1.7.0'
compile project(':bindannotation')
}
auto-service:使用它就不需要把processor在META-INF配置了,编译时配置的Processor能够自动被发现并处理。
javapoet:辅助生成代码,能够更简单的生成.java源文件
定义一个ViewBindProcessor类:
@AutoService(Processor.class)
public class ViewBindProcessor extends AbstractProcessor {
private static final String BINDING_CLASS_SUFFIX = "$$ViewBinder";//生成类的后缀 以后会用反射去取
private static final ClassName VIEW_BINDER = ClassName.get("com.nick.study.viewinject", "ViewBinder");
private Elements elementUtils;
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
elementUtils = processingEnv.getElementUtils();
}
@Override
public Set<String> getSupportedAnnotationTypes() {
Set<String> types = new LinkedHashSet<>();
types.add(ViewBind.class.getCanonicalName());
types.add(BindClick.class.getCanonicalName());
return types;
}
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
Map<String, ViewBindInfo> targetClassMap = new LinkedHashMap<>();//定义一个按类名为key值,保存其类下所有使用了注解的元素
Set<? extends Element> set = roundEnv.getElementsAnnotatedWith(ViewBind.class); //取出所有使用ViewBind注解的元素
// Element表示一个程序元素,比如包、类或者方法。
for (Element element : set) {
if (element.getKind() != ElementKind.FIELD) { //如果此元素的不是类成员,则抛出异常
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "only support class field");
break;
}
TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();
String packageName = getPackageName(enclosingElement); //得到此元素所在的包名
String className = getClassName(enclosingElement,packageName); //得到此元素所在类的类名
String classFullPath = packageName+"."+className; //完整类名
ViewBindInfo viewBindInfo = targetClassMap.get(classFullPath);
if(viewBindInfo == null){
viewBindInfo = new ViewBindInfo();
targetClassMap.put(classFullPath,viewBindInfo); //保存这个类的注解信息
}
viewBindInfo.packageName = packageName;
viewBindInfo.className = className;
viewBindInfo.viewBindElementList.add(element); //所有的viewBind元素保存在一个链表里
viewBindInfo.typeClassName = ClassName.bestGuess(getClassName(enclosingElement, packageName));//此ViewBind注解所在的类名类
}
Set<? extends Element> clickSet = roundEnv.getElementsAnnotatedWith(BindClick.class); //处理BindClick注解如上面一致
for (Element element : clickSet) {
if (element.getKind() != ElementKind.METHOD) {
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "only support class method");
break;
}
TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();
String packageName = getPackageName(enclosingElement);
String className = getClassName(enclosingElement,packageName);
String classFullPath = packageName+"."+className;
processingEnv.getMessager().printMessage(Diagnostic.Kind.WARNING,"process: "+classFullPath);
ViewBindInfo viewBindInfo = targetClassMap.get(classFullPath);
if(viewBindInfo == null){
viewBindInfo = new ViewBindInfo();
targetClassMap.put(classFullPath,viewBindInfo);
}
viewBindInfo.packageName = packageName;
viewBindInfo.className = className;
viewBindInfo.viewClickElementList.add(element);
viewBindInfo.typeClassName = ClassName.bestGuess(getClassName(enclosingElement, packageName));
}
buildViewBindClass(targetClassMap); //生成相应的java文件
return false;
}
private void buildViewBindClass( Map<String, ViewBindInfo> targetClassMap ){
if(targetClassMap.size() == 0){
return;
}
for (Map.Entry<String, ViewBindInfo> item : targetClassMap.entrySet()) {
String newClassName = item.getValue().className+BINDING_CLASS_SUFFIX; //java文件名
String packageName = item.getValue().packageName;
ClassName typeClassName = item.getValue().typeClassName;
String methodName = "viewBind"; //方法名,跟自定义的ViewBinder接口中一致
MethodSpec.Builder viewBindMethodBuilder = MethodSpec.methodBuilder(methodName)
.addModifiers(Modifier.PUBLIC)
.returns(TypeName.VOID) //返回值void
.addAnnotation(Override.class)
.addParameter(typeClassName, "target", Modifier.FINAL); //添加一个参数,类型为typeClassName,形参名为target
for(Element element : item.getValue().viewBindElementList){
int id = element.getAnnotation(ViewBind.class).value();
ClassName viewClass = ClassName.bestGuess(element.asType().toString());
//生成findViewById方法
viewBindMethodBuilder.addStatement("target.$L=($T)target.findViewById($L)",element.getSimpleName().toString(),viewClass, id);
}
if(item.getValue().viewClickElementList.size() > 0){
viewBindMethodBuilder.addStatement("$T listener", ClassTypeUtils.ANDROID_ON_CLICK_LISTENER);
}
for(Element element : item.getValue().viewClickElementList){
int id = element.getAnnotation(BindClick.class).id();
// declare OnClickListener anonymous class
TypeSpec listener = TypeSpec.anonymousClassBuilder("")
.addSuperinterface(ClassTypeUtils.ANDROID_ON_CLICK_LISTENER)
.addMethod(MethodSpec.methodBuilder("onClick")
.addAnnotation(Override.class)
.addModifiers(Modifier.PUBLIC)
.returns(TypeName.VOID)
.addParameter(ClassTypeUtils.ANDROID_VIEW, "view")
.addStatement("target.$N()",((ExecutableElement)element).getSimpleName())
.build())
.build();
viewBindMethodBuilder.addStatement("listener = $L ", listener);
// set listeners
viewBindMethodBuilder.addStatement("target.findViewById($L).setOnClickListener(listener)", id);
}
MethodSpec viewBindMethod = viewBindMethodBuilder.build();
TypeSpec viewBind = TypeSpec.classBuilder(newClassName) //生成viewBind方法
.addModifiers(Modifier.PUBLIC, Modifier.FINAL) //方法的修饰限定词
.addTypeVariable(TypeVariableName.get("T", typeClassName))
.addSuperinterface(ParameterizedTypeName.get(VIEW_BINDER, typeClassName))
.addMethod(viewBindMethod) //增加方法里面的语句
.build();
//生成java文件
JavaFile javaFile = JavaFile.builder(packageName, viewBind).build();
try {
javaFile.writeTo(processingEnv.getFiler());
} catch (IOException e) {
e.printStackTrace();
}
}
}
private String getPackageName(TypeElement type) {
return elementUtils.getPackageOf(type).getQualifiedName().toString();
}
private static String getClassName(TypeElement type, String packageName) {
int packageLen = packageName.length() + 1;
return type.getQualifiedName().toString().substring(packageLen).replace('.', '$');
}
//代表某个使用了ViewBind或BindClick注解类中的element注解信息
private class ViewBindInfo{
String packageName;
String className;
ClassName typeClassName; //JavaPoet类,代表某个类名
List<Element> viewBindElementList = new ArrayList<>(); //当前类下的所有ViewBind注解元素
List<Element> viewClickElementList = new ArrayList<>();//当前类下的所有ViewClick注解元素
}
}
使用注解
下面我们就可以开始在activity中使用了:
public class MainActivity extends AppCompatActivity {
@ViewBind(R.id.user_tv)
TextView mTextView;
@ViewBind(R.id.user_tv2)
TextView mTextView2;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ViewBind.init(this);
mTextView.setText("hahaha!!!! annotation success!!!!");
mTextView2.setText("hahaha!!!! annotation success!!!!");
}
@BindClick(id = R.id.user_tv)
public void onClick(){
Toast.makeText(this,"hahaha!!!! annotation click success!!!!",Toast.LENGTH_SHORT).show();
}
}
此时编译下项目,就会发现在app的build\generated\source\apt\目录下生成一个MainActivity$$ViewBinder.java文件,这就是apt编译时自动帮我们生成的,其代码如下:
public final class MainActivity$$ViewBinder<T extends MainActivity> implements ViewBinder<MainActivity> {
@Override
public void viewBind(final MainActivity target) {
target.mTextView=(TextView)target.findViewById(2131492944);
target.mTextView2=(TextView)target.findViewById(2131492945);
View.OnClickListener listener;
listener = new View.OnClickListener() {
@Override
public void onClick(View view) {
target.onClick();
}
} ;
target.findViewById(2131492944).setOnClickListener(listener);
}
}
这就跟我们手动代码一样,运行下,看看效果:
JavaPoet
上面用到的JavaPoet 是一个用来生成 Java源文件的Java API。
javapoet 常用的API
大多数JavaPoet的API使用的是简单的不可变的Java对象。通过建造者模式,链式方法,可变参数使得API比较友好。JavaPoet提供了(TypeSpec)用于创建类或者接口,(FieldSpec)用来创建字段,(MethodSpec)用来创建方法和构造函数,(ParameterSpec)用来创建参数,(AnnotationSpec)用于创建注解。
还可以通过字符串来构建代码块:
MethodSpec main = MethodSpec.methodBuilder("main")
.addCode(""
+ "int total = 0;\n"
+ "for (int i = 0; i < 10; i++) {\n"
+ " total += i;\n"
+ "}\n")
.build();
生成的代码如下:
void main() {
int total = 0;
for (int i = 0; i < 10; i++) {
total += i;
}
}
addStatement(): 方法会给代码语句加上分号和换行
beginControlFlow() + endControlFlow() 需要一起使用,会提供换行符和缩进。
addCode() 以字符串的形式添加代码内
constructorBuilder() 生成构造器函数
addAnnotation 添加注解
addSuperinterface 给类添加实现的接口
superclass 给类添加继承的父类
ClassName.bestGuess(“类全名称”) 返回ClassName对象,这里的类全名称表示的类必须要存在,会自动导入相应的包
ClassName.get(“包名”,”类名”) 返回ClassName对象,不检查该类是否存在
TypeSpec.interfaceBuilder(“HelloWorld”)生成一个HelloWorld接口addTypeVariable(TypeVariableName.get(“T”, typeClassName))
会给生成的类加上泛型
javaPoet占位符
$L:代表的是字面量
$S:Strings
$N:Names(我们自己生成的方法名或者变量名等等)
$T: Types 这里的$T,在生成的源代码里面,也会自动导入你的类。
完
更多精彩Android技术可以关注我们的微信公众号,扫一扫下方的二维码或搜索关注公共号: Android老鸟