Java 注解及其在 Android 中的應用

一般的,注解在 Android 中有兩種應用方式沪悲,一種方式是基于反射的,即在程序的運行期間獲取類信息進行反射調用提茁;另一種是使用注解處理仗嗦,在編譯期間生成許多代碼,然后在運行期間通過調用這些代碼來實現(xiàn)目標功能甘凭。

在本篇文章中,我們會先重溫一下 Java 的注解相關的知識火邓,然后分別介紹一下上面兩種方式的實際應用丹弱。

1、Java 注解回顧

1. Java 注解的基礎知識

Java 中的注解分成標準注解和元注解铲咨。標準注解是 Java 為我們提供的預定義的注解躲胳,共有四種:@Override@Deprecated纤勒、@SuppressWarnnings@SafeVarags坯苹。元注解是用來提供給用戶自定義注解用的,共有五種(截止到Java8):@Target摇天、@Retention粹湃、@Documented@Inherited@Repeatable泉坐,這里我們重點介紹這五種元注解为鳄。

不過,首先我們還是先看一下一個基本的注解的定義的規(guī)范腕让。下面我們自定義了一個名為UseCase的注解孤钦,可以看出我們用到了上面提及的幾種元注解:

    @Documented
    @Retention(RetentionPolicy.RUNTIME)
    @Target(value={METHOD, FIELD})
    public @interface UseCase {
        public int id();
        public String description() default "default value";
    }

這是一個普通的注解的定義。從上面我們也可以總結出纯丸,在定義注解的時候偏形,有以下幾個地方需要注意:

  1. 使用 @interface 聲明并且指定注解的名稱;
  2. 注解的定義類似于接口中的方法的定義觉鼻,但要注意兩者之間本質上是不同的俊扭;
  3. 可以通過 default 為指定的元素指定一個默認值,如果用戶沒有為其指定值滑凉,就使用默認值统扳。

2. 元注解

好的,看完了一個基本的注解的定義畅姊,我們來看一下上面用到的 Java 元注解的含義咒钟。

@Target

@Target 用來指定注解能夠修飾的對象的類型。因為 @Target 本身也是一個注解若未,所以你可以在源碼中查看它的定義朱嘴。該注解接收的參數(shù)是一個 ElementType 類型的數(shù)組,所以,就是說我們自定義的注解可以應用到多種類型的對象萍嬉,而對象的類型由 ElementType 定義乌昔。ElementType 是一個枚舉,它的枚舉值如下:

  • TYPE:類壤追、接口或者enum聲明
  • FIELD:域聲明磕道,包括enum實例
  • METHOD:方法聲明
  • PARAMETER:參數(shù)聲明
  • CONSTRUCTOR:構造器聲明
  • LOCAL_VARIABLE:局部變量聲明
  • ANNOTATION_TYPE:注解聲明
  • PACKAGE:包聲明
  • TYPE_PARAMETER:類型參數(shù)聲明
  • TYPE_USE:使用類型

所以,比如根據(jù)上面的內容行冰,我們可以直到我們的自定義注解 @UseCase 只能應用于方法和字段溺蕉。

@Retention

用來指定注解的保留策略,比如有一些注解悼做,當你在自己的代碼中使用它們的時候你會把它寫在方法上面疯特,但是當你反編譯之后卻發(fā)現(xiàn)這些注解不在了;而有些注解反編譯之后依然存在肛走,發(fā)生這種情況的原因就是在使用該注解的時候指定了不同的參數(shù)潜圃。

@Target 相同的是這個注解也使用枚舉來指定值的類型谈息,不同的是它只能指定一個值,具體可以看源碼。這里它使用的是 RetentionPolicy 枚舉针余,它的幾個值的含義如下:

  • SOURCE:注解將被編譯器丟棄
  • CLASS:注解在class文件中使用姥宝,但會被JVM丟棄
  • RUNTIME:VM將在運行期保留注解岳悟,故可以通過反射讀取注解的信息

