接着看看Java提供的内置注解,主要有5个,如下:
自定义注解的时候用到的,也就是自定义注解的注解;元注解的作用就是负责注解其他注解。Java8定义了5个标准的meta-annotation类型,它们被用来提供对其它 annotation类型作说明。
@Target
用于描述注解的使用范围,取值(ElementType)有:
/** 两种方式均可,如果只需要为value成员变量指定值,可以直接在后面括号里指定value成员变量的值,无需name=value的形式
*/
@Target(value = ElementType.FIELD)
@Target(ElementType.FIELD)
@Retention
表示需要在什么级别保存该注释信息,用于描述注解的生命周期,取值(RetentionPoicy)有:
@Documented
将注解包含在javadoc文档中
@Inherited
允许子类继承父类中的注解
public @interface 注解名{
定义体
}
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface TestAnnotation {
public int id() default -1;
public String name() default "";
}
java.lang.reflect.AnnotatedElement
Java使用Annotation接口来代表程序元素前面的注解,该接口是所有Annotation类型的父接口。除此之外,Java在java.lang.reflect 包下新增了AnnotatedElement接口,该接口代表程序中可以接受注解的程序元素,该接口主要有如下几个实现类:
java.lang.reflect 包下主要包含一些实现反射功能的工具类,实际上,java.lang.reflect 包所有提供的反射API扩充了读取运行时Annotation信息的能力。当一个Annotation类型被定义为运行时的Annotation后,该注解才能是运行时可见,当class文件被装载时被保存在class文件中的Annotation才会被虚拟机读取。
AnnotatedElement 接口是所有程序元素(Class、Method和Constructor)的父接口,所以程序通过反射获取了某个类的AnnotatedElement对象之后,程序就可以调用该对象的四个方法来访问Annotation信息:
下面程序通过使用Annotation来简化事件点击编程,传统点击事件
/***********注解声明***************/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyInject {
int value();
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MyClick {
int[] value();
}
/***********注解使用***************/
public class RunActivity extends AppCompatActivity {
@MyInject(R.id.button1)
public Button button1;
@MyInject(R.id.button2)
public Button button2;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_run);
MyViewUtils.inject(this);//注解处理器
}
@MyClick({R.id.button1, R.id.button2})
public void submit(View view){
Toast.makeText(this, ((Button)view).getText(), Toast.LENGTH_SHORT).show();
}
}
/***********注解处理器***************/
public class MyViewUtils {
public static void inject(final Activity activity){
//反射属性
Field[] declaredFields = activity.getClass().getDeclaredFields();
for (int i = 0; i < declaredFields.length; i++) {
Field field = declaredFields[i];
field.setAccessible(true);
MyInject annotation = field.getAnnotation(MyInject.class);
if (annotation!=null) {
int id = annotation.value();
View view = activity.findViewById(id);
try {
field.set(activity, view);
} catch (Exception e) {
e.printStackTrace();
}
}
}
//反射方法
Method[] declaredMethods = activity.getClass().getDeclaredMethods();
for (int i = 0; i < declaredMethods.length; i++) {
final Method method = declaredMethods[i];
method.setAccessible(true);
MyClick annotation = method.getAnnotation(MyClick.class);
if (annotation!=null) {
int[] value = annotation.value();
for (int j : value) {
int id = j;
final View btn = activity.findViewById(id);
btn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
try {
method.invoke(activity, btn);
} catch (Exception e) {
e.printStackTrace();
}
}
});
}
}
}
}
}
Java 8以前,同一个程序元素前最多只能使用一个相同类型的Annotation,如果需要使用多个相同类型Annotation,则必须使用Annotation容器。
@Results({@Result(name="success", location="succ.jsp"),@Result(name="fail", location="fail.jsp")})
public Action fooAction(){
...
}
注:实质是@Results只包含一个名为value,值为Result[]的成员变量。
java8新增@Repeatable元注解,表示被修饰的注解可以用在同一个程序元素加上多个相同的注解(包含不同的属性值),其实Repeatable只是语法糖而已。
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Repeatable(value = Result.class) //可以多次添加
public @interface Result {
String name();
String location();
}
@Result(name="success", location="succ.jsp")
@Result(name="fail", location="fail.jsp")
public Action fooAction(){
...
}
Java8 为ElementType枚举新增了 ElementType.TYPE_PARAMETER以及ElementType.USE,@Target(ElementType.TYPE_USE)被称为类型注解,可用于任何用到类型的地方。
/**初始化对象时*/
String myString = new @NotNull String();
//对象类型转化时
myString = (@NonNull String) str;
//使用 implements 表达式时
class MyList implements @ReadOnly List<@ReadOnly T>{
...
}
//使用 throws 表达式时
public void validateValues() throws @Critical ValidationFailedException{
...
}
上面讲到自定义注解,我们的实现方式是运行时注解,都是在运行时通过反射的方式来实现的,这样必然会带来一个问题,那就是性能的损耗。那么有没有更好的实现方式呢?既可以实现注入,还能保证性能无损耗?那就是编译时注解。
APT(Annotation Processing Tool)是一种注解处理工具,它可以根据源文件中的Annotation动态生成额外的源文件和其他文件,文件具体内容由Annotation 处理器编写者决定,APT还会编译动态生成的源文件和以前的源文件,将它们一起生成class文件。在运行时期,这类注解是没有的,会依靠动态生成的类做一些操作,因为没有反射,效率和直接调用方法没什么区别。
还记得我们之前说Android 6.0 运行时权限处理时使用的PermissionsDispatcher库吗,使用的正是编译时注解。其他编译时注解库还有butterknife、ParcelableGenerator等。
这里我们还是实现一个View注入的框架。
(1)准备
编写此类框架,一般需要建立多个module,例如本文即将实现的例子:
对于module间的依赖,因为编写注解处理器需要依赖相关注解,所以:
compiler依赖annotation
我们在使用的过程中,会用到注解以及相关API,所以
app依赖api和compiler(依赖compiler不是个好做法,后面介绍更适当的方法)
api依赖annotation
(2)注解模块
注解模块,主要用于存放一些注解类
//编译时注解这里到CLASS即可,运行时注解需要RUNTIME
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface Bind
{
int value(); //这里我们需要在使用时传入一个id
}
(3)注解处理器
1)AbstractProcessor几个方法
这里定义一个注解处理器 MyProcessor,每一个处理器都是继承于AbstractProcessor,并要求必须复写 process() 方法,通常我们使用会去复写以下4个方法:
/**
* 每一个注解处理器类都必须有一个空的构造函数,默认不写就行;
*/
public class MyProcessor extends AbstractProcessor {
/**
* init()方法会被注解处理工具调用,并输入ProcessingEnviroment参数,可以帮助我们去初始化一些辅助类(Elements、Filer、Messager、Types、Filer等):
Filer mFileUtils; 跟文件相关的辅助类,生成JavaSourceCode.
Elements mElementUtils;跟元素相关的辅助类,帮助我们去获取一些元素相关的信息。
Messager mMessager;跟日志相关的辅助类。
Element几个子类:
- VariableElement //一般代表成员变量
- ExecutableElement //一般代表类中的方法
- TypeElement //一般代表代表类
- PackageElement //一般代表Package
*/
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
}
/**
* 这相当于每个处理器的主函数main(),你在这里写你的扫描、评估和处理注解的代码,以及生成Java文件。
* 输入参数RoundEnviroment,可以让你查询出包含特定注解的被注解元素
* @param annotations 请求处理的注解类型
* @param roundEnv 有关当前和以前的信息环境
* @return 如果返回 true,则这些注解已声明并且不要求后续 Processor 处理它们(一个元素多个注解,后面的注解将不可用);
* 如果返回 false,则这些注解未声明并且可能要求后续 Processor 处理它们
*/
@Override
public boolean process(Set extends TypeElement> annotations, RoundEnvironment roundEnv) {
return false;
}
/**
* 这里必须指定,这个注解处理器是注册给哪个注解的。注意,它的返回值是一个字符串的集合,包含本处理器想要处理的注解类型的合法全称
* @return 注解器所支持的注解类型集合,如果没有这样的类型,则返回一个空集合
*/
@Override
public Set getSupportedAnnotationTypes() {
Set annotataions = new LinkedHashSet();
annotataions.add(Bind.class.getCanonicalName());
return annotataions;
}
/**
* 指定使用的Java版本,通常这里返回SourceVersion.latestSupported(),默认返回SourceVersion.RELEASE_7
* @return 使用的Java版本
*/
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
}
上面注释说的挺清楚了,我们需要处理的工作在 process() 方法中进行,等下给出例子。对于 getSupportedAnnotationTypes() 方法标明了这个注解处理器要处理哪些注解,返回的是一个Set 值,说明一个注解处理器可以处理多个注解。除了在这个方法中指定要处理的注解外,还可以通过注解的方式来指定(SourceVersion也一样):
@SupportedSourceVersion(SourceVersion.RELEASE_7)
@SupportedAnnotationTypes("com.example.Bind")
public class MyProcessor extends AbstractProcessor {
// ...
}
因为兼容的原因,特别是针对Android平台,建议使用重载 getSupportedAnnotationTypes() 和 getSupportedSourceVersion()方法代替@SupportedAnnotationTypes 和@SupportedSourceVersion
2)process的实现
process中的实现,相比较会比较复杂一点,一般你可以认为两个大步骤:
什么叫收集信息呢?就是根据你的注解声明,拿到对应的Element,然后获取到我们所需要的信息,这个信息肯定是为了后面生成JavaFileObject所准备的。
例如本例,我们会针对每一个类生成一个代理类,例如CompileActivity我们会生成一个CompileActivity$$ViewInjector。那么如果多个类中声明了注解,就生成多个代理类,这里需要:
@Override
public boolean process(Set extends TypeElement> annotations, RoundEnvironment roundEnv) {
messager.printMessage(Diagnostic.Kind.NOTE , "process...");
//process可能会多次调用,避免生成重复的代理类,避免生成类的类名已存在异常
mProxyMap.clear();
/**收集信息*/
Set extends Element> elesWithBind = roundEnv.getElementsAnnotatedWith(Bind.class);
for (Element element : elesWithBind) {
/**检查element类型*/
checkAnnotationValid(element, Bind.class);
VariableElement variableElement = (VariableElement) element;
//class type
TypeElement classElement = (TypeElement) variableElement.getEnclosingElement();
//类的全路径
String fqClassName = classElement.getQualifiedName().toString();
ProxyInfo proxyInfo = mProxyMap.get(fqClassName);
if (proxyInfo == null) {
proxyInfo = new ProxyInfo(elementUtils, classElement);
mProxyMap.put(fqClassName, proxyInfo);
}
Bind bindAnnotation = variableElement.getAnnotation(Bind.class);
int id = bindAnnotation.value();
proxyInfo.injectVariables.put(id , variableElement);
}
/**生成代理类*/
for (String key : mProxyMap.keySet()) {
ProxyInfo proxyInfo = mProxyMap.get(key);
try {
JavaFileObject jfo = processingEnv.getFiler().createSourceFile(
proxyInfo.getProxyClassFullName(),
proxyInfo.getTypeElement());
Writer writer = jfo.openWriter();
//生成Java代码
writer.write(proxyInfo.generateJavaCode());
writer.flush();
writer.close();
} catch (IOException e) {
error(proxyInfo.getTypeElement(), "Unable to write injector for type %s: %s",
proxyInfo.getTypeElement(), e.getMessage());
}
}
return true;
}
生成Java代码的方法都在ProxyInfo里面
public String generateJavaCode() {
StringBuilder builder = new StringBuilder();
builder.append("// Generated code. Do not modify!\n");
builder.append("package ").append(packageName).append(";\n\n");
builder.append("import com.hx.*;\n");
builder.append("import com.hx.api.ViewInject;\n");
builder.append('\n');
builder.append("public class ").append(proxyClassName).append(" implements " + ProxyInfo.PROXY + "<" + typeElement.getQualifiedName() + ">");
builder.append(" {\n");
generateMethods(builder);
builder.append('\n');
builder.append("}\n");
return builder.toString();
}
private void generateMethods(StringBuilder builder) {
builder.append("@Override\n ");
builder.append("public void inject(" + typeElement.getQualifiedName() + " host, Object source ) {\n");
for (int id : injectVariables.keySet()) {
VariableElement element = injectVariables.get(id);
String name = element.getSimpleName().toString();
String type = element.asType().toString();
builder.append(" if(source instanceof android.app.Activity){\n");
builder.append("host." + name).append(" = ");
builder.append("(" + type + ")(((android.app.Activity)source).findViewById( " + id + "));\n");
builder.append("\n}else{\n");
builder.append("host." + name).append(" = ");
builder.append("(" + type + ")(((android.view.View)source).findViewById( " + id + "));\n");
builder.append("\n};");
}
builder.append(" }\n");
}
这里主要就是靠收集到的信息,拼接完成的代理类对象了,看起来会比较头疼,不过我给出一个生成后的代码,对比着看会很多。
// Generated code. Do not modify!
package com.hx.annotation;
import com.hx.*;
import com.hx.api.ViewInject;
public class CompileActivity$$ViewInject implements ViewInject<com.hx.annotation.CompileActivity> {
@Override
public void inject(com.hx.annotation.CompileActivity host, Object source ) {
if(source instanceof android.app.Activity){
host.image = (android.widget.ImageView)(((android.app.Activity)source).findViewById(2131427369));
}else{
host.image = (android.widget.ImageView)(((android.view.View)source).findViewById(2131427369));
};
if(source instanceof android.app.Activity){
host.text = (android.widget.TextView)(((android.app.Activity)source).findViewById(2131427415));
}else{
host.text = (android.widget.TextView) (((android.view.View)source).findViewById(2131427415));
};
}
}
这里注意下,生成的代码实现了一个接口ViewInjector,该接口是为了统一所有的代理类对象的类型,到时候我们需要强转代理类对象为该接口类型,调用其方法;接口是泛型,主要就是传入实际类对象,例如CompileActivity,因为我们在生成代理类中的代码,实际上就是实际类.成员变量的方式进行访问,所以,使用编译时注解的成员变量一般都不允许private修饰符修饰(有的允许,但是需要提供getter,setter访问方法)。
上面是完全采用拼接的方式编写Java代码,是不是看的头晕了,你也可以使用一些开源库,来通过Java api的方式来生成代码,例如:
javapoet
A java API for generating .java source files.有兴趣自己研究去。
3)运行注解处理器
上面第二步注解处理器写好了,这时如果你直接编译或者运行工程的话,是看不到任何输出的,这里还要做的一步操作是指定注解处理器的所在(必须要用一个服务文件来注册它),需要做如下操作:
1、在 compiler 库的 main 目录下新建 resources 资源文件夹;
2、在 resources文件夹下建立 META-INF/services 目录文件夹;
3、在 META-INF/services 目录文件夹下创建 javax.annotation.processing.Processor 文件;
4、在 javax.annotation.processing.Processor 文件写入注解处理器的全称,包括包路径;
javax.annotation.processing.Processor文件:
com.example.ViewInjectProcessor
(4)API模块
有了代理类之后,我们一般还会提供API供用户去访问,例如本例的访问入口是:
//Activity中
ViewInjector.injectView(Activity);
//Fragment中,获取ViewHolder中
ViewInjector.injectView(this, view);
模仿了butterknife,第一个参数为宿主对象,第二个参数为实际调用findViewById的对象;当然在Activity中,两个参数就一样了。
API一般如何编写呢?
其实很简单,只要你了解了其原理,这个API就干两件事:
这两件事应该不复杂,第一件事是拼接代理类名,然后反射生成对象,第二件事强转调用。
public class ViewInjector {
private static final String SUFFIX = "$$ViewInject";
public static void injectView(Activity activity)
{
ViewInject proxyActivity = findProxyActivity(activity);
proxyActivity.inject(activity, activity);
}
public static void injectView(Object object, View view)
{
ViewInject proxyActivity = findProxyActivity(object);
proxyActivity.inject(object, view);
}
private static ViewInject findProxyActivity(Object activity)
{
try
{
Class clazz = activity.getClass();
Class injectorClazz = Class.forName(clazz.getName() + SUFFIX);
return (ViewInject) injectorClazz.newInstance();
} catch (ClassNotFoundException e)
{
e.printStackTrace();
} catch (InstantiationException e)
{
e.printStackTrace();
} catch (IllegalAccessException e)
{
e.printStackTrace();
}
throw new RuntimeException(String.format("can not find %s , something when compiler.", activity.getClass().getSimpleName() + SUFFIX));
}
}
代码很简单,拼接代理类的全路径,然后通过newInstance生成实例,然后强转为ViewInject,调用代理类的inject方法。
这里一般情况会对生成的代理类做一下缓存处理,比如使用Map存储下,不用再生成,这里我们就不去做了。
以上我们就完成了一个编译时注解框架的编写。
(5)编译
到这里我们重新编译下工程就应该有输出了,如果没看到输出则先清理下工程再编译,如下两个操作:
生成的代理文件在哪里呢?其实在app模块的build目录下:\app\build\generated\source\apt\debug\com\hx\annotation
在注解处理器的process方法中我们打了一句log信息
messager.printMessage(Diagnostic.Kind.NOTE , "process...");
那么log信息从哪里看呢?如果你从Android Monitor中看,保证你看不出任何输出,其实需要在Gradle Console中查看信息
这里的输出log可以用来在编译过程中做简单调试用。 更多的调试注解大家可以查看这篇文章如何debug自定义AbstractProcessor, 我这里就不过多赘述了。
(6)高效处理方式
上面的处理方式有如下几个问题:
(1)先看第一个问题:
AutoService注解处理器是Google开发的,用来生成 META-INF/services/javax.annotation.processing.Processor 文件的,你只需要在你定义的注解处理器上添加 @AutoService(Processor.class) 就可以了,简直不能再方便了。
首先compiler模块添加依赖:
compile 'com.google.auto.service:auto-service:1.0-rc2'
添加好以后就可以直接用了,在我们之前定义的注解处理器上使用:
@AutoService(Processor.class)
public class ViewInjectProcessor extends AbstractProcessor {
// ...
}
一句话完全搞定!如果要使用 autoservice的话,需要把配置文件给删掉,不然默认的配置文件会吧autoservice给覆盖掉,生成不出来文件,这两种方式选择一种就可以了。
(2)再看第二个问题:
这就需要使用到Android-apt了,那么什么是android-apt呢?
大体来讲它有两个作用:
这个就可以很好地解决上面我们遇到的问题了,来看下怎么用。
首先在整个工程的 build.gradle 中添加如下两段语句:
buildscript {
repositories {
jcenter()
mavenCentral() // add
}
dependencies {
classpath 'com.android.tools.build:gradle:2.1.2'
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8' // add
}
}
在主项目(app)的 build.gradle 中也添加两段语句:
apply plugin: 'com.android.application'
apply plugin: 'com.neenbedankt.android-apt' // add
...
dependencies {
compile fileTree(include: ['*.jar'], dir: 'libs')
androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
exclude group: 'com.android.support', module: 'support-annotations'
})
compile 'com.android.support:appcompat-v7:25.3.1'
testCompile 'junit:junit:4.12'
compile project(':Api')
// compile project(':complier') 替换为
apt project(':complier')
}
同步一下,这样就OK了,重新运行可以很好地工作了。
看一下生成的apk
使用apt的方式由于注解处理类没有被打包到APK中,APK由2288KB缩小到1450KB,是不是很酷。
Demo下载地址