註解學習筆記

weixin_34120274發表於2017-11-24

什麼是註解

  • 在Java語法中,使用@符號作為開頭,並在@後面緊跟註解名。被運用於類,介面,方法和欄位之上。

  • 註解也叫後設資料,是一種程式碼級別的說明,與類,介面。列舉是在用一個層次上,他可以宣告在包,類,欄位,方法,區域性變數,方法引數等的前面,用來對這些變數進行說明,註釋。註解可以提高程式碼的可讀性,它可以向編譯器,虛擬機器等解釋說明一些事情。降低專案的耦合度,自動生成Java程式碼,自動完成一些規律性的程式碼,減少開發者的工作量。

註解分類

  • Java內建註解
  • 元註解
  • 自定義註解
    • 執行時註解
    • 編譯時註解

註解作用分類

  • 編寫文件
    • 通過程式碼裡標識的後設資料生成文件【生成文件doc文件】
  • 程式碼分析
    • 通過程式碼裡標識的後設資料對程式碼進行分析【使用反射】
  • 編譯檢查
    • 通過程式碼裡標識的後設資料讓編譯器能夠實現基本的編譯檢查【Override】

Java欄位(類成員)和屬性

  • 屬性只侷限於類中方法宣告,並不與類中其他的成員相關

  • Java中的屬性通常可以理解為get和set方法;而欄位通常叫做類成員

  • 欄位通常是在類中定義的類成員變數

元註解(負責註解其他的註解)

  • @Target

    • 表示該註解用於什麼地方,可能的ElementType引數包括:
      • CONSTRUCTOR:構造器的宣告
      • FIELD:域宣告
      • LOCAL_VARIABLE:區域性變數宣告
      • METHOD:方法宣告
      • PACKAGE:包宣告
      • PARAMETER:引數宣告
      • TYPE:類,介面或enum宣告
  • @Retention

    • 表示在什麼級別保留此資訊,可選的RetentionPolicy引數包括:
      • SOURCE:註解僅存在程式碼中,註解會被編譯器丟棄
      • CLASS:註解會在class檔案中保留,但會被VM丟棄
      • RUNTIME:VM執行期間也會保留該註解,因此可以通過反射來獲得該註解
  • @Documented

    • 將註解包含在javadoc中
  • @Inherited

    • 允許子類繼承父類的註解

Java內建註解

  • @Override,表示當前的方法定義將覆蓋超類中的方法,如果出現錯誤,編譯器就會報錯。

    • 當我們的子類覆寫父類中的方法的時候,我們使用這個註解,這一定程度的提高了程式的可讀性也避免了維護中的一些問題,比如說,當修改父類方法簽名(方法名和引數)的時候,你有很多個子類方法簽名也必須修改,否則編譯器就會報錯,當你的類越來越多的時候,那麼這個註解確實會幫上你的忙。如果你沒有使用這個註解,那麼你就很難追蹤到這個問題。
  • @Deprecated:如果使用此註解,編譯器會出現警告資訊。

    • 一個棄用的元素(類,方法和欄位)在java中表示不再重要,它表示了該元素將會被取代或者在將來被刪除。
      當我們棄用(deprecate)某些元素的時候我們使用這個註解。所以當程式使用該棄用的元素的時候編譯器會彈出警告。當然我們也需要在註釋中使用@deprecated標籤來標示該註解元素。
  • @SuppressWarnings:忽略編譯器的警告資訊

    • 當我們想讓編譯器忽略一些警告資訊的時候,我們使用這個註解。比如在下面這個示例中,我們的deprecatedMethod()方法被標記了@Deprecated註解,所以編譯器會報警告資訊,但是我們使用了@SuppressWarnings("deprecation")也就讓編譯器不在報這個警告資訊了

自定義註解

  • 執行時註解大多數時候實時執行時使用反射來實現所需效果,這很大程度上影響效率
  • 編譯時註解在編譯時生成對應Java程式碼實現程式碼注入

自定義註解實現及使用

自定義註解使用@interface來宣告一個註解。建立一個自定義註解遵循: public @interface 註解名 {方法引數}

自定義註解示例一

@Documented
@Target(ElementType.METHOD)
@Inherited                                                                                                                                                                                                                                                                                                                                                                           @Retention(RetentionPolicy.RUNTIME)
public @interface Annotation{                                                                                                                                 
    int studentAge() default 18;   //定義預設值
    String studentName();
    String stuAddress();
    String stuStream() default "CSE";
}
@Annotation(studentName = "Chaitanya", stuAddress = "Agra, India")
public class Class {                                                                                                                                                                   
    ...
}

自定義註解示例二

@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface getViewTo {
    int value() default  -1;
}
public class MainActivity extends AppCompatActivity {

    @getViewTo(R.id.textview)
    private TextView mTv;

