通常的,註解在 Android 中有兩種應用方式,一種方式是基於反射的,即在程序的運行期間獲取類信息進行反射調用;另外一種是使用註解處理,在編譯期間生成許多代碼,而後在運行期間經過調用這些代碼來實現目標功能。java
在本篇文章中,咱們會先重溫一下 Java 的註解相關的知識,而後分別介紹一下上面兩種方式的實際應用。android
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"; } 複製代碼
這是一個普通的註解的定義。從上面咱們也能夠總結出,在定義註解的時候,有如下幾個地方須要注意:數據庫
@interface
聲明而且指定註解的名稱;default
爲指定的元素指定一個默認值,若是用戶沒有爲其指定值,就使用默認值。好的,看完了一個基本的註解的定義,咱們來看一下上面用到的 Java 元註解的含義。api
@Target
用來指定註解可以修飾的對象的類型。由於 @Target
自己也是一個註解,因此你能夠在源碼中查看它的定義。該註解接收的參數是一個 ElementType
類型的數組,因此,就是說咱們自定義的註解能夠應用到多種類型的對象,而對象的類型由 ElementType
定義。ElementType
是一個枚舉,它的枚舉值以下:數組
因此,好比根據上面的內容,咱們能夠直到咱們的自定義註解 @UseCase
只能應用於方法和字段。緩存
用來指定註解的保留策略,好比有一些註解,當你在本身的代碼中使用它們的時候你會把它寫在方法上面,可是當你反編譯以後卻發現這些註解不在了;而有些註解反編譯以後依然存在,發生這種狀況的緣由就是在使用該註解的時候指定了不一樣的參數。markdown
與 @Target
相同的是這個註解也使用枚舉來指定值的類型,不一樣的是它只能指定一個值,具體能夠看源碼。這裏它使用的是 RetentionPolicy
枚舉,它的幾個值的含義以下:app
當咱們在 Android 中使用註解的時候,一種是在運行時使用的,因此咱們要用 RUNTIME
;另外一種是在編譯時使用的,因此咱們用 CLASS
。
這三個元註解的功能比較簡單和容易理解,這裏咱們一塊兒給出便可:
@Documented
表示此註解將包含在 javadoc 中;@Inherited
表示容許子類繼承父類的註解;@Repeatable
是 Java8 中新增的註解,表示指定的註解能夠重複應用到指定的對象上面。上文,咱們回顧了 Java 中註解相關的知識點,相信你已經對註解的內容有了一些瞭解,那麼咱們接下來看一下註解在實際開發中的兩種應用方式。
在我開始爲個人開源項目 馬克筆記 編寫數據庫的時候,我考慮了使用註解來爲數據庫對象指定字段的信息,並根據這心信息來拼接出建立數據庫表的 SQL 語句。當時也想用反射來動態爲每一個字段賦值的,可是考慮到反射的性能比較差,最終放棄了這個方案。可是,使用註解處理的方式能夠完美的解決咱們的問題,即在編譯的時候動態生成一堆代碼,實際賦值的時候調用這些方法來完成。這先後兩種方案就是咱們今天要講的註解的兩種使用方式。
這裏爲了演示基於反射的註解的使用方式,咱們寫一個小的 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()
方法的結果來肯定這個方法和字段是否使用了指定類型的註解。
上面的代碼能夠解決一些問題,但同時,咱們還有一些地方須要注意:
也許你以前已經使用過 ButterKnife 這樣的注入框架,不知道你是否記得在 Gradle 中引用它的時候加入了下面這行依賴:
annotationProcessor 'com.jakewharton:butterknife-compiler:8.8.1' 複製代碼
這裏的 annotationProcessor 就是咱們這裏要講的註解處理。本質上它會在編譯的時候,在你調用 ButterKnife.bind(this);
方法的那個類所在的包下面生成一些類,當調用 ButterKnife.bind(this);
的時候實際上就完成了爲使用註解的方法和控件綁定的過程。也就是,本質上仍是調用了 findViewById()
,只是這個過程被隱藏了,不用你來完成了,僅此而已。
下面,咱們就使用註解處理的功能來製做一個相似於 ButterKnife 的簡單庫。不過,在那以前咱們還須要作一些準備——一些知識點須要進行說明。即 Javapoet和AbstractProcessor
。
Javapoet 是一個用來生成 .java
文件的 Java API,由 Square 開發,你能夠在它的 Github 主頁中瞭解它的基本使用方法。它的好處就是對方法、類文件和代碼等的拼接進行了封裝,有了它,咱們就不用再按照字符串的方式去拼接出一段代碼了。相比於直接使用字符串的方式,它還能夠生成代碼的同時直接 import
對應的引用,能夠說是很是方便、快捷的一個庫了。
這裏的 AbstractProcessor
是用來生成類文件的核心類,它是一個抽象類,通常使用的時候咱們只要覆寫它的方法中的4個就能夠了。下面是這些方法及其定義:
init
:在生成代碼以前被調用,能夠從它參數 ProcessingEnvironment
獲取到很是多有用的工具類;process
:用於生成代碼的 Java 方法,能夠從參數 RoundEnvironment
中獲取使用指定的註解的對象的信息,幷包裝成一個 Element
類型返回;getSupportedAnnotationTypes
:用於指定該處理器適用的註解;getSupportedSourceVersion
:用來指定你使用的 Java 的版本。這幾個方法中,除了 process
,其餘方法都不是必須覆寫的方法。這裏的 getSupportedAnnotationTypes
和 getSupportedSourceVersion
可使用注 @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出的文件來找到生成的文件。
在定製以前,咱們先看一下程序的最終執行結果,也許這樣會更有助於理解整個過程的原理。咱們程序的最終的執行結果是,在編譯的時候,在使用咱們的工具的類的相同級別的包下面生成一個類。以下圖所示:
這裏的 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,看完了程序的最終結果,咱們來看一下如何生成上面的那個類文件。
首先,咱們須要定義註解用來提供給用戶進行事件和控件的綁定,
@Target(ElementType.FIELD) @Retention(RetentionPolicy.CLASS) public @interface BindView { int id(); } @Target(ElementType.METHOD) @Retention(RetentionPolicy.CLASS) public @interface OnClick { int[] ids(); } 複製代碼
如上面的代碼所示,能夠看出咱們分別用了 ElementType.FIELD
和 ElementType.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 中獲取這些信息並將它們封裝成一個對象來方便咱們調用。因而就產生了 BindViewField
和 OnClickMethod
兩個類。它們分別用來描述使用 @BindView
註解和使用 @OnClick
註解的對象的信息。此外,還有一個 AnnotatedClass
,它用來描述使用註解的整個類的信息,而且其中定義了List<BindViewField>
和 List<OnClickMethod>
,分別用來存儲該類中應用註解的字段和方法的信息。
與生成文件和獲取註解的對象信息相關的幾個字段都是從 AbstractProcessor 中獲取的。以下面的代碼所示,咱們能夠從 AbstractProcessor 的 init()
方法的 ProcessingEnvironment
中獲取到 Elements
、Filer
和 Messager
。它們的做用分別是: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)
方法生成類文件。
上面的代碼重點在於解析使用註解的類的信息,至於如何根據類信息生成類文件,咱們還須要看下 AnnotatedClass
的 generateFinder()
方法,其代碼以下所示。這裏咱們用了以前提到的 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 ,咱們只須要在 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!"); } } 複製代碼
這裏有幾個地方須要注意:
protected
,由於咱們使用了直接引用的方式,而生成的文件和上面的類包相同,因此至少應該保證包級別訪問權限;這裏咱們總結一下按照第二種方式使用註解的時候須要步驟:
註解常見的第三種使用方式是用來取代枚舉的。由於枚舉相比於普通的字符串或者整數會帶來額外的內存佔用,所以對於 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