注解學(xué)習(xí)筆記

什么是注解

  • 在Java語(yǔ)法中,使用@符號(hào)作為開(kāi)頭,并在@后面緊跟注解名。被運(yùn)用于類,接口,方法和字段之上。

  • 注解也叫元數(shù)據(jù),是一種代碼級(jí)別的說(shuō)明,與類,接口。枚舉是在用一個(gè)層次上,他可以聲明在包,類,字段,方法,局部變量,方法參數(shù)等的前面,用來(lái)對(duì)這些變量進(jìn)行說(shuō)明,注釋。注解可以提高代碼的可讀性,它可以向編譯器,虛擬機(jī)等解釋說(shuō)明一些事情。降低項(xiàng)目的耦合度,自動(dòng)生成Java代碼,自動(dòng)完成一些規(guī)律性的代碼,減少開(kāi)發(fā)者的工作量。

注解分類

  • Java內(nèi)置注解
  • 元注解
  • 自定義注解
    • 運(yùn)行時(shí)注解
    • 編譯時(shí)注解

注解作用分類

  • 編寫文檔
    • 通過(guò)代碼里標(biāo)識(shí)的元數(shù)據(jù)生成文檔【生成文檔doc文檔】
  • 代碼分析
    • 通過(guò)代碼里標(biāo)識(shí)的元數(shù)據(jù)對(duì)代碼進(jìn)行分析【使用反射】
  • 編譯檢查
    • 通過(guò)代碼里標(biāo)識(shí)的元數(shù)據(jù)讓編譯器能夠?qū)崿F(xiàn)基本的編譯檢查【Override】

Java字段(類成員)和屬性

  • 屬性只局限于類中方法聲明,并不與類中其他的成員相關(guān)

  • Java中的屬性通??梢岳斫鉃間et和set方法;而字段通常叫做類成員

  • 字段通常是在類中定義的類成員變量

元注解(負(fù)責(zé)注解其他的注解)

  • @Target

    • 表示該注解用于什么地方,可能的ElementType參數(shù)包括:
      • CONSTRUCTOR:構(gòu)造器的聲明
      • FIELD:域聲明
      • LOCAL_VARIABLE:局部變量聲明
      • METHOD:方法聲明
      • PACKAGE:包聲明
      • PARAMETER:參數(shù)聲明
      • TYPE:類,接口或enum聲明
  • @Retention

    • 表示在什么級(jí)別保留此信息,可選的RetentionPolicy參數(shù)包括:
      • SOURCE:注解僅存在代碼中,注解會(huì)被編譯器丟棄
      • CLASS:注解會(huì)在class文件中保留,但會(huì)被VM丟棄
      • RUNTIME:VM運(yùn)行期間也會(huì)保留該注解,因此可以通過(guò)反射來(lái)獲得該注解
  • @Documented

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

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

Java內(nèi)置注解

  • @Override,表示當(dāng)前的方法定義將覆蓋超類中的方法,如果出現(xiàn)錯(cuò)誤,編譯器就會(huì)報(bào)錯(cuò)。

    • 當(dāng)我們的子類覆寫父類中的方法的時(shí)候,我們使用這個(gè)注解,這一定程度的提高了程序的可讀性也避免了維護(hù)中的一些問(wèn)題,比如說(shuō),當(dāng)修改父類方法簽名(方法名和參數(shù))的時(shí)候,你有很多個(gè)子類方法簽名也必須修改,否則編譯器就會(huì)報(bào)錯(cuò),當(dāng)你的類越來(lái)越多的時(shí)候,那么這個(gè)注解確實(shí)會(huì)幫上你的忙。如果你沒(méi)有使用這個(gè)注解,那么你就很難追蹤到這個(gè)問(wèn)題。
  • @Deprecated:如果使用此注解,編譯器會(huì)出現(xiàn)警告信息。

    • 一個(gè)棄用的元素(類,方法和字段)在java中表示不再重要,它表示了該元素將會(huì)被取代或者在將來(lái)被刪除。
      當(dāng)我們棄用(deprecate)某些元素的時(shí)候我們使用這個(gè)注解。所以當(dāng)程序使用該棄用的元素的時(shí)候編譯器會(huì)彈出警告。當(dāng)然我們也需要在注釋中使用@deprecated標(biāo)簽來(lái)標(biāo)示該注解元素。
  • @SuppressWarnings:忽略編譯器的警告信息

    • 當(dāng)我們想讓編譯器忽略一些警告信息的時(shí)候,我們使用這個(gè)注解。比如在下面這個(gè)示例中,我們的deprecatedMethod()方法被標(biāo)記了@Deprecated注解,所以編譯器會(huì)報(bào)警告信息,但是我們使用了@SuppressWarnings("deprecation")也就讓編譯器不在報(bào)這個(gè)警告信息了

