Java 註解及其在 Android 中的應用

WngShhng發表於2018-08-26

一般的,註解在 Android 中有兩種應用方式,一種方式是基於反射的,即在程式的執行期間獲取類資訊進行反射呼叫;另一種是使用註解處理,在編譯期間生成許多程式碼,然後在執行期間通過呼叫這些程式碼來實現目標功能。

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

1、Java 註解回顧

1. Java 註解的基礎知識

Java 中的註解分成標準註解和元註解。標準註解是 Java 為我們提供的預定義的註解,共有四種:@Override@Deprecated@SuppressWarnnings@SafeVarags。元註解是用來提供給使用者自定義註解用的,共有五種(截止到Java8):@Target@Retention@Documented@Inherited@Repeatable,這裡我們重點介紹這五種元註解。

不過,首先我們還是先看一下一個基本的註解的定義的規範。下面我們自定義了一個名為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 本身也是一個註解,所以你可以在原始碼中檢視它的定義。該註解接收的引數是一個 ElementType 型別的陣列,所以,就是說我們自定義的註解可以應用到多種型別的物件,而物件的型別由 ElementType 定義。ElementType 是一個列舉,它的列舉值如下:

  • TYPE:類、介面或者enum宣告
  • FIELD:域宣告,包括enum例項
  • METHOD:方法宣告
  • PARAMETER:引數宣告
  • CONSTRUCTOR:構造器宣告
  • LOCAL_VARIABLE:區域性變數宣告
  • ANNOTATION_TYPE:註解宣告
  • PACKAGE:包宣告
  • TYPE_PARAMETER:型別引數宣告
  • TYPE_USE:使用型別

所以,比如根據上面的內容,我們可以直到我們的自定義註解 @UseCase 只能應用於方法和欄位。

@Retention

用來指定註解的保留策略,比如有一些註解,當你在自己的程式碼中使用它們的時候你會把它寫在方法上面,但是當你反編譯之後卻發現這些註解不在了;而有些註解反編譯之後依然存在,發生這種情況的原因就是在使用該註解的時候指定了不同的引數。

@Target 相同的是這個註解也使用列舉來指定值的型別,不同的是它只能指定一個值,具體可以看原始碼。這裡它使用的是 RetentionPolicy 列舉,它的幾個值的含義如下:

  • SOURCE:註解將被編譯器丟棄
  • CLASS:註解在class檔案中使用,但會被JVM丟棄
  • RUNTIME:VM將在執行期保留註解,故可以通過反射讀取註解的資訊

當我們在 Android 中使用註解的時候,一種是在執行時使用的,所以我們要用 RUNTIME;另一種是在編譯時使用的,所以我們用 CLASS

@Documented、@Inherited 和 @Repeatable

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

  • @Documented 表示此註解將包含在 javadoc 中;
  • @Inherited 表示允許子類繼承父類的註解;
  • @Repeatable 是 Java8 中新增的註解,表示指定的註解可以重複應用到指定的物件上面。

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

2、註解的兩種使用方式

在我開始為我的開源專案 馬克筆記 編寫資料庫的時候,我考慮了使用註解來為資料庫物件指定欄位的資訊,並根據這心資訊來拼接出建立資料庫表的 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 會自動給出提示。

3、總結

以上就是註解的兩種比較常見的使用方式。第一種是通過反射來進行的,因為反射本身的效率比較低,所以比較適用於發射比較少的場景;第二種方式是在編譯期間通過編譯器生成程式碼來實現的,相比於第一種,它還是可能會用到反射的,但是不必在執行時對類的每個方法和欄位進行遍歷,因而效率高得多。

以上。

獲取原始碼:Android-references

相關文章