    /**
     * 解析註解,獲取控制元件
     */
    private void getAllAnnotationView() {
        //獲得成員變數
        Field[] fields = this.getClass().getDeclaredFields();

        for (Field field : fields) {
          try {
            //判斷註解
            if (field.getAnnotations() != null) {
              //確定註解型別
              if (field.isAnnotationPresent(GetViewTo.class)) {
                //允許修改反射屬性
                field.setAccessible(true);
                GetViewTo getViewTo = field.getAnnotation(GetViewTo.class);
                //findViewById將註解的id,找到View注入成員變數中
                field.set(this, findViewById(getViewTo.value()));
              }
            }
          } catch (Exception e) {
          }
        }
      }
}

編譯時註解

說到編譯時註解,就不得不說註解處理器 AbstractProcessor,如果你有注意,一般第三方註解相關的類庫(基於註解的框架),如bufferKnike、ARouter,都有一個Compiler命名的Module,如下圖X2.3,這裡面一般都是註解處理器,用於編譯時處理對應的註解。

註解處理器(Annotation Processor)是javac的一個工具,它用來在編譯時掃描和處理註解(Annotation)。你可以對自定義註解註冊相應的註解處理器,用於處理註解邏輯

javac是收錄於JDK中的Java語言編譯器。該工具可以將字尾名為.java的原始檔編譯為字尾名為.class的可以執行於Java虛擬機器的位元組碼。

註解處理器

實現一個自定義註解處理器,至少重寫四個方法,並註冊你的Processor(為自定義註解註冊相應的註解處理器,用於處理註解邏輯)

  • @AutoService(Processor.class),谷歌提供的自動註冊註解,為你生成註冊Processor所需要的格式檔案(com.google.auto相關包)。
  • init(ProcessingEnvironment env),初始化處理器,一般在這裡獲取我們需要的工具類。
  • getSupportedAnnotationTypes(),指定註解處理器是註冊給哪個註解的,返回指定支援的註解類集合。
  • getSupportedSourceVersion() ,指定java版本。
  • process(),處理器實際處理邏輯入口。
註解處理器基本程式碼

init()方法傳入一個引數processingEnv,可以幫助我們去初始化一些輔助類:

  • Filer mFileUtils; 跟檔案相關的輔助類,生成JavaSourceCode.
  • Elements mElementUtils;跟元素相關的輔助類,幫助我們去獲取一些元素相關的資訊。
  • Messager mMessager;跟日誌相關的輔助類。
註解處理器一般處理邏輯

1、遍歷得到原始碼中,需要解析的元素列表。
2、判斷元素是否可見和符合要求。
3、組織資料結構得到輸出類引數。
4、輸入生成Java檔案。
5、錯誤處理。

Processor處理過程中,會掃描全部Java原始碼,程式碼的每一個部分都是一個特定型別(比如類、變數、方法)的Element,它們像是XML一層的層級機構,比如類、變數、方法等,每個Element代表一個靜態的、語言級別的構件。

Element代表的是原始碼,而TypeElement代表的是原始碼中的型別元素,例如類。然而,TypeElement並不包含類本身的資訊。你可以從TypeElement中獲取類的名字,但是你獲取不到類的資訊,例如它的父類。這種資訊需要通過TypeMirror獲取。你可以通過呼叫elements.asType()獲取元素的TypeMirror。

Element 相關子類

  • VariableElement //一般代表成員變數
  • ExecutableElement //一般代表類中的方法
  • TypeElement //一般代表代表類
  • PackageElement //一般代表Package

如何編寫基於編譯時註解的專案

在Android應用開發中,我們常常為了提升開發效率會選擇使用一些基於註解的框架,但是由於反射造成一定執行效率的損耗,所以我們會更青睞於編譯時註解的框架,例如:

  • ButterKnife免去我們編寫View的初始化以及事件的注入的程式碼。
  • EventBus3方便我們實現組建間通訊。
  • Fragmentargs輕鬆的為Fragment新增引數資訊,並提供建立方法。
  • ParcelableGenerator可實現自動將任意物件轉換為Parcelable型別,方便物件傳輸。

專案結構劃分

在編寫此類框架的時候,一般需要建立多個module,例如:

  • xxx-annotation 用於存放註解等,Java模組
  • xxx-compiler 用於編寫註解處理器,Java模組
  • xxx-api 用於給使用者提供使用的API,本例為Andriod模組
  • xxx-sample 示例,本例為Andriod模組

註解處理器只需要在編譯的時候使用,並不需要打包到APK中。因此為了使用者考慮,我們需要將註解處理器分離為單獨的module。

對於module間的依賴,因為編寫註解處理器需要依賴相關注解,所以:
ioc-compiler依賴ioc-annotation>。我們在使用的過程中,會用到註解以及相關API。所以ioc-sample依賴ioc-apiioc-api依賴ioc-annotation

註解模組的實現

註解模組,主要用於存放一些註解類。

註解處理器的實現

實現一個註解處理器,至少需要重寫四個方法。該模組,我們一般會依賴註解模組,以及可以使用一個auto-service庫,auto-service庫可以幫我們去生成META-INF等資訊。
build.gradle的依賴情況如下:

dependencies {
    compile 'com.google.auto.service:auto-service:1.0-rc2'
    compile project (':ioc-annotation')
}

process的實現