自定義注解

  • 運(yùn)行時(shí)注解大多數(shù)時(shí)候?qū)崟r(shí)運(yùn)行時(shí)使用反射來(lái)實(shí)現(xiàn)所需效果,這很大程度上影響效率
  • 編譯時(shí)注解在編譯時(shí)生成對(duì)應(yīng)Java代碼實(shí)現(xiàn)代碼注入

自定義注解實(shí)現(xiàn)及使用

自定義注解使用@interface來(lái)聲明一個(gè)注解。創(chuàng)建一個(gè)自定義注解遵循: public @interface 注解名 {方法參數(shù)}

自定義注解示例一

@Documented
@Target(ElementType.METHOD)
@Inherited                                                                                                                                                                                                                                                                                                                                                                           @Retention(RetentionPolicy.RUNTIME)
public @interface Annotation{                                                                                                                                 
    int studentAge() default 18;   //定義默認(rèn)值
    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) {
          }
        }
      }
}

編譯時(shí)注解

說(shuō)到編譯時(shí)注解,就不得不說(shuō)注解處理器 AbstractProcessor,如果你有注意,一般第三方注解相關(guān)的類庫(kù)(基于注解的框架),如bufferKnike、ARouter,都有一個(gè)Compiler命名的Module,如下圖X2.3,這里面一般都是注解處理器,用于編譯時(shí)處理對(duì)應(yīng)的注解。

注解處理器(Annotation Processor)是javac的一個(gè)工具,它用來(lái)在編譯時(shí)掃描和處理注解(Annotation)。你可以對(duì)自定義注解注冊(cè)相應(yīng)的注解處理器,用于處理注解邏輯。

javac是收錄于JDK中的Java語(yǔ)言編譯器。該工具可以將后綴名為.java的源文件編譯為后綴名為.class的可以運(yùn)行于Java虛擬機(jī)的字節(jié)碼。

注解處理器

實(shí)現(xiàn)一個(gè)自定義注解處理器,至少重寫四個(gè)方法,并注冊(cè)你的Processor(為自定義注解注冊(cè)相應(yīng)的注解處理器,用于處理注解邏輯)

  • @AutoService(Processor.class),谷歌提供的自動(dòng)注冊(cè)注解,為你生成注冊(cè)Processor所需要的格式文件(com.google.auto相關(guān)包)。
  • init(ProcessingEnvironment env),初始化處理器,一般在這里獲取我們需要的工具類。
  • getSupportedAnnotationTypes(),指定注解處理器是注冊(cè)給哪個(gè)注解的,返回指定支持的注解類集合。
  • getSupportedSourceVersion() ,指定java版本。
  • process(),處理器實(shí)際處理邏輯入口。
注解處理器基本代碼

init()方法傳入一個(gè)參數(shù)processingEnv,可以幫助我們?nèi)コ跏蓟恍┹o助類:

  • Filer mFileUtils; 跟文件相關(guān)的輔助類,生成JavaSourceCode.
  • Elements mElementUtils;跟元素相關(guān)的輔助類,幫助我們?nèi)カ@取一些元素相關(guān)的信息。
  • Messager mMessager;跟日志相關(guān)的輔助類。
注解處理器一般處理邏輯

1、遍歷得到源碼中,需要解析的元素列表。
2、判斷元素是否可見(jiàn)和符合要求。
3、組織數(shù)據(jù)結(jié)構(gòu)得到輸出類參數(shù)。
4、輸入生成Java文件。
5、錯(cuò)誤處理。

Processor處理過(guò)程中,會(huì)掃描全部Java源碼,代碼的每一個(gè)部分都是一個(gè)特定類型(比如類、變量、方法)的Element,它們像是XML一層的層級(jí)機(jī)構(gòu),比如類、變量、方法等,每個(gè)Element代表一個(gè)靜態(tài)的、語(yǔ)言級(jí)別的構(gòu)件。

Element代表的是源代碼,而TypeElement代表的是源代碼中的類型元素,例如類。然而,TypeElement并不包含類本身的信息。你可以從TypeElement中獲取類的名字,但是你獲取不到類的信息,例如它的父類。這種信息需要通過(guò)TypeMirror獲取。你可以通過(guò)調(diào)用elements.asType()獲取元素的TypeMirror。

Element 相關(guān)子類

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

如何編寫基于編譯時(shí)注解的項(xiàng)目

