Java 進階 | 註解及其在 Android 中的應用

通常的,註解在 Android 中有兩種應用方式,一種方式是基於反射的,即在程序的運行期間獲取類信息進行反射調用;另外一種是使用註解處理,在編譯期間生成許多代碼,而後在運行期間經過調用這些代碼來實現目標功能。java

在本篇文章中,咱們會先重溫一下 Java 的註解相關的知識,而後分別介紹一下上面兩種方式的實際應用。android

一、Java 註解回顧

1. Java 註解的基礎知識

Java 中的註解分紅標準註解和元註解。標準註解是 Java 爲咱們提供的預約義的註解,共有四種:@Override@Deprecated@SuppressWarnnings@SafeVarags。元註解是用來提供給用戶自定義註解用的,共有五種(截止到Java8):@Target@Retention@Documented@Inherited@Repeatable,這裏咱們重點介紹這五種元註解。git

不過,首先咱們仍是先看一下一個基本的註解的定義的規範。下面咱們自定義了一個名爲UseCase的註解,能夠看出咱們用到了上面說起的幾種元註解:github

@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 元註解的含義。api

@Target

@Target 用來指定註解可以修飾的對象的類型。由於 @Target 自己也是一個註解,因此你能夠在源碼中查看它的定義。該註解接收的參數是一個 ElementType 類型的數組,因此,就是說咱們自定義的註解能夠應用到多種類型的對象,而對象的類型由 ElementType 定義。ElementType 是一個枚舉,它的枚舉值以下:數組

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

因此,好比根據上面的內容,咱們能夠直到咱們的自定義註解 @UseCase 只能應用於方法和字段。緩存

@Retention

用來指定註解的保留策略,好比有一些註解,當你在本身的代碼中使用它們的時候你會把它寫在方法上面,可是當你反編譯以後卻發現這些註解不在了;而有些註解反編譯以後依然存在,發生這種狀況的緣由就是在使用該註解的時候指定了不一樣的參數。markdown

@Target 相同的是這個註解也使用枚舉來指定值的類型,不一樣的是它只能指定一個值,具體能夠看源碼。這裏它使用的是 RetentionPolicy 枚舉,它的幾個值的含義以下:app

  • SOURCE:註解將被編譯器丟棄
  • CLASS:註解在class文件中使用,但會被JVM丟棄
  • RUNTIME:VM將在運行期保留註解,故能夠經過反射讀取註解的信息

當咱們在 Android 中使用註解的時候,一種是在運行時使用的,因此咱們要用 RUNTIME;另外一種是在編譯時使用的,因此咱們用 CLASS

@Documented、@Inherited 和 @Repeatable

這三個元註解的功能比較簡單和容易理解,這裏咱們一塊兒給出便可:

  • @Documented 表示此註解將包含在 javadoc 中;
  • @Inherited 表示容許子類繼承父類的註解;
  • @Repeatable 是 Java8 中新增的註解,表示指定的註解能夠重複應用到指定的對象上面。

上文,咱們回顧了 Java 中註解相關的知識點,相信你已經對註解的內容有了一些瞭解,那麼咱們接下來看一下註解在實際開發中的兩種應用方式。

二、註解的兩種使用方式

在我開始爲個人開源項目 馬克筆記 編寫數據庫的時候,我考慮了使用註解來爲數據庫對象指定字段的信息,並根據這心信息來拼接出建立數據庫表的 SQL 語句。當時也想用反射來動態爲每一個字段賦值的,可是考慮到反射的性能比較差,最終放棄了這個方案。可是,使用註解處理的方式能夠完美的解決咱們的問題,即在編譯的時候動態生成一堆代碼,實際賦值的時候調用這些方法來完成。這先後兩種方案就是咱們今天要講的註解的兩種使用方式。

2.1 基於反射使用註解

這裏爲了演示基於反射的註解的使用方式,咱們寫一個小的 Java 程序,要實現的目的是:定義兩個個註解,一個應用於方法,一個應用於字段,而後咱們使用這兩個註解來定義一個類。咱們想要在代碼中動態地打印出使用了註解的方法和字段的信息和註解信息。

