开始是接触注解,然后就想着注解怎么用,我看到的很多都是通过在运行期的注解反射获取值,然后使用,这样挺方便的
下面这个demo出自于Android,几分钟教你怎么应用自定义注解
注解:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface LayoutInject {
int value() default -1;
}
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD})
public @interface ViewInject {
int value() default -1;
}
两个注解,一个用于setContentView,一个用于findViewById
然后解析:
private int mLayoutId = -1;
/**
* 注解布局 LayoutId
*/
private void displayInjectLayout() {
Class<?> clazz = this.getClass();
if (clazz.getAnnotations() != null) {
if (clazz.isAnnotationPresent(LayoutInject.class)) {
LayoutInject inject = clazz.getAnnotation(LayoutInject.class);
mLayoutId = inject.value();
setContentView(mLayoutId);
}
}
}
这个很好理解:首先获取到当前类,判断当前类上是否有使用注解LayoutInject,如果有,则拿到这个注解上的值,然后调用setContentView这个方法,(这里为什么会有这个方法,因为这两个解析方法,我定义在BaseActivity中的,继承了Activity)
/**
* 解析注解view id
*/
private void displayInjectView() {
if (mLayoutId <= 0) return;
Class<?> clazz = this.getClass();
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
try {
if (field.getAnnotations() != null) {
if (field.isAnnotationPresent(ViewInject.class)) {
field.setAccessible(true);
ViewInject inject = field.getAnnotation(ViewInject.class);
field.set(this, this.findViewById(inject.value()));
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
这个也好理解:判断布局是否存在,如果不在,也就没必要进行下去了;同样获取到当前类,通过反射拿到它的所有Field,然后循环查找判断这个Field上是否有ViewInject这个注解,有的话就取到值,然后调用它的findViewById方法。
使用:
public class BaseActivity extends AppCompatActivity {
private int mLayoutId = -1;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
displayInjectLayout();
displayInjectView();
}
private void displayInjectView() {
......
}
private void displayInjectLayout() {
......
}
}
@LayoutInject(R.layout.activity_main)
public class MainActivity extends BaseActivity {
@ViewInject(R.id.tv_message)
private TextView tv_message;
@ViewInject(R.id.dbText)
private TextView dbText;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
......
}
}
这样用起来也是挺方便的,但是使用反射总为人诟病,虽然实际上用于setContentView和findViewById在反射上并没有降低性能,JAVA反射会降低你的程序性能吗?(原文的网址我打不开,我看的就是这个,能科学上网的自己去xxx)上测试后说只有只能执行100万遍的时候才能看出差别,那我们这个百万分之一的性能能影响多少呢?
现在大部分都使用apt,就跟着学习一下;在摸索了好几天之后,才有点收获,因为现在gradle2.2之后普遍使用annotationProcessor,所以我也就从这里开始,之前的android.apt我就不深入碰了
这个主要分四部分:
apt-annotation 注解
apt-compiler 解析器(主要)
apt-api 调用的封装
apt-service 声明
apt-app 注解应用
主要还是前四个部分,最后一个就是example
现在根据一个demo来说:
一小时搞明白注解处理器(Annotation Processor Tool)
apt-demo
apt-annotation
注解这个没什么好说的,元注解就那么几个,只是这里保留的时期不再是RUNTIME
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface BindView {
int value();
}
apt-compiler
这个就比较重要了,写法比较套路化
一个就四个方法:
public synchronized void init(ProcessingEnvironment processingEnv)
public SourceVersion getSupportedSourceVersion()
public Set<String> getSupportedAnnotationTypes()
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnv)
其中中间两个还可以作为注解申明在类头上
第一个主要是获取辅助工具:比如打印信息,生成文件,元素工具
第二个是声明支持的java版本
第三个是支持解析的注解
第四个就是注解解析并生成java文件
private Elements elementUtils;
private Filer filer;
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
elementUtils = processingEnv.getElementUtils();
filer = processingEnv.getFiler();
}
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
@Override
public Set<String> getSupportedAnnotationTypes() {
Set<String> types = new LinkedHashSet<>();
types.add(ContentView.class.getCanonicalName());
types.add(BindView.class.getCanonicalName());
types.add(OnClick.class.getCanonicalName());
return types;
}
这三个方法写起了基本固定
主要是最后一个:
这里有一个概念很重要Element:
package com.example; // PackageElement
public class MyClass { // TypeElement
private int a; // VariableElement
private Foo other; // VariableElement
public Foo () {} // ExecuteableElement
public void setA ( // ExecuteableElement
int newA // TypeElement
) {
}
}
对照着能看出来,(包pacekage)、(类、枚举、接口type)、(属性variable)、(方法excuteable)
@AutoService(Processor.class)
public class BindViewProcessor extends AbstractProcessor {
private Elements elementUtils;
private Filer filer;
private Map<String, AnnotatedClass> annotatedClassMap = new HashMap<>();
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
elementUtils = processingEnv.getElementUtils();
filer = processingEnv.getFiler();
}
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
@Override
public Set<String> getSupportedAnnotationTypes() {
Set<String> types = new LinkedHashSet<>();
types.add(ContentView.class.getCanonicalName());
types.add(BindView.class.getCanonicalName());
types.add(OnClick.class.getCanonicalName());
return types;
}
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnv) {
annotatedClassMap.clear();
try {
processBindView(roundEnv);
} catch (IllegalAccessException e) {
e.printStackTrace();
}
try {
for (AnnotatedClass annotatedClass : annotatedClassMap.values()) {
annotatedClass.generateFinder().writeTo(filer);
}
} catch (IOException e) {
e.printStackTrace();
}
return true;
}
private void processBindView(RoundEnvironment roundEnv) throws IllegalAccessException {
for (Element element : roundEnv.getElementsAnnotatedWith(BindView.class)) {
AnnotatedClass annotatedClass = getAnnotatedClass(element);
BindViewField field = new BindViewField(element);
annotatedClass.addField(field);
}
}
private AnnotatedClass getAnnotatedClass(Element element) {
TypeElement encloseElement = (TypeElement) element.getEnclosingElement();
String fullClassName = encloseElement.getQualifiedName().toString();
AnnotatedClass annotatedClass = annotatedClassMap.get(fullClassName);
if (annotatedClass == null) {
annotatedClass = new AnnotatedClass(encloseElement, elementUtils);
annotatedClassMap.put(fullClassName, annotatedClass);
}
return annotatedClass;
}
}
从上往下看:
@AutoService这个注解就是用来顶替apt-service这个模块的
implementation 'com.google.auto.service:auto-service:1.0-rc2'
Elements是元素操作的辅助工具
Filer是生成额Java文件的辅助工具
然后主要就集中在processor中
还可以看到多出来两个方法,又涉及到两个实体:
package com.xiey94.bindview_compiler;
import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.JavaFile;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.ParameterizedTypeName;
import com.squareup.javapoet.TypeName;
import com.squareup.javapoet.TypeSpec;
import com.xiey94.bindview_annotation.ContentView;
import java.util.ArrayList;
import java.util.List;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;
import javax.lang.model.util.Elements;
/**
* Created by xieyang on 2018/5/7.
* Email: xiey94@qq.com
*/
public class AnnotatedClass {
private TypeElement classElement;
private List<BindViewField> fields;
private List<OnClickMethod> methods;
private List<ContentViewClass> contentViewClasses;
private ContentViewClass contentViewClass;
public Elements elementUtils;
public AnnotatedClass(TypeElement classElement, Elements elements) {
this.classElement = classElement;
this.elementUtils = elements;
this.fields = new ArrayList<>();
this.methods = new ArrayList<>();
this.contentViewClasses = new ArrayList<>();
}
public void addField(BindViewField field) {
fields.add(field);
}
public JavaFile generateFinder() {
MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder("inject")
.addModifiers(Modifier.PUBLIC)
.addAnnotation(Override.class)
.addParameter(TypeName.get(classElement.asType()), "host", Modifier.FINAL)
.addParameter(TypeName.OBJECT, "source")
.addParameter(TypeUtil.FINDER, "finder");
for (BindViewField field : fields) {
methodBuilder.addStatement("host.$N=($T)finder.findView(source,$L)", field.getFieldName()
, ClassName.get(field.getFieldType()), field.getId());
}
String packageName = getPackageName(classElement);
String className = getClassName(classElement, packageName);
ClassName bindClassName = ClassName.get(packageName, className);
TypeSpec finderClass = TypeSpec.classBuilder(bindClassName.simpleName() + "$$Injector")
.addModifiers(Modifier.PUBLIC)
.addSuperinterface(ParameterizedTypeName.get(TypeUtil.INJECTOR, TypeName.get(classElement.asType())))
.addMethod(methodBuilder.build())
.build();
return JavaFile.builder(packageName, finderClass).build();
}
public String getPackageName(TypeElement type) {
return elementUtils.getPackageOf(type).getQualifiedName().toString();
}
public String getClassName(TypeElement type, String packageName) {
int packageLen = packageName.length() + 1;
return type.getQualifiedName().toString().substring(packageLen).replace('.', '$');
}
}
package com.xiey94.bindview_compiler;
import com.xiey94.bindview_annotation.BindView;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.Name;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.TypeMirror;
/**
* Created by xieyang on 2018/5/7.
* Email: xiey94@qq.com
*/
public class BindViewField {
private int id;
private VariableElement fieldElement;
public BindViewField(Element element) throws IllegalAccessException {
if (element.getKind() != ElementKind.FIELD) {
throw new IllegalAccessException(String.format("wrong1"));
}
fieldElement = (VariableElement) element;
BindView bindView = fieldElement.getAnnotation(BindView.class);
id = bindView.value();
if (id < 0) {
throw new IllegalAccessException(String.format("wrong2"));
}
}
public int getId() {
return id;
}
public Name getFieldName() {
return fieldElement.getSimpleName();
}
public TypeMirror getFieldType() {
return fieldElement.asType();
}
}
这里需要提前将apt-api放出来,里面的代码思想是:
定义一个接口,让生成的代码实现这个接口,然后解析的时候,将解析的代码放置到这个方法中,然后反射拿到这个类的实例,调用这个接口方法
public interface Injector<T> {
void inject(T host, Object source, IFinder finder);
}
public class ButterKnife {
private ButterKnife() {
}
private static final ActivityFinder ACTIVITY_FINDER = new ActivityFinder();
private static final ViewFinder VIEW_FINDER = new ViewFinder();
private static Map<String, Injector> FINDER_MAP = new HashMap<>();
public static void bind(Activity activity) {
bind(activity, activity, ACTIVITY_FINDER);
}
public static void bind(View view) {
bind(view, view);
}
public static void bind(Object host, View view) {
bind(host, view, VIEW_FINDER);
}
private static void bind(Object host, Object source, IFinder finder) {
String className = host.getClass().getName();
try {
Injector injector = FINDER_MAP.get(className);
if (injector == null) {
Class<?> finderClass = Class.forName(className + "$$Injector");
injector = (Injector) finderClass.newInstance();
FINDER_MAP.put(className, injector);
}
injector.inject(host, source, finder);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
}
}
}
这个接口主要是用来寻找id的
public interface IFinder {
Context getContext(Object source);
View findView(Object source, int id);
}
public class ActivityFinder implements IFinder {
@Override
public Context getContext(Object source) {
return (Activity) source;
}
@Override
public View findView(Object source, int id) {
return ((Activity) source).findViewById(id);
}
}
public class ViewFinder implements IFinder {
@Override
public Context getContext(Object source) {
return ((View) source).getContext();
}
@Override
public View findView(Object source, int id) {
return ((View) source).findViewById(id);
}
}
再回到解析器那块,
BindViewField:这个主要是把带注解的属性封装成一个实体
在构造器方法中,拿到元素,判断元素所属的类型,然后拿到元素上的注解,拿到注解的值,两个get方法,一个是该元素锁代表的属性的名称和id值
AnnotatedClass:将一个类封装成一个实体(类中包含带注解的属性)
在构造器方法中获取需要的工具;一个addField方法,将所属当前类的带注解的属性装进集合中,然后就是生成文件generateFinder,这个方法先放下,回去看看BindViewProcessor方法
在process方法中调用了processBindView方法,这个方法就是针对整个应用取出来所有的带BindView注解的Element(也就是Field属性),然后初始化AnnotateClass,调用了getAnnotatedClass方法,在这个方法里面,首先是获取类名(这个可作为key为唯一标示),然后判断当前集合中是否存在这个实例,避免重复创建,然后初始化BindViewField对象,将这个对象扔到AnnotatedClass中,这样就完成了分拣工作,(将带注解的Element扔到自己对应的类中,再包所有的类放到集合中,最后遍历这个集合生成Java文件),也就到了generateFilder这个方法;
这里用到了一个生成Java文件的库,省的去拼装
implementation 'com.squareup:javapoet:1.9.0'
具体用法去他们的github上看看JavaPoet
开始生成一个inject方法,继承Injector,在该方法中调用IFinder的findView方法,最后生成xx$$Injector类文件
apt-api
看上面
apt-app
调用比较简单,
@ContentView(R.layout.activity_main)
public class MainActivity extends AppCompatActivity {
@BindView(R.id.tv_1)
public TextView tv1;
@BindView(R.id.tv_2)
public TextView tv2;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ButterKnife.bind(this);
tv1.setText("success !!!");
tv2.setText("success two !!!");
}
@OnClick({R.id.tv_1, R.id.tv_2})
public void onClick(View view) {
switch (view.getId()) {
case R.id.tv_1:
Toast.makeText(MainActivity.this, "1", Toast.LENGTH_SHORT).show();
break;
case R.id.tv_2:
Toast.makeText(MainActivity.this, "2", Toast.LENGTH_SHORT).show();
break;
default:
break;
}
}
}
注解生成文件,
public class MainActivity$$Injector implements Injector<MainActivity> {
@Override
public void inject(final MainActivity host, Object source, IFinder finder) {
host.setContentView(2131296283);
host.tv1=(TextView)finder.findView(source,2131165315);
host.tv2=(TextView)finder.findView(source,2131165316);
View.OnClickListener listener;
listener=new View.OnClickListener() {
@Override
public void onClick(View view) {
host.onClick(view);
}
} ;
finder.findView(source,2131165315).setOnClickListener(listener);
finder.findView(source,2131165316).setOnClickListener(listener);
}
}
ButterKnife.bind(this);
通过apt-api调用生成的文件
整个流程大概就是这样,再来梳理一下:
1、定义注解(BindView)
2.1、将带注解的元素封装成实体,主要是用来获取元素名称和注解所带的参数(BindViewField)
2.2、将注解所在的类封装成实体,将注解实体扔进来,获取注解实体的值,然后生成Java文件(AnnotatedClass)
2.3、不同的注解排队一个一个来进行如上操作(process-processBindView)
3、在调用方法中反射获取到生成的Java文件当前类的实例,调用定义好的接口方法(injector.inject)
demo:GitHub
Android,几分钟教你怎么应用自定义注解
JAVA反射会降低你的程序性能吗?
一小时搞明白注解处理器(Annotation Processor Tool)
apt-demo
JavaPoet
JavaPoet - 优雅地生成代码
APT
Android 利用 APT 技术在编译期生成代码
Android Studio中使用apt
利用APT实现Android编译时注解
你必须知道的APT、annotationProcessor、android-apt、Provided、自定义注解
Android之使用apt编写编译时注解
Android APT(编译时生成代码)
Android Apt之Activity Route的示例
FastRoute
安卓AOP三剑客:APT,AspectJ,Javassist
你必须知道的APT、annotationProcessor、android-apt、Provided、自定义注解
GitHub