在Android應(yīng)用開(kāi)發(fā)中,我們常常為了提升開(kāi)發(fā)效率會(huì)選擇使用一些基于注解的框架,但是由于反射造成一定運(yùn)行效率的損耗,所以我們會(huì)更青睞于編譯時(shí)注解的框架,例如:

  • ButterKnife免去我們編寫View的初始化以及事件的注入的代碼。
  • EventBus3方便我們實(shí)現(xiàn)組建間通訊。
  • Fragmentargs輕松的為Fragment添加參數(shù)信息,并提供創(chuàng)建方法。
  • ParcelableGenerator可實(shí)現(xiàn)自動(dòng)將任意對(duì)象轉(zhuǎn)換為Parcelable類型,方便對(duì)象傳輸。

項(xiàng)目結(jié)構(gòu)劃分

在編寫此類框架的時(shí)候,一般需要建立多個(gè)module,例如:

  • xxx-annotation 用于存放注解等,Java模塊
  • xxx-compiler 用于編寫注解處理器,Java模塊
  • xxx-api 用于給用戶提供使用的API,本例為Andriod模塊
  • xxx-sample 示例,本例為Andriod模塊

注解處理器只需要在編譯的時(shí)候使用,并不需要打包到APK中。因此為了用戶考慮,我們需要將注解處理器分離為單獨(dú)的module。

對(duì)于module間的依賴,因?yàn)榫帉懽⒔馓幚砥餍枰蕾囅嚓P(guān)注解,所以:
ioc-compiler依賴ioc-annotation>。我們?cè)谑褂玫倪^(guò)程中,會(huì)用到注解以及相關(guān)API。所以ioc-sample依賴ioc-api;ioc-api依賴ioc-annotation

注解模塊的實(shí)現(xiàn)

注解模塊,主要用于存放一些注解類。

注解處理器的實(shí)現(xiàn)

實(shí)現(xiàn)一個(gè)注解處理器,至少需要重寫四個(gè)方法。該模塊,我們一般會(huì)依賴注解模塊,以及可以使用一個(gè)auto-service庫(kù),auto-service庫(kù)可以幫我們?nèi)ド蒑ETA-INF等信息。
build.gradle的依賴情況如下:

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

process的實(shí)現(xiàn)

process()注解處理器實(shí)際處理邏輯入口。主要是獲取被注解的參數(shù)列表,組織數(shù)據(jù)結(jié)構(gòu)得到輸出類參數(shù),生成Java文件。process中的實(shí)現(xiàn)一般可以認(rèn)為兩個(gè)步驟:

  • 收集信息
  • 生成代理類(本文把編譯時(shí)生成的類叫代理類)

什么叫收集信息呢?就是根據(jù)你的注解聲明,拿到對(duì)應(yīng)的Element,然后獲取到我們所需要的信息,這個(gè)信息肯定是為了后面生成JavaFileObject所準(zhǔn)備的。
例如本例,我們會(huì)針對(duì)每一個(gè)類生成一個(gè)代理類,例如MainActivity我們會(huì)生成一個(gè)MainActivity$$ViewInjector。那么如果多個(gè)類中聲明了注解,就對(duì)應(yīng)了多個(gè)類,這里就需要:

  • 一個(gè)類對(duì)象,代表具體某個(gè)類的代理類生成的全部信息,本例中為ProxyInfo
  • 一個(gè)集合,存放上述類對(duì)象(到時(shí)候遍歷生成代理類),本例中Map<String, ProxyInfo>,key為類的全路徑。
收集信息

首先調(diào)用mProxyMap.clear(),因?yàn)?code>process可能會(huì)多次調(diào)用,避免生成重復(fù)的代理類,避免生成類的類名已存在異常。

然后,通過(guò)roundEnv.getElementsAnnotatedWith()獲取被@BindView注解的元素,這里返回值,按照我們的預(yù)期應(yīng)該是VariableElement集合,因?yàn)槲覀冇糜诔蓡T變量上。

接下來(lái)for循環(huán)我們的元素,首先檢查類型是否是VariableElement(對(duì)元素列表進(jìn)行額外判斷,校驗(yàn)元素是否可用),然后獲取元素VariableElement對(duì)應(yīng)的類信息TypeElement,繼而生成ProxyInfo對(duì)象。這里先通過(guò)一個(gè)mProxyMap進(jìn)行檢查,keyqualifiedName即類的全路徑,如果沒(méi)有生成才會(huì)去生成一個(gè)新的ProxyInfo實(shí)例,ProxyInfo與類是一一對(duì)應(yīng)的。

接下來(lái),會(huì)將與該類對(duì)應(yīng)的且被@BindView聲明的VariableElement加入到ProxyInfo中去,key為我們聲明時(shí)填寫的id,即View的id。
這樣就完成了信息的收集,收集完成信息后,應(yīng)該就可以去生成代理類了。

