Android自定义注解与注解器实现点击事件绑定

背景:前些天看过的butterKnife解析,感觉自己对注解这一块的了解缺口很大,所以稍微学习了一下,感觉还是很好玩的,所以记录下来。本文长期更新维护。

注解是什么?

这个东西其实一直活在我们的代码中,比如继承的@Override,到butterKnife中的@BindView,但是我们(我)可能习惯性的忽略它。相对于长长的重复性代码(findViewById(xxx)),它更加简介,可读性强,后期维护也比较方便。至于缺点,我想到的是自定义注解在没有说明完好的情况下可能对后来者不是很友好,存在一定的学习成本;另外一点是背后的实现逻辑交给注解器来处理,一旦注解类型多了,处理的逻辑也就多了,因此学习、维护与改错都比较难,相对于直接嵌入工程使用的小段代码而言。

注解类型?

注解中存在元注解(概念上类似与基本数据结构int,short,long等),共有四种@Retention, @Target, @Inherited, @Documented。
一个注解大概长这样子:

@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface BindView {
    int value();
}

比较重要的是@Retention, @Target

@Retention 是指注解保留的范围,默认有三种:

  • SOURCE 源码级别注解,该类型的注解信息只会保留在.java源码中,>.class中不会存在
  • CLASS 编译时注解,会保留在.java和.class中,执行时会被java虚拟机丢弃
  • RUNTIME 运行时注解,不仅是.java .class,还会加载到虚拟机中,可以通过反射机制读取注解信息(方便)

@Target 取值是一个ElementType的类型数组(后面会讲到),用来指定我们注解的使用范围,有这么几种:


image

其中最后两种是java 8新增的,在之前的版本中只允许在声明式前使用注解,但是现在可以用在type之前:
TYPE_PARAMETER 用来表示类型参数,比如

class test<@GzoomAnnotation T>{
      //...
  }

TYPE_USE 适用范围更广,适用于标注的各式形态,比如:

Module m =(@GzoomAnnotation Module)new Object();

更加详细的可以参考这篇文章

注解的过程?

注解大体上又分为运行时注解编译时注解,简单的说就是以什么时候处理注解为分界线。

  • 运行时注解相对比较简单,可以看成“标签”,给属性(或者方法等等)特殊化,在需要的时候找到这些标签的标记,这其中使用了反射的方法。这种方法的优点是方便,简单易学,本质上就是在运行时进行代码调用,和我们平常的反射区别不是很大;缺点还是反射,使得性能比较低。推荐文章:Android中的自定义注解(反射实现-运行时注解)(++还有一点,在Android平台上,查询注解的效率比较低,特别是在Android 4.0之前的系统上,可以看看这个Bug,其中也推荐我们用编译时注解,所以个人观点是慎用++)
  • 编译时注解不需要适用反射,在编译阶段它不能操作已经有的java文件,因此为了实现我们的“目的”,我们可以创造目标java文件来实现代码逻辑功能。
编译时注解流程
注解过程.jpeg

参考工程:Android注解使用之通过annotationProcessor注解生成代码实现自己的ButterKnife框架

工程大体流程:

工程大体流程

原文作者对工程的说明已经很好了,所以我在这里就不班门弄斧了,从我自己diy的点击事件说起。

注解

最基础的当然是我们的注解,给源代码加“标签”,这里我是这样设计的:

@Retention(RetentionPolicy.CLASS)

@Target(ElementType.FIELD)
public @interface GZBindView {
    int value();
}

首先是Retention,我们基于编译注解,所以需要保存直到class文件中;在Target上,我们选择方法注解FIELD;此注解还需要id值来指定绑定的控件,因此我们使用默认的value()方法,这样的好处是不用特别指定字段。

