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等库的源码

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,717评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,501评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,311评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,417评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,500评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,538评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,557评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,310评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,759评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,065评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,233评论 1 343
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,909评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,548评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,172评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,420评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,103评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,098评论 2 352

推荐阅读更多精彩内容