生成代理類

遍歷mProxyMap,然后取得每一個(gè)ProxyInfo,最后通過(guò)mFileUtils.createSourceFile()來(lái)創(chuàng)建文件對(duì)象,類名為proxyInfo.getProxyClassFullName(),寫入的內(nèi)容為proxyInfo.generateJavaCode()(生成Java代碼)。

生成Java代碼

ProxyInfo.generateJavaCode()方法通過(guò)收集得到的信息,拼接完成的代理類對(duì)象。也可以使用開(kāi)源庫(kù),例如:javapoet,來(lái)通過(guò)Java API的方式來(lái)生成代碼。javapoet (com.squareup:javapoet)是一個(gè)根據(jù)指定參數(shù),生成java文件的開(kāi)源庫(kù)。

生成的代碼實(shí)現(xiàn)了一個(gè)接口ViewInjector<T>,該接口是為了統(tǒng)一所有的代理類對(duì)象的類型,到時(shí)候我們需要強(qiáng)轉(zhuǎn)代理類對(duì)象為該接口類型,調(diào)用其方法。接口是泛型,主要就是傳入實(shí)際類對(duì)象,例如:MainActivity,因?yàn)槲覀冊(cè)谏纱眍愔械拇a,實(shí)際上就是實(shí)際類.成員變量的方式進(jìn)行訪問(wèn),所以,使用編譯時(shí)注解的成員變量一般都不允許private修飾符修飾(有的允許,但是需要提供getter,setter訪問(wèn)方法)。

API模塊的實(shí)現(xiàn)

有了代理類之后,我們一般還會(huì)提供API供用戶去訪問(wèn)

API一般如何編寫呢?

  • 根據(jù)傳入的host尋找我們生成的代理類:例如:MainActivity->MainActity$$ViewInjector。
  • 強(qiáng)轉(zhuǎn)為統(tǒng)一的接口,調(diào)用接口提供的方法。

這兩件事應(yīng)該不復(fù)雜,第一件事是拼接代理類名,然后反射生成對(duì)象,第二件事強(qiáng)轉(zhuǎn)調(diào)用。拼接代理類的全路徑,然后通過(guò)newInstance生成實(shí)例,然后強(qiáng)轉(zhuǎn),調(diào)用代理類的inject()方法。

ButterKnife工作流程解析

Butter Knife,專門為Android View設(shè)計(jì)的綁定注解,專業(yè)解決各種findViewById。

ButterKnife有哪些優(yōu)勢(shì)?

  1. 強(qiáng)大的View綁定和Click事件處理功能,簡(jiǎn)化代碼,提升開(kāi)發(fā)效率
  2. 方便的處理Adapter里的ViewHolder綁定問(wèn)題
  3. 運(yùn)行時(shí)不會(huì)影響APP效率,使用配置方便
  4. 代碼清晰,可讀性強(qiáng)

ButterKnife工作流程

  1. 開(kāi)始它會(huì)掃描Java代碼中所有的ButterKnife注解@Bind、@OnClick、@OnItemClicked等。
  2. 當(dāng)它發(fā)現(xiàn)一個(gè)類中含有任何一個(gè)注解時(shí), ButterKnifeProcessor會(huì)幫你生成一個(gè)Java類,名字<類名>$$ViewInjector.java,這個(gè)新生成的類實(shí)現(xiàn)了ViewBinder接口。
  3. 這個(gè)ViewBinder類中包含了所有對(duì)應(yīng)的代碼,比如@Bind注解對(duì)應(yīng)findViewById(), @OnClick對(duì)應(yīng)了view.setOnClickListener()等等。
  4. 最后當(dāng)Activity啟動(dòng)ButterKnife.bind(this)執(zhí)行時(shí),ButterKnife會(huì)去加載對(duì)應(yīng)的ViewBinder類調(diào)用它們的bind()方法。

Java注解工作流程

  • 注解是在編譯(Compile)時(shí)期進(jìn)行處理的
  • 注解處理器(Annotation Processor)讀取Java代碼處理相應(yīng)的注解,并且生成對(duì)應(yīng)的代碼
  • 生成的Java代碼被當(dāng)做普通的Java類再次編譯
  • 注解處理器不能修改存在Java輸入文件,也不能對(duì)方法做修改或者添加


    Java編譯流程.png

參考資料

Android注解快速入門和實(shí)用解析

Android 如何編寫基于編譯時(shí)注解的項(xiàng)目

自定義Java注解處理器

ButterKnife框架原理

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容