处理器
  1. 支持注解类型
 @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> types = new LinkedHashSet<>();
        types.add(GZBindView.class.getCanonicalName());
        types.add(GZClickView.class.getCanonicalName());
        return types;
    }
  1. 初始化工具
 @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        //filter用来创建新的源文件、class文件以及辅助文件
        mFiler = processingEnv.getFiler();
        //elements中包含着操作element的工具方法
        mElementUtils = processingEnv.getElementUtils();
        //用来报告错误、警告以及其他提示信息
        mMessager = processingEnv.getMessager();
        mAnnotatedClassMap = new TreeMap<>();
        //processingZEnvirment中还有操作TYPE mirror的
        //processingEnv.getTypeUtils();
    }
  1. 在process过程中处理目标注解
 @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        //RoundEnvironment
        //可以返回包含指定注解类型的元素的集合
        mAnnotatedClassMap.clear();
        try {
            //增加方法,处理点击注解
            processBindView(roundEnv);
            processClickBindMethod(roundEnv);
        } catch (IllegalArgumentException e) {
            e.printStackTrace();
            error(e.getMessage());
        }
       、、、
        return true;
    }
处理的流程就是,找到被我们目标注解标记的部分,然后进行储存
/**处理点击事件绑定*/
    private void processClickBindMethod(RoundEnvironment roundEnv) {
        for(Element element : roundEnv.getElementsAnnotatedWith(GZClickView.class))
        {
            //获取对应的生成类
            AnnotatedClass annotatedClass = getAnnotatedClass(element);
          //生成我们的目标注解模型,方便后期文件输出
            ClickViewFIled clickFile = new ClickViewFIled(element);
           annotatedClass.addClickField(clickFile);
        }

    }
获取生成类:
  /**获取注解所在文件对应的生成类*/
    private AnnotatedClass getAnnotatedClass(Element element) {
        //typeElement表示类或者接口元素
        TypeElement typeElement = (TypeElement) element.getEnclosingElement();
        String fullName = typeElement.getQualifiedName().toString();
        //这里其实就是变相获得了注解的类名(完全限定名称,这里是这么说的)
        AnnotatedClass annotatedClass = mAnnotatedClassMap.get(fullName);
        // Map<String, AnnotatedClass> 
        if (annotatedClass == null) {
            annotatedClass = new AnnotatedClass(typeElement, mElementUtils);
            mAnnotatedClassMap.put(fullName, annotatedClass);
        }
        return annotatedClass;
    }
生成类中保存了什么东西呢?
    /**类或者接口元素*/
    private TypeElement mTypeElement;

    /**绑定的view对象*/
    private ArrayList<BindViewField> mFields;

    /**辅助类,用于后文的文件输出*/
    private Elements mElements;

    /**绑定方法域*/
   private ArrayList<ClickViewFIled> mClickFiled;


    /**增加绑定方法域*/
    void addClickField(ClickViewFIled fIled)
    {
        mClickFiled.add(fIled);
    }

    /**
     * @param typeElement 注解所在的类或者接口
     *
     * @param elements 辅助类
     * */
    AnnotatedClass(TypeElement typeElement, Elements elements) {
        mTypeElement = typeElement;
        mElements = elements;
        mFields = new ArrayList<>();
        mClickFiled = new ArrayList<>();
    }

可能也或多或少能猜到,这些是为后面的文件输出做准备。

ClickViewFIled是什么呢?
先跳到另外一个问题,我们如何将注解的方法注册到目标view的点击事件中呢?
首先注册语句可以自己写,但是怎么在一个新的类中引用老类(Activity)中的方法?我开始想复杂了,竟然想用反射= =转念一想,这样我们编译时注解还有意义吗。。。性能又下来了。。

方法二:在Activity中进行绑定的时候我们传入了自身对象,对吧?( ++GZoomViewBinder.bind(this);++)那利用起来不就好了吗?
所以我们只需要保存注解时的id以及方法名就可以了,就此设计了++ClickViewFIled++

public class ClickViewFIled {
    /**方法元素*/
   private ExecutableElement executableElement;
    /**控件id*/
    private int resId ;
    /**绑定方法名*/
    private  String methodName ;
   public ClickViewFIled(Element element)
    {
        //只支持方法注解
        if(element.getKind()!= ElementKind.METHOD)
        {
            throw new IllegalArgumentException(String.format("Only method can be annotated with @%s",
                    GZClickView.class.getSimpleName()));
        }
       //转化成方法元素
        executableElement = (ExecutableElement) element;
        //获取注解对象整体
        GZClickView gzClickView = executableElement.getAnnotation(GZClickView.class);
        //获取id
        resId = gzClickView.value();
        if (resId<0){
            throw new IllegalArgumentException(
                    String.format("value() in %s for field %s is not valid !", GZBindView.class.getSimpleName(),
                            executableElement.getSimpleName()));
        }
         methodName = executableElement.getSimpleName().toString();


    }