process()註解處理器實際處理邏輯入口。主要是獲取被註解的引數列表,組織資料結構得到輸出類引數,生成Java檔案。process中的實現一般可以認為兩個步驟:

  • 收集資訊
  • 生成代理類(本文把編譯時生成的類叫代理類)

什麼叫收集資訊呢?就是根據你的註解宣告,拿到對應的Element,然後獲取到我們所需要的資訊,這個資訊肯定是為了後面生成JavaFileObject所準備的。
例如本例,我們會針對每一個類生成一個代理類,例如MainActivity我們會生成一個MainActivity$$ViewInjector。那麼如果多個類中宣告瞭註解,就對應了多個類,這裡就需要:

  • 一個類物件,代表具體某個類的代理類生成的全部資訊,本例中為ProxyInfo
  • 一個集合,存放上述類物件(到時候遍歷生成代理類),本例中Map<String, ProxyInfo>,key為類的全路徑。
收集資訊

首先呼叫mProxyMap.clear(),因為process可能會多次呼叫,避免生成重複的代理類,避免生成類的類名已存在異常。

然後,通過roundEnv.getElementsAnnotatedWith()獲取被@BindView註解的元素,這裡返回值,按照我們的預期應該是VariableElement集合,因為我們用於成員變數上。

接下來for迴圈我們的元素,首先檢查型別是否是VariableElement(對元素列表進行額外判斷,校驗元素是否可用),然後獲取元素VariableElement對應的類資訊TypeElement,繼而生成ProxyInfo物件。這裡先通過一個mProxyMap進行檢查,keyqualifiedName即類的全路徑,如果沒有生成才會去生成一個新的ProxyInfo例項,ProxyInfo與類是一一對應的。

接下來,會將與該類對應的且被@BindView宣告的VariableElement加入到ProxyInfo中去,key為我們宣告時填寫的id,即View的id。
這樣就完成了資訊的收集,收集完成資訊後,應該就可以去生成代理類了。

生成代理類

遍歷mProxyMap,然後取得每一個ProxyInfo,最後通過mFileUtils.createSourceFile()來建立檔案物件,類名為proxyInfo.getProxyClassFullName(),寫入的內容為proxyInfo.generateJavaCode()(生成Java程式碼)

生成Java程式碼

ProxyInfo.generateJavaCode()方法通過收集得到的資訊,拼接完成的代理類物件。也可以使用開源庫,例如:javapoet,來通過Java API的方式來生成程式碼。javapoet (com.squareup:javapoet)是一個根據指定引數,生成java檔案的開源庫。

生成的程式碼實現了一個介面ViewInjector<T>,該介面是為了統一所有的代理類物件的型別,到時候我們需要強轉代理類物件為該介面型別,呼叫其方法。介面是泛型,主要就是傳入實際類物件,例如:MainActivity因為我們在生成代理類中的程式碼,實際上就是實際類.成員變數的方式進行訪問,所以,使用編譯時註解的成員變數一般都不允許private修飾符修飾(有的允許,但是需要提供getter,setter訪問方法)。

API模組的實現

有了代理類之後,我們一般還會提供API供使用者去訪問

API一般如何編寫呢?

  • 根據傳入的host尋找我們生成的代理類:例如:MainActivity->MainActity$$ViewInjector
  • 強轉為統一的介面,呼叫介面提供的方法。

這兩件事應該不復雜,第一件事是拼接代理類名,然後反射生成物件,第二件事強轉呼叫。拼接代理類的全路徑,然後通過newInstance生成例項,然後強轉,呼叫代理類的inject()方法。

ButterKnife工作流程解析

Butter Knife,專門為Android View設計的繫結註解,專業解決各種findViewById。

ButterKnife有哪些優勢?

  1. 強大的View繫結和Click事件處理功能,簡化程式碼,提升開發效率
  2. 方便的處理Adapter裡的ViewHolder繫結問題
  3. 執行時不會影響APP效率,使用配置方便
  4. 程式碼清晰,可讀性強

ButterKnife工作流程

  1. 開始它會掃描Java程式碼中所有的ButterKnife註解@Bind、@OnClick、@OnItemClicked等。
  2. 當它發現一個類中含有任何一個註解時, ButterKnifeProcessor會幫你生成一個Java類,名字<類名>$$ViewInjector.java,這個新生成的類實現了ViewBinder介面。
  3. 這個ViewBinder類中包含了所有對應的程式碼,比如@Bind註解對應findViewById(), @OnClick對應了view.setOnClickListener()等等。
  4. 最後當Activity啟動ButterKnife.bind(this)執行時,ButterKnife會去載入對應的ViewBinder類呼叫它們的bind()方法。

Java註解工作流程

  • 註解是在編譯(Compile)時期進行處理的
  • 註解處理器(Annotation Processor)讀取Java程式碼處理相應的註解,並且生成對應的程式碼
  • 生成的Java程式碼被當做普通的Java類再次編譯
  • 註解處理器不能修改存在Java輸入檔案,也不能對方法做修改或者新增


    3155067-280fd162d7e3779d.png
    Java編譯流程.png

參考資料

Android註解快速入門和實用解析

Android 如何編寫基於編譯時註解的專案

自定義Java註解處理器

ButterKnife框架原理

相關文章