當我們在 Android 中使用注解的時候愧驱,一種是在運行時使用的,所以我們要用 RUNTIME腾誉;另一種是在編譯時使用的徘层,所以我們用 CLASS

@Documented利职、@Inherited 和 @Repeatable

這三個元注解的功能比較簡單和容易理解趣效,這里我們一起給出即可:

  • @Documented 表示此注解將包含在 javadoc 中;
  • @Inherited 表示允許子類繼承父類的注解猪贪;
  • @Repeatable 是 Java8 中新增的注解跷敬,表示指定的注解可以重復應用到指定的對象上面。

上文热押,我們回顧了 Java 中注解相關的知識點西傀,相信你已經(jīng)對注解的內容有了一些了解,那么我們接下來看一下注解在實際開發(fā)中的兩種應用方式桶癣。

2拥褂、注解的兩種使用方式

在我開始為我的開源項目 馬克筆記 編寫數(shù)據(jù)庫的時候,我考慮了使用注解來為數(shù)據(jù)庫對象指定字段的信息牙寞,并根據(jù)這心信息來拼接出創(chuàng)建數(shù)據(jù)庫表的 SQL 語句饺鹃。當時也想用反射來動態(tài)為每個字段賦值的莫秆,但是考慮到反射的性能比較差,最終放棄了這個方案悔详。但是镊屎,使用注解處理的方式可以完美的解決我們的問題,即在編譯的時候動態(tài)生成一堆代碼茄螃,實際賦值的時候調用這些方法來完成缝驳。這前后兩種方案就是我們今天要講的注解的兩種使用方式。

2.1 基于反射使用注解

這里為了演示基于反射的注解的使用方式归苍,我們寫一個小的 Java 程序党巾,要實現(xiàn)的目的是:定義兩個個注解,一個應用于方法霜医,一個應用于字段,然后我們使用這兩個注解來定義一個類驳规。我們想要在代碼中動態(tài)地打印出使用了注解的方法和字段的信息和注解信息肴敛。

這里我們先定義兩個注解,應用于字段的 @Column 注解和應用于方法 @Important 注解:

    @Target(value = {ElementType.FIELD})
    @Retention(RetentionPolicy.RUNTIME)
    public @interface Column {
        String name();
    }

    @Target(value = {ElementType.METHOD})
    @Retention(RetentionPolicy.RUNTIME)
    public @interface WrappedMethod {
        // empty
    }

然后我們定義了一個Person類吗购,并使用注解為其中的部分方法和字段添加注解:

    private static class Person {

        @Column(name = "id")
        private int id;

        @Column(name = "first_name")
        private String firstName;

        @Column(name = "last_name")
        private String lastName;

        private int temp;

        @WrappedMethod()
        public String getInfo() {
            return id + " :" + firstName + " " + lastName;
        }

        public String method() {
            return "Nothing";
        }
    }

然后医男,我們使用Person類來獲取該類的字段和方法的信息,并輸出具有注解的部分:

    public static void main(String...args) {
        Class<?> c = Person.class;
        Method[] methods = c.getDeclaredMethods();
        for (Method method : methods) {
            if (method.getAnnotation(WrappedMethod.class) != null) {
                System.out.print(method.getName() + " ");
            }
        }
        System.out.println();
        Field[] fields = c.getDeclaredFields();
        for (Field field : fields) {
            Column column = field.getAnnotation(Column.class);
            if (column != null) {
                System.out.print(column.name() + "-" + field.getName() + ", ");
            }
        }
    }

輸出結果:

getInfo
id-id, first_name-firstName, last_name-lastName, 

在上面的代碼的執(zhí)行結果捻勉,我們可以看出:使用了注解和反射之后镀梭,我們成功的打印出了使用了注解的字段。這里我們需要先獲取指定的類的 Class 類型踱启,然后用反射獲取它的所有方法和字段信息并進行遍歷报账,通過判斷它們的 getAnnotation() 方法的結果來確定這個方法和字段是否使用了指定類型的注解。