    public ExecutableElement getExecutableElement() {
        return executableElement;
    }

    public int getResId() {
        return resId;
    }

    public String getMethodName() {
        return methodName;
    }
}
生成目标文件xxx$$ViewBinder.java

这一块我们使用的是squareup的javapoet进行文件输出,提供了很多方法。
使用:

dependencies {
   、、、
    //提供各种API生成Java代码文件
    compile 'com.squareup:javapoet:1.7.0'
}

生成可以直接看注释,基本是api的使用:

  JavaFile generateFile() {
        //定义方法 bindbindView(final T host, Object object, ViewFinder finder);
        MethodSpec.Builder bindViewMethod = MethodSpec.methodBuilder("bindView")
                .addModifiers(Modifier.PUBLIC)
                .addAnnotation(Override.class)
                .addParameter(TypeName.get(mTypeElement.asType()), "host")
                //后面我们需要使用源文件注册方法到控件中,因此这里需要final
                .addParameter(TypeName.OBJECT, "source",Modifier.FINAL)
                .addParameter(TypeUtil.PROVIDER, "finder");


        for (BindViewField field : mFields) {
            // find views
            bindViewMethod.addStatement("host.$N = ($T)(finder.findView(source, $L))", field.getFieldName(), ClassName.get(field.getFieldType()), field.getResId());
        }

        ClassName androidView = ClassName.get("android.view","View");

        //add clickFiled
        if(mClickFiled!=null) {
            for (ClickViewFIled fIled : mClickFiled) {
                bindViewMethod.addStatement("finder.findView(source, $L).setOnClickListener(new $T.OnClickListener()" +
                        " {" +
                        "@Override " +
                        "public void onClick($T view) " +
                        "{ " +
                        " (($T)source).$N " +
                        "}" +
                        "}" +
                        ");", fIled.getResId(),androidView,androidView,TypeName.get(mTypeElement.asType()),fIled.getMethodName() + "();");
            }//使用source直接调用方法
        }
        //类似的,这里生成unbind方法
        MethodSpec.Builder unBindViewMethod = MethodSpec.methodBuilder("unBindView")
                .addModifiers(Modifier.PUBLIC)
                .addParameter(TypeName.get(mTypeElement.asType()), "host")
                .addAnnotation(Override.class);
        for (BindViewField field : mFields) {
            unBindViewMethod.addStatement("host.$N = null", field.getFieldName());
        }


        //generaClass 生成类
        TypeSpec injectClass = TypeSpec.classBuilder(mTypeElement.getSimpleName() + "$$ViewBinder")//类名字
                .addModifiers(Modifier.PUBLIC)
                .addSuperinterface(ParameterizedTypeName.get(TypeUtil.BINDER, TypeName.get(mTypeElement.asType())))//接口,首先是接口然后是范型
               //再加入我们的目标方法
                .addMethod(bindViewMethod.build())
                .addMethod(unBindViewMethod.build())
                .build();

        String packageName = mElements.getPackageOf(mTypeElement).getQualifiedName().toString();
        return JavaFile.builder(packageName, injectClass)
                .build();
    }

这里有个比较好玩的地方就是,javapoet中的类型转换是自带import的。。。这个比较好玩,因为工程是java library,一开始我还傻傻的找import的api,发现只有import static的

4、使用

 @GZBindView(R.id.textView)
    TextView textView;

    @GZBindView(R.id.button)
    Button button;


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        GZoomViewBinder.bind(this);
        textView.setText("Gzoom's annotation compiler");
    }
    /**自定义-给button设定监听器*/
    @GZClickView(R.id.button)
    public void buttonClick()
    {
        Log.d("gzoom","this is button,help me!");
    }

在GZoomViewBinder.bind中所做的工作就是找到目前调用类的¥$ViewBinder.java,然后实现方法。

到这里,我们的工程就结束了。想详细了解的朋友可以查看
源码
非常欢迎大家评论与指正,star那更是极好的。感谢观看。

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

推荐阅读更多精彩内容