Android中注解的使用教程
导言
平常开发中经常会使用到ButterKnife、EventBus这些有使用注解的第三方库,直观来看作用就是“明显”,通过一个标注说明当前方法/属性的意义,从而使得代码的可读性变强,是一种不错的开发手段
注解基础
1.定义一个注解
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.METHOD)
public @interface PageBackground {
}
@Retention:
public enum RetentionPolicy {
/**
* Annotations are to be discarded by the compiler.
*/
SOURCE,
/**
* Annotations are to be recorded in the class file by the compiler
* but need not be retained by the VM at run time. This is the default
* behavior.
*/
CLASS,
/**
* Annotations are to be recorded in the class file by the compiler and
* retained by the VM at run time, so they may be read reflectively.
*
* @see java.lang.reflect.AnnotatedElement
*/
RUNTIME
}
SOURCE:当前注解仅仅是声明,只会在源代码中留存,编译的时候将会被删除,这意味这无法在编译期间和运行时通过反射获取到当前注解的一些信息
CLASS:注解会在.class字节码中,但是不需要由虚拟机在运行时保留(注意实测通过华为P9,是可以在运行时反射获取的),这个也是注解的默认行为
RUNTIME:注解会被保留到运行时,那么可以通过反射获取
一般来说,推荐的使用模式为
SOURCE:单纯阅读使用
CLASS:单纯编译时使用
RUNTIME:运行时需要反射使用
@Target:
public enum ElementType {
/** Class, interface (including annotation type), or enum declaration */
TYPE,
/** Field declaration (includes enum constants) */
FIELD,
/** Method declaration */
METHOD,
/** Formal parameter declaration */
PARAMETER,
/** Constructor declaration */
CONSTRUCTOR,
/** Local variable declaration */
LOCAL_VARIABLE,
/** Annotation type declaration */
ANNOTATION_TYPE,
/** Package declaration */
PACKAGE,
/**
* Type parameter declaration
*
* @since 1.8
*/
TYPE_PARAMETER,
/**
* Use of a type
*
* @since 1.8
*/
TYPE_USE
}
有点多,看几个平常可能会使用的
TYPE:作用在类/接口声明上
@RequiresApi
public class AActivity extends Activity{
}
ANNOTATION_TYPE:作用在注解声明上的,比方说TARGET自己就是这种类型的注解
@Target(ElementType.ANNOTATION_TYPE)
public @interface Target {
/**
* Returns an array of the kinds of elements an annotation type
* can be applied to.
* @return an array of the kinds of elements an annotation type
* can be applied to
*/
ElementType[] value();
}
FIELD:作用在属性上面,比方说类中的某个参数
@SerializedName("sss")
private Button loginButton;
METHOD:作用在类中的方法上面
@PageBackground
public Map<String,String> demo(){
return null;
}
编译期处理注解
1.创建一个android的module,用于定义注解等信息
用于定义一些注解类
2.创建一个java的module(需引入上述注解的module)
3.在java的module下定义类继承AbstractProcessor
@SupportedAnnotationTypes("fanjh.mine.buriedpointannotation.PageBackground")
@SupportedSourceVersion(SourceVersion.RELEASE_7)
public class AppBackgroundProcessor extends AbstractProcessor {
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
messager = processingEnv.getMessager();
filer = processingEnv.getFiler();
types = processingEnv.getTypeUtils();
}
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
return false;
}
}
4.在java的module下面新建一个文件,用于注册AbstractProcessor
文件声明(需要保证目录路径和文件名一致)
AbstractProcessor声明
5.在主项目的gradle中引入当前module
依赖导入
到这一步配置就已经完成,接下来要进入到具体的AbstractProcessor的编写
6.用途的思考:
编译的时候如何使用注解?参考一些现成的库
EventBus:在没有改成@Subscribe注解之前,通过的是反射OnEvent起头类似的方法名(不同线程不一样)来处理,要去记各种方法名,而且还要求不能重名,给人的感觉就是太过死板。通过注解之后,可以方便的标记运行线程,方法名可以自定义,而且阅读起来也方便很多,直观很多
ButterKnife:直接通过注解替代大量的findViewByID等手动的重复代码,如果结合插件的话就更加方便了
7.实际操作:
上面其实提到了,通过继承AbstractProcessor可以完成编译期的操作,如果想要替代大量的人工操作,那么首先需要有一个【服务类】,也就是说编译期的操作应该是生成一些新的类
compile 'com.squareup:javapoet:1.10.0'
这里推荐square的javapoet库,通过Builder模式可以快速写出一个类,封装了大量的拼接操作,使用起来非常方便
看一个简单的例子:
/**
* @author fanjh
* @date 2018/2/9 10:23
* @description
* @note SupportedAnnotationTypes指定当前获取到的注解,相当于一个过滤器
**/
@SupportedAnnotationTypes("fanjh.mine.buriedpointannotation.PageBackground")
@SupportedSourceVersion(SourceVersion.RELEASE_7)
public class AppBackgroundProcessor extends AbstractProcessor {
private Messager messager;
private Filer filer;
/**
* 用于初始化一些工具
* 后续可以使用这些工具进行操作
* @param processingEnv
*/
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
//用于打印日志
messager = processingEnv.getMessager();
//用于写出类文件
filer = processingEnv.getFiler();
}
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
//可以通过当前方法获取指定的注解
Set<Element> set = (Set<Element>) roundEnv.getElementsAnnotatedWith(PageBackground.class);
if(null == set){
return false;
}
Map<String,String> caches = new HashMap<>();
//遍历当前代码中所有的指定注解
for (Element element : set) {
//获取当前注解的作用对象
if (element.getKind() == ElementKind.METHOD) {
//这里实际上就是把有当前注解的类名和方法名进行缓存
ExecutableElement executableElement = (ExecutableElement) element;
TypeElement typeElement = (TypeElement) executableElement.getEnclosingElement();
String className = typeElement.getQualifiedName().toString();
log(className);
if(executableElement.getParameters().size() > 0){
throw new IllegalArgumentException("当前注解标记方法不能有参数!");
}
TypeMirror typeMirror = executableElement.getReturnType();
TypeKind typeKind = typeMirror.getKind();
if(TypeKind.DECLARED != typeKind || !"java.util.Map<java.lang.String,java.lang.String>".equals(typeMirror.toString())){
throw new IllegalArgumentException("当前注解标记方法返回值类型有误!");
}
String methodName = executableElement.getSimpleName().toString();
caches.put(className,methodName);
}
}
//当前有指定的注解
if(caches.size() > 0) {
//通过javapoet生成指定的类
FieldSpec fieldSpec = FieldSpec.builder(HashMap.class, "cache",
Modifier.PRIVATE, Modifier.FINAL, Modifier.STATIC).
initializer("new HashMap<String,String>()").
build();
CodeBlock.Builder staticBuilder = CodeBlock.builder();
for(Map.Entry<String,String> entry:caches.entrySet()){
staticBuilder.addStatement("cache.put($S,$S)", entry.getKey(), entry.getValue());
}
MethodSpec methodSpec = MethodSpec.methodBuilder("getMethod").
addModifiers(Modifier.PUBLIC).
addParameter(String.class,"className").
returns(String.class).
addStatement("return (String)cache.get(className)").
build();
TypeSpec typeSpec = TypeSpec.classBuilder(Const.APP_BACKGROUND_CLASSNAME).
addModifiers(Modifier.PUBLIC).
addStaticBlock(staticBuilder.build()).
addSuperinterface(IBuriedPointApt.class).
addField(fieldSpec).
addMethod(methodSpec).
build();
JavaFile javaFile = JavaFile.builder(Const.PACKAGE_NAME, typeSpec).build();
try {
javaFile.writeTo(filer);
} catch (IOException e) {
e.printStackTrace();
}
}
log("-----------------------------");
return false;
}
/**
* 打印日志
* @param content
*/
private void log(String content){
messager.printMessage(Diagnostic.Kind.NOTE, content);
}
}
当代码写完之后,运行程序,可以在指定的位置看到编译期生成的类
查看编译期生成的类
编译期生成的类
思路很简单:
1.定义一个接口,后期通过反射可以转型为接口
2.通过注解生成一个缓存
3.运行时通过接口的方法从缓存中获取想要的数据即可
4.这个例子就是通过反射指定的类来调用指定的方法
比方说
public class AnnotationFinder {
private IBuriedPointApt iBuriedPointApt;
private boolean hasApt = true;
private String className;
private Class an;
public AnnotationFinder(String className,Class an) {
this.className = className;
this.an = an;
}
/**
* 编译期已经生成指定的索引
* 当前通过索引来获取参数
* @param activity 当前活动
* @return 指定的参数
*/
private HashMap<String,String> getAptParams(Activity activity){
String methodName = iBuriedPointApt.getMethod(activity.getClass().getCanonicalName());
if(null == methodName){
return null;
}
Method method = null;
try {
method = activity.getClass().getMethod(methodName);
return (HashMap<String, String>) method.invoke(activity);
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
return null;
}
/**
* 从缓存中获取,这个对应编译期生成的类
* @param activity 当前活动
* @return 指定的参数
*/
private HashMap<String,String> getParamsFromIndex(Activity activity){
//当前编译期没有生成对应的类
if(!hasApt){
return null;
}
//尝试直接使用之前已经反射出指定的辅助类
if(null != iBuriedPointApt){
return getAptParams(activity);
}
try {
//通过之前定义的规则来反射指定的类
Class cls = Class.forName(Const.PACKAGE_NAME + "." + className);
iBuriedPointApt = (IBuriedPointApt) cls.newInstance();
return getAptParams(activity);
//出现任何的异常都不允许再使用索引了
} catch (ClassNotFoundException e) {
hasApt = false;
e.printStackTrace();
} catch (IllegalAccessException e) {
hasApt = false;
e.printStackTrace();
} catch (InstantiationException e) {
hasApt = false;
e.printStackTrace();
}
return null;
}
/**
* 通过反射来获取参数
* @param activity 当前活动
* @param c 注解
* @return 指定的参数
*/
private HashMap<String,String> getParamsFromReflect(Activity activity,Class c){
HashMap<String,String> params = new HashMap<>();
//获取当前类中定义的所有方法
Method[] methods = activity.getClass().getDeclaredMethods();
for(Method method:methods){
//尝试从当前方法获取指定的注解
Annotation annotation = method.getAnnotation(c);
if(null != annotation){
try {
params = (HashMap<String, String>) method.invoke(activity);
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
break;
}
}
return params;
}
public HashMap<String,String> getParams(Activity activity,boolean useIndex){
//是否使用索引,实际上就是缓存
if(useIndex){
return getParamsFromIndex(activity);
}else{
return getParamsFromReflect(activity,an);
}
}
}
实际上这里就是为了一个简单的功能
@PageBackground
public Map<String,String> background(){
HashMap<String,String> params = new HashMap<>();
params.put("type","report");
params.put("background",getClass().getSimpleName());
return params;
}
通过指定的注解,来返回想要的参数,这里的场景是埋点的时候,当App进入后台的时候上报当前页面的一些数据
结语
通过注解的使用,确实可以方便一些特定场景的使用,更加成熟的应用,可以去看EventBus、ButterKnife等库的源码