上面的代碼可以解決一些問題埠偿,但同時透罢,我們還有一些地方需要注意:

  1. 如果指定的方法或者字段名被混淆了怎么辦? 對于一些可以自定義名稱的情景冠蒋,我們可以在注解中加入?yún)?shù)為該字段或者方法指定一個名稱羽圃;
  2. 上面使用了很多的反射,這會影響程序的性能嗎抖剿? 使用注解的方式肯定性能不會高朽寞,但是如果注解的使用沒有那么頻繁,上面方法不會有特別大的性能損耗斩郎,比如拼接 SQL 這樣的操作脑融,可能只需要執(zhí)行一次。不過孽拷,根本的解決辦法是使用注解的第二種使用方式吨掌!

2.2 基于 annotationProcessor 使用注解

也許你之前已經(jīng)使用過 ButterKnife 這樣的注入框架,不知道你是否記得在 Gradle 中引用它的時候加入了下面這行依賴:

    annotationProcessor 'com.jakewharton:butterknife-compiler:8.8.1'

這里的 annotationProcessor 就是我們這里要講的注解處理。本質上它會在編譯的時候膜宋,在你調用 ButterKnife.bind(this); 方法的那個類所在的包下面生成一些類窿侈,當調用 ButterKnife.bind(this); 的時候實際上就完成了為使用注解的方法和控件綁定的過程。也就是秋茫,本質上還是調用了 findViewById()史简,只是這個過程被隱藏了,不用你來完成了肛著,僅此而已圆兵。

下面,我們就使用注解處理的功能來制作一個類似于 ButterKnife 的簡單庫枢贿。不過殉农,在那之前我們還需要做一些準備——一些知識點需要進行說明。即 JavapoetAbstractProcessor局荚。

Javapoet & AbstractProcessor

Javapoet 是一個用來生成 .java 文件的 Java API超凳,由 Square 開發(fā),你可以在它的 Github 主頁中了解它的基本使用方法耀态。它的好處就是對方法轮傍、類文件和代碼等的拼接進行了封裝,有了它首装,我們就不用再按照字符串的方式去拼接出一段代碼了创夜。相比于直接使用字符串的方式,它還可以生成代碼的同時直接 import 對應的引用仙逻,可以說是非常方便驰吓、快捷的一個庫了。

這里的 AbstractProcessor 是用來生成類文件的核心類系奉,它是一個抽象類棚瘟,一般使用的時候我們只要覆寫它的方法中的4個就可以了。下面是這些方法及其定義:

  1. init:在生成代碼之前被調用喜最,可以從它參數(shù) ProcessingEnvironment 獲取到非常多有用的工具類偎蘸;
  2. process:用于生成代碼的 Java 方法,可以從參數(shù) RoundEnvironment 中獲取使用指定的注解的對象的信息瞬内,并包裝成一個 Element 類型返回迷雪;
  3. getSupportedAnnotationTypes:用于指定該處理器適用的注解;
  4. getSupportedSourceVersion:用來指定你使用的 Java 的版本虫蝶。

這幾個方法中章咧,除了 process,其他方法都不是必須覆寫的方法能真。這里的 getSupportedAnnotationTypesgetSupportedSourceVersion 可以使用注 @SupportedAnnotationTypes@SupportedSourceVersion 來替換赁严,但是不建議這么做扰柠。因為前面的注解接收的參數(shù)是字符串,如果你使用了混淆可能就比較麻煩疼约,后面的注解只能使用枚舉卤档,相對欠缺了靈活性。

另一個我們需要特別說明的地方是程剥,繼承 AbstractProcessor 并實現(xiàn)了我們自己的處理器之后還要對它進行注冊才能使用劝枣。一種做法是在與 java 同的目錄下面創(chuàng)建一個 resources 文件夾,并在其中創(chuàng)建 META-INF/service 文件夾织鲸,然后在其中創(chuàng)建一個名為javax.annotation.processing.Processor 的文件舔腾,并在其中寫上我們的處理器的完整路徑。另一種做法是使用谷歌的 @AutoService 注解搂擦,你只需要在自己的處理器上面加上 @AutoService(Processor.class) 一行代碼即可稳诚。當然,前提是你需要在自己的項目中引入依賴:

    compile 'com.google.auto.service:auto-service:1.0-rc2'

按照后面的這種方式一樣會在目錄下面生成上面的那個文件瀑踢,只是這個過程不需要我們來操作了采桃。你可以通過查看buidl出的文件來找到生成的文件。

MyKnife 的最終結果

在定制之前丘损,我們先看一下程序的最終執(zhí)行結果,也許這樣會更有助于理解整個過程的原理工扎。我們程序的最終的執(zhí)行結果是徘钥,在編譯的時候,在使用我們的工具的類的相同級別的包下面生成一個類肢娘。如下圖所示:

程序的執(zhí)行結果

這里的 me.shouheng.libraries 是我們應用 MyKnife 的包呈础,這里我們在它下面生成了一個名為 MyKnifeActivity$$Injector 的類,它的定義如下:

    public class MyKnifeActivity$$Injector implements Injector<MyKnifeActivity> {
      @Override
      public void inject(final MyKnifeActivity host, Object source, Finder finder) {
        host.textView=(TextView)finder.findView(source, 2131230952);
        View.OnClickListener listener;
        listener = new View.OnClickListener() {
          @Override
          public void onClick(View view) {
            host.OnClick();
          }
        };
        finder.findView(source, 2131230762).setOnClickListener(listener);
      }
    }

因為我們應用 MyKnife 的類是 MyKnifeActivity橱健,所以這里就生成了名為 MyKnifeActivity$$Injector 的類而钞。通過上面的代碼,可以看出它實際上調用了 Finder 的方法來為我們的控件 textView 賦值拘荡,然后使用控件的 setOnClickListener() 方法為點擊事件賦值臼节。這里的 Finder 是我們封裝的一個對象,用來從指定的源中獲取控件的類珊皿,本質上還是調用了指定源的 findViewById() 方法网缝。

然后,與 ButterKnife 類似的是蟋定,在使用我們的工具的時候粉臊,也需要在 Activity 的 onCreate() 中調用 bind() 方法。這里我們看下這個方法做了什么操作:

    public static void bind(Object host, Object source, Finder finder) {
        String className = host.getClass().getName();
        try {
            Injector injector = FINDER_MAPPER.get(className);
            if (injector == null) {
                Class<?> finderClass = Class.forName(className + "$$Injector");
                injector = (Injector) finderClass.newInstance();
                FINDER_MAPPER.put(className, injector);
            }
            injector.inject(host, source, finder);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        }
    }

從上面的代碼中可以看出驶兜,調用 bind() 方法的時候會從 FINDER_MAPPER 嘗試獲取指定 類名$$Injector 的文件扼仲。所以远寸,如果說我們應用 bind()的類是 MyKnifeActivity,那么這里獲取到的類將會是 MyKnifeActivity$$Injector屠凶。然后驰后,當我們調用 inject 方法的時候就執(zhí)行了我們上面的注入操作,來完成對控件和點擊事件的賦值阅畴。這里的 FINDER_MAPPER 是一個哈希表倡怎,用來緩存指定的 Injector 的。所以贱枣,從上面也可以看出监署,這里進行值綁定的時候使用了反射,所以纽哥,在應用框架的時候還需要對混淆進行處理钠乏。

OK,看完了程序的最終結果春塌,我們來看一下如何生成上面的那個類文件晓避。

API 和注解的定義

首先只壳,我們需要定義注解用來提供給用戶進行事件和控件的綁定俏拱,

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

    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.CLASS)
    public @interface OnClick {
        int[] ids();
    }

如上面的代碼所示,可以看出我們分別用了 ElementType.FIELDElementType.METHOD 指定它們是應用于字段和方法的吼句,然后用了 RetentionPolicy.CLASS 標明它們不會被保留到程序運行時锅必。