這裏咱們先定義兩個註解,應用於字段的 @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, 
複製代碼

在上面的代碼的執行結果,咱們能夠看出:使用了註解和反射以後,咱們成功的打印出了使用了註解的字段。這裏咱們須要先獲取指定的類的 Class 類型,而後用反射獲取它的全部方法和字段信息並進行遍歷,經過判斷它們的 getAnnotation() 方法的結果來肯定這個方法和字段是否使用了指定類型的註解。

上面的代碼能夠解決一些問題,但同時,咱們還有一些地方須要注意:

  1. 若是指定的方法或者字段名被混淆了怎麼辦? 對於一些能夠自定義名稱的情景,咱們能夠在註解中加入參數爲該字段或者方法指定一個名稱;
  2. 上面使用了不少的反射,這會影響程序的性能嗎? 使用註解的方式確定性能不會高,可是若是註解的使用沒有那麼頻繁,上面方法不會有特別大的性能損耗,好比拼接 SQL 這樣的操做,可能只須要執行一次。不過,根本的解決辦法是使用註解的第二種使用方式!

2.2 基於 annotationProcessor 使用註解

也許你以前已經使用過 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 開發,你能夠在它的 Github 主頁中瞭解它的基本使用方法。它的好處就是對方法、類文件和代碼等的拼接進行了封裝,有了它,咱們就不用再按照字符串的方式去拼接出一段代碼了。相比於直接使用字符串的方式,它還能夠生成代碼的同時直接 import 對應的引用,能夠說是很是方便、快捷的一個庫了。

這裏的 AbstractProcessor 是用來生成類文件的核心類,它是一個抽象類,通常使用的時候咱們只要覆寫它的方法中的4個就能夠了。下面是這些方法及其定義:

  1. init:在生成代碼以前被調用,能夠從它參數 ProcessingEnvironment 獲取到很是多有用的工具類;
  2. process:用於生成代碼的 Java 方法,能夠從參數 RoundEnvironment 中獲取使用指定的註解的對象的信息,幷包裝成一個 Element 類型返回;
  3. getSupportedAnnotationTypes:用於指定該處理器適用的註解;
  4. getSupportedSourceVersion:用來指定你使用的 Java 的版本。

這幾個方法中,除了 process,其餘方法都不是必須覆寫的方法。這裏的 getSupportedAnnotationTypesgetSupportedSourceVersion 可使用注 @SupportedAnnotationTypes@SupportedSourceVersion 來替換,可是不建議這麼作。由於前面的註解接收的參數是字符串,若是你使用了混淆可能就比較麻煩,後面的註解只能使用枚舉,相對欠缺了靈活性。

另外一個咱們須要特別說明的地方是,繼承 AbstractProcessor 並實現了咱們本身的處理器以後還要對它進行註冊才能使用。一種作法是在與 java 同的目錄下面建立一個 resources 文件夾,並在其中建立 META-INF/service 文件夾,而後在其中建立一個名爲javax.annotation.processing.Processor 的文件,並在其中寫上咱們的處理器的完整路徑。另外一種作法是使用谷歌的 @AutoService 註解,你只須要在本身的處理器上面加上 @AutoService(Processor.class) 一行代碼便可。固然,前提是你須要在本身的項目中引入依賴:

compile 'com.google.auto.service:auto-service:1.0-rc2'
複製代碼

按照後面的這種方式同樣會在目錄下面生成上面的那個文件,只是這個過程不須要咱們來操做了。你能夠經過查看buidl出的文件來找到生成的文件。

MyKnife 的最終結果

在定製以前,咱們先看一下程序的最終執行結果,也許這樣會更有助於理解整個過程的原理。咱們程序的最終的執行結果是,在編譯的時候,在使用咱們的工具的類的相同級別的包下面生成一個類。以下圖所示:

程序的執行結果

這裏的 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 方法的時候就執行了咱們上面的注入操做,來完成對控件和點擊事件的賦值。這裏的 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();
        }
    }
複製代碼

