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那更是極好的。感謝觀看。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容