然后,我們需要定義 MyKnife惕艳,它提供了一個 bind() 方法搞隐,其定義如下:

    public static void bind(Object host, Object source, Finder finder) {
        String className = host.getClass().getName();
        try {
            Injector injector = FINDER_MAPPER.get(className);
            if (injector == null) {
                Class<?> finderClass = Class.forName(className + "$$Injector");
                injector = (Injector) finderClass.newInstance();
                FINDER_MAPPER.put(className, injector);
            }
            injector.inject(host, source, finder);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        }
    }

這里的三個參數(shù)的含義分別是:host 是調用綁定方法的類,比如 Activity 等远搪;source是從用來獲取綁定的值的數(shù)據(jù)源劣纲,一般理解是從 source 中獲取控件賦值給 host 中的字段,通常兩者是相同的谁鳍;最后一個參數(shù) finder 是一個接口癞季,是獲取數(shù)據(jù)的方法的一個封裝,有兩默認的實現(xiàn)倘潜,一個是 ActivityFinder余佛,一個是 ViewFinder,分別用來從 Activity 和 View 中查找控件窍荧。

我們之前已經(jīng)講過 bind() 方法的作用辉巡,即使用反射根據(jù)類名來獲取一個 Injector,然后調用它的 inject() 方法進行注入蕊退。這里的 Injector 是一個接口郊楣,我們不會寫代碼去實現(xiàn)它憔恳,而是在編譯的時候讓編譯器直接生成它的實現(xiàn)類。

代碼的生成過程

在介紹 Javapoet 和 AbstractProcessor 的時候净蚤,我們提到過 Element钥组,它封裝了應用注解的對象(方法、字段或者類等)的信息今瀑。我們可以從 Element 中獲取這些信息并將它們封裝成一個對象來方便我們調用程梦。于是就產(chǎn)生了 BindViewFieldOnClickMethod 兩個類。它們分別用來描述使用 @BindView 注解和使用 @OnClick 注解的對象的信息橘荠。此外屿附,還有一個 AnnotatedClass,它用來描述使用注解的整個類的信息哥童,并且其中定義了List<BindViewField>List<OnClickMethod>挺份,分別用來存儲該類中應用注解的字段和方法的信息。

與生成文件和獲取注解的對象信息相關的幾個字段都是從 AbstractProcessor 中獲取的贮懈。如下面的代碼所示匀泊,我們可以從 AbstractProcessor 的 init() 方法的 ProcessingEnvironment 中獲取到 ElementsFilerMessager朵你。它們的作用分別是:Elements 類似于一個工具類各聘,用來從 Element 中獲取注解對象的信息;Filer 用來支持通過注釋處理器創(chuàng)建新文件抡医;Messager 提供注釋處理器用來報告錯誤消息躲因、警告和其他通知的方式。

    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
        elements = processingEnvironment.getElementUtils();
        messager = processingEnvironment.getMessager();
        filer = processingEnvironment.getFiler();
    }