這裏的三個參數的含義分別是:host 是調用綁定方法的類,好比 Activity 等;source是從用來獲取綁定的值的數據源,通常理解是從 source 中獲取控件賦值給 host 中的字段,一般二者是相同的;最後一個參數 finder 是一個接口,是獲取數據的方法的一個封裝,有兩默認的實現,一個是 ActivityFinder,一個是 ViewFinder,分別用來從 Activity 和 View 中查找控件。

咱們以前已經講過 bind() 方法的做用,即便用反射根據類名來獲取一個 Injector,而後調用它的 inject() 方法進行注入。這裏的 Injector 是一個接口,咱們不會寫代碼去實現它,而是在編譯的時候讓編譯器直接生成它的實現類。

代碼的生成過程

在介紹 Javapoet 和 AbstractProcessor 的時候,咱們提到過 Element,它封裝了應用註解的對象(方法、字段或者類等)的信息。咱們能夠從 Element 中獲取這些信息並將它們封裝成一個對象來方便咱們調用。因而就產生了 BindViewFieldOnClickMethod 兩個類。它們分別用來描述使用 @BindView 註解和使用 @OnClick 註解的對象的信息。此外,還有一個 AnnotatedClass,它用來描述使用註解的整個類的信息,而且其中定義了List<BindViewField>List<OnClickMethod>,分別用來存儲該類中應用註解的字段和方法的信息。

與生成文件和獲取註解的對象信息相關的幾個字段都是從 AbstractProcessor 中獲取的。以下面的代碼所示,咱們能夠從 AbstractProcessor 的 init() 方法的 ProcessingEnvironment 中獲取到 ElementsFilerMessager。它們的做用分別是:Elements 相似於一個工具類,用來從 Element 中獲取註解對象的信息;Filer 用來支持經過註釋處理器建立新文件;Messager 提供註釋處理器用來報告錯誤消息、警告和其餘通知的方式。

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

而後在 AbstractProcessor 的 process() 方法中的 RoundEnvironment 參數中,咱們又能夠獲取到指定註解對應的 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() 方法的時候,會根據傳入的 RoundEnvironment 分別處理兩種註解。兩個註解的相關信息都會被解析成 List<BindViewField>List<OnClickMethod>,而後把使用註解的整個類的信息統一放置在 AnnotatedClass 中。爲了提高程序的效率,這裏用了緩存來存儲類信息。最後,咱們調用了 annotatedClass.generateFinder() 獲取一個JavaFile,並調用它的 writeTo(filer) 方法生成類文件。

上面的代碼重點在於解析使用註解的類的信息,至於如何根據類信息生成類文件,咱們還須要看下 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. 而後,咱們須要實現 AbstractProcessor ,覆寫各個方法,註冊,並在 process 方法中完成生成類文件的操做。

2.3 使用註解替換枚舉

註解常見的第三種使用方式是用來取代枚舉的。由於枚舉相比於普通的字符串或者整數會帶來額外的內存佔用,所以對於 Android 這種對內存要求比較高的項目而言就須要對枚舉進行優化。固然,咱們使用字符串常量或者整數常量替換枚舉就能夠了,可是這種方式的參數能夠接受任意字符串和整型的值。假如咱們但願可以像枚舉同樣對傳入的參數的範圍進行限制,就須要使用枚舉了!

好比,咱們須要對相機的閃光燈參數進行限制,每一個參數經過一個整型的變量指定。而後,咱們經過一個方法接受整型的參數,並經過註解來要求指定的整型必須在咱們上述聲明的整型範圍以內。咱們能夠這樣定義,

首先,咱們定義一個類 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;
    }
}
複製代碼

這樣當咱們傳入的參數不在咱們自定義枚舉的 @IntDef 指定的範圍以內的時候,IDE 會自動給出提示。

三、總結

以上就是註解的兩種比較常見的使用方式。第一種是經過反射來進行的,由於反射自己的效率比較低,因此比較適用於發射比較少的場景;第二種方式是在編譯期間經過編譯器生成代碼來實現的,相比於第一種,它仍是可能會用到反射的,可是沒必要在運行時對類的每一個方法和字段進行遍歷,於是效率高得多。

以上。

獲取源碼:Android-references

相關文章
相關標籤/搜索