然后在 AbstractProcessor 的 process() 方法中的 RoundEnvironment 參數(shù)中魂拦,我們又可以獲取到指定注解對應的 Element 信息。代碼如下所示:

    private Map<String, AnnotatedClass> map = new HashMap<>();

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        map.clear();
        try {
            // 分別用來處理我們定義的兩種注解
            processBindView(roundEnvironment);
            processOnClick(roundEnvironment);
        } catch (IllegalArgumentException e) {
            return true;
        }

        try {
            // 為緩存的各個使用注解的類生成類文件
            for (AnnotatedClass annotatedClass : map.values()) {
                annotatedClass.generateFinder().writeTo(filer);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return true;
    }

    // 從RoundEnvironment中獲取@BindView注解的信息
    private void processBindView(RoundEnvironment roundEnv) {
        for (Element element : roundEnv.getElementsAnnotatedWith(BindView.class)) {
            AnnotatedClass annotatedClass = getAnnotatedClass(element);
            BindViewField field = new BindViewField(element);
            annotatedClass.addField(field);
        }
    }

    // 從RoundEnvironment中獲取@OnClick注解的信息
    private void processOnClick(RoundEnvironment roundEnv) {
        for (Element element : roundEnv.getElementsAnnotatedWith(OnClick.class)) {
            AnnotatedClass annotatedClass = getAnnotatedClass(element);
            OnClickMethod method = new OnClickMethod(element);
            annotatedClass.addMethod(method);
        }
    }

    // 獲取使用注解的類的信息搁嗓,先嘗試從緩存中獲取芯勘,緩存中沒有的話就實例化一個并放進緩存中
    private AnnotatedClass getAnnotatedClass(Element element) {
        TypeElement encloseElement = (TypeElement) element.getEnclosingElement();
        String fullClassName = encloseElement.getQualifiedName().toString();
        AnnotatedClass annotatedClass = map.get(fullClassName);
        if (annotatedClass == null) {
            annotatedClass = new AnnotatedClass(encloseElement, elements);
            map.put(fullClassName, annotatedClass);
        }
        return annotatedClass;
    }

上面的代碼的邏輯是,在調用 process() 方法的時候腺逛,會根據(jù)傳入的 RoundEnvironment 分別處理兩種注解荷愕。兩個注解的相關信息都會被解析成 List<BindViewField>List<OnClickMethod>,然后把使用注解的整個類的信息統(tǒng)一放置在 AnnotatedClass 中棍矛。為了提升程序的效率安疗,這里用了緩存來存儲類信息。最后够委,我們調用了 annotatedClass.generateFinder() 獲取一個JavaFile荐类,并調用它的 writeTo(filer) 方法生成類文件。

上面的代碼重點在于解析使用注解的類的信息茁帽,至于如何根據(jù)類信息生成類文件玉罐,我們還需要看下 AnnotatedClassgenerateFinder() 方法屈嗤,其代碼如下所示。這里我們用了之前提到的 Javapoet 來幫助我們生成類文件:

    public JavaFile generateFinder() {
        // 這里用來定義inject方法的簽名
        MethodSpec.Builder builder = MethodSpec.methodBuilder("inject")
                .addModifiers(Modifier.PUBLIC)
                .addAnnotation(Override.class)
                .addParameter(TypeName.get(typeElement.asType()), "host", Modifier.FINAL)
                .addParameter(TypeName.OBJECT, "source")
                .addParameter(TypeUtils.FINDER, "finder");
        // 這里用來定義inject方法中@BindView注解的綁定過程
        for (BindViewField field : bindViewFields) {
            builder.addStatement("host.$N=($T)finder.findView(source, $L)",
                    field.getFieldName(),
                    ClassName.get(field.getFieldType()),
                    field.getViewId());
        }
        // 這里用來定義inject方法中@OnClick注解的綁定過程
        if (onClickMethods.size() > 0) {
            builder.addStatement("$T listener", TypeUtils.ONCLICK_LISTENER);
        }
        for (OnClickMethod method : onClickMethods) {
            TypeSpec listener = TypeSpec.anonymousClassBuilder("")
                    .addSuperinterface(TypeUtils.ONCLICK_LISTENER)
                    .addMethod(MethodSpec.methodBuilder("onClick")
                            .addAnnotation(Override.class)
                            .addModifiers(Modifier.PUBLIC)
                            .returns(TypeName.VOID)
                            .addParameter(TypeUtils.ANDROID_VIEW, "view")
                            .addStatement("host.$N()", method.getMethodName())
                            .build())
                    .build();
            builder.addStatement("listener = $L", listener);
            for (int id : method.getIds()) {
                builder.addStatement("finder.findView(source, $L).setOnClickListener(listener)", id);
            }
        }
        // 這里用來獲取要生成的類所在的包的信息
        String packageName = getPackageName(typeElement);
        String className = getClassName(typeElement, packageName);
        ClassName bindClassName = ClassName.get(packageName, className);

        // 用來最終組裝成我們要輸出的類
        TypeSpec finderClass = TypeSpec.classBuilder(bindClassName.simpleName() + "$$Injector")
                .addModifiers(Modifier.PUBLIC)
                .addSuperinterface(ParameterizedTypeName.get(TypeUtils.INJECTOR, TypeName.get(typeElement.asType())))
                .addMethod(builder.build())
                .build();
        return JavaFile.builder(packageName, finderClass).build();
    }

上面就是我們用來最終生成類文件的方法吊输,這里用了 Javapoet 饶号,如果對它不是很了解可以到 Github 上面了解一下它的用法。

這樣我們就完成了整個方法的定義季蚂。

使用 MyKnife

使用我們定義的 MyKnife 茫船,我們只需要在 Gradle 里面引入我們的包即可:

    implementation project(':knife-api')
    implementation project(':knife-annotation')
    annotationProcessor project(':knife-compiler')

也許你在有的地方看到過要使用 android-apt 引入注解處理器,其實這里的annotationProcessor 與之作用是一樣的扭屁。這里推薦使用 annotationProcessor算谈,因為它更加簡潔,不需要額外的配置疯搅,也是官方推薦的使用方式濒生。

然后,我們只需要在代碼中使用它們就可以了:

public class MyKnifeActivity extends CommonActivity<ActivityMyKnifeBinding> {

    @BindView(id = R.id.tv)
    public TextView textView;

    @OnClick(ids = {R.id.btn})
    public void OnClick() {
        ToastUtils.makeToast("OnClick");
    }

    @Override
    protected int getLayoutResId() {
        return R.layout.activity_my_knife;
    }

    @Override
    protected void doCreateView(Bundle savedInstanceState) {
        MyKnife.bind(this);
        textView.setText("This is MyKnife demo!");
    }
}

這里有幾個地方需要注意:

  1. 使用注解的方法和字段需要至少是 protected幔欧,因為我們使用了直接引用的方式罪治,而生成的文件和上面的類包相同,所以至少應該保證包級別訪問權限礁蔗;
  2. 上面使用注解的方式只能在當前 Module 作為 application 的時候使用觉义,作為 library 的時候無法使用,這是因為只有當 Module 作為 application 的時候浴井,R文件中的 id 是 final 的晒骇,作為 library 的時候是非 final 的。

總結

這里我們總結一下按照第二種方式使用注解的時候需要步驟:

  1. 首先磺浙,我們需要按照自己的需要考慮如何定義注解洪囤。
  2. 然后,我們需要實現(xiàn) AbstractProcessor 撕氧,覆寫各個方法瘤缩,注冊,并在 process 方法中完成生成類文件的操作伦泥。

2.3 使用注解替換枚舉

注解常見的第三種使用方式是用來取代枚舉的剥啤。因為枚舉相比于普通的字符串或者整數(shù)會帶來額外的內存占用,因此對于 Android 這種對內存要求比較高的項目而言就需要對枚舉進行優(yōu)化不脯。當然府怯,我們使用字符串常量或者整數(shù)常量替換枚舉就可以了,但是這種方式的參數(shù)可以接受任意字符串和整型的值防楷。假如我們希望能夠像枚舉一樣對傳入的參數(shù)的范圍進行限制牺丙,就需要使用枚舉了!

比如复局,我們需要對相機的閃光燈參數(shù)進行限制赘被,每個參數(shù)通過一個整型的變量指定是整。然后,我們通過一個方法接受整型的參數(shù)民假,并通過注解來要求指定的整型必須在我們上述聲明的整型范圍之內浮入。我們可以這樣定義,

首先羊异,我們定義一個類 Camera 用來存儲閃光燈的枚舉值和注解事秀,

public final class Camera {

    public static final int FLASH_AUTO                      = 0;
    public static final int FLASH_ON                        = 1;
    public static final int FLASH_OFF                       = 2;
    public static final int FLASH_TORCH                     = 3;
    public static final int FLASH_RED_EYE                   = 4;

    @IntDef({FLASH_ON, FLASH_OFF, FLASH_AUTO, FLASH_TORCH, FLASH_RED_EYE})
    @Retention(RetentionPolicy.SOURCE)
    public @interface FlashMode {
    }
}

如上所示,這樣我們就定義了枚舉值及其注解野舶。然后易迹,我們可以這樣使用該注解,

public final class Configuration implements Parcelable {

    @Camera.FlashMode
    private int flashMode = Camera.FLASH_AUTO;

    public void setFlashMode(@Camera.FlashMode int flashMode) {
        this.flashMode = flashMode;
    }
}

這樣當我們傳入的參數(shù)不在我們自定義枚舉的 @IntDef 指定的范圍之內的時候平道,IDE 會自動給出提示睹欲。

3、總結

以上就是注解的兩種比較常見的使用方式一屋。第一種是通過反射來進行的窘疮,因為反射本身的效率比較低,所以比較適用于發(fā)射比較少的場景冀墨;第二種方式是在編譯期間通過編譯器生成代碼來實現(xiàn)的闸衫,相比于第一種,它還是可能會用到反射的诽嘉,但是不必在運行時對類的每個方法和字段進行遍歷蔚出,因而效率高得多。

以上虫腋。

獲取源碼:Android-references

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末骄酗,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子悦冀,更是在濱河造成了極大的恐慌趋翻,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,591評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件雏门,死亡現(xiàn)場離奇詭異嘿歌,居然都是意外死亡掸掏,警方通過查閱死者的電腦和手機茁影,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,448評論 3 392
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來丧凤,“玉大人募闲,你說我怎么就攤上這事≡复” “怎么了浩螺?”我有些...
    開封第一講書人閱讀 162,823評論 0 353
  • 文/不壞的土叔 我叫張陵靴患,是天一觀的道長。 經(jīng)常有香客問我要出,道長鸳君,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,204評論 1 292
  • 正文 為了忘掉前任患蹂,我火速辦了婚禮或颊,結果婚禮上,老公的妹妹穿的比我還像新娘传于。我一直安慰自己囱挑,他們只是感情好,可當我...
    茶點故事閱讀 67,228評論 6 388
  • 文/花漫 我一把揭開白布沼溜。 她就那樣靜靜地躺著平挑,像睡著了一般。 火紅的嫁衣襯著肌膚如雪系草。 梳的紋絲不亂的頭發(fā)上通熄,一...
    開封第一講書人閱讀 51,190評論 1 299
  • 那天,我揣著相機與錄音悄但,去河邊找鬼棠隐。 笑死,一個胖子當著我的面吹牛檐嚣,可吹牛的內容都是我干的助泽。 我是一名探鬼主播,決...
    沈念sama閱讀 40,078評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼嚎京,長吁一口氣:“原來是場噩夢啊……” “哼嗡贺!你這毒婦竟也來了?” 一聲冷哼從身側響起鞍帝,我...
    開封第一講書人閱讀 38,923評論 0 274
  • 序言:老撾萬榮一對情侶失蹤诫睬,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后帕涌,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體摄凡,經(jīng)...
    沈念sama閱讀 45,334評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,550評論 2 333
  • 正文 我和宋清朗相戀三年蚓曼,在試婚紗的時候發(fā)現(xiàn)自己被綠了亲澡。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,727評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡纫版,死狀恐怖床绪,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤癞己,帶...
    沈念sama閱讀 35,428評論 5 343
  • 正文 年R本政府宣布膀斋,位于F島的核電站,受9級特大地震影響痹雅,放射性物質發(fā)生泄漏仰担。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,022評論 3 326
  • 文/蒙蒙 一绩社、第九天 我趴在偏房一處隱蔽的房頂上張望惰匙。 院中可真熱鬧,春花似錦铃将、人聲如沸项鬼。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,672評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽绘盟。三九已至,卻和暖如春悯仙,著一層夾襖步出監(jiān)牢的瞬間龄毡,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,826評論 1 269
  • 我被黑心中介騙來泰國打工锡垄, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留沦零,地道東北人。 一個月前我還...
    沈念sama閱讀 47,734評論 2 368
  • 正文 我出身青樓货岭,卻偏偏與公主長得像路操,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子千贯,可洞房花燭夜當晚...
    茶點故事閱讀 44,619評論 2 354

推薦閱讀更多精彩內容