Demo GitHub地址:https://github.com/lg2179/AnnotationDemo
案例簡述
在我們Android項目中很多第三方庫都用到了註解,像我們項目中最常用的butterKnife以及eventBusy以及Retrofit都是以註解爲基礎進行使用的,通過使用註解能很大程度上節省代碼,但同樣註解也很容易讓初學者產生困惑,特別是在使用這類項目的時候,有時候出現錯誤會難以調試,主要原因還是很多人並不瞭解這類框架其內部的原理,所以遇到問題時會消耗大量的時間去排查。其次我們如果有好的想法,發現某些代碼需要重複創建,我們也可以自己來寫個框架方便自己日常的編碼,提升編碼效率;最後也算是自身技術的提升,本文我們介紹一下註解的原理以及使用過程,以及自己編寫一個View注入的框架來減少我們編寫頁面時重複使用的findViewById()方法;
概念介紹
1.註解定義
註解(Annotation),也叫元數據。一種代碼級別的說明。它是JDK1.5及以後版本引入的一個特性,與類、接口、枚舉是在同一個層次。它可以聲明在包、類、字段、方法、局部變量、方法參數等的前面,用來對這些元素進行說明,註釋。
初學者可以這樣理解註解:想像代碼具有生命,註解就是對於代碼中某些鮮活個體的貼上去的一張標籤。簡化來講,註解如同一張標籤。
2.註解作用
1、生成文檔。這是最常見的,也是java 最早提供的註解。常用的有@param @return 等
2、跟蹤代碼依賴性,實現替代配置文件功能。比如Dagger 2依賴注入,未來java開發,將大量註解配置,具有很大用處;
3、在編譯時進行格式檢查。如@override 放在方法前,如果你這個方法並不是覆蓋了超類方法,則編譯時就能檢查出。
3.元註解
Java.lang.annotation提供了四種元註解,專門註解其他的註解(在自定義註解的時候需要使用到元註解):
(1)@Documented 註解是否將包含在JavaDoc中
(2)@Retention 什麼時候使用該註解,
RetentionPolicy.SOURCE:在編譯階段丟棄,這些註解在編譯結束之後就不再有任何意義,所以他們不會寫入字節碼,像@overrid這類註解。
RetentionPolicy.CLASS:在類加載的時候被丟棄,在字節碼文件的處理中有用,註解默認使用這種方式,
RetentionPolicy.RUNTIME:始終不會丟棄,運行期也保留該注 解,因此可以使用反射機制讀取該註解的信息。運行時期使用反射可能會影響性能。
(3)@Target 表示該註解用於什麼地方,默認值爲任何元素,表示該註解用於任何地方,可用的ElementType參數包括:
ElementType.CONSTRUCTOR:用於描述構造器
ElementType.FIELD:成員變量,對象,屬性(包括enum實例)
ElementType.LOCAL_VARIBALE:用於描述局部變量
ElementType.METHOD:用於描述方法
ElementType.PACKAGE:用於描述包
ElementType.PARAMTER:用於描述參數
ElementType.TYPE:用於描述類,接口(包括註解類型)活enumeration聲明
(4)@Inherited 定義該註解和子類的關係,如果一個使用了@Inherited修飾的annotation類型被用於一個class,則這個annotation將被用於該class的子類
4.自定義註解
自定義註解類編寫需要注意的一些規則:
1.Annotation型定義爲@interface,所有的Annotation會自動繼承java.lang.Annotation這個接口,並且不能再去集成別的類或是接口。
2.參數成員只能用public或default這兩個訪問權限修飾符。
3.參數成員只能用基本類型byte,short,char,int,long,float,double,boolean八種基本數據類型和String,Enum, Calss, annotation等數據類型以及一些類型的數組。
4.要獲取類方法和字段的註解信息,必須通過Java的反射技術來獲取Annotation對象,因爲除此之外沒有其他的獲取註解對象的方法。
5.自定義註解需要使用到元註解。
案例分析
編寫前的準備
在編寫註解處理器的時候一般是需要建立多個module來作爲功能區分,我們編寫的這個View注入的例子是這樣來劃分功能的。
圖1.模塊劃分
apt_annotation 用於存放註解
apt_compiler 用於存放編寫的註解處理器
apt_api 註解需要作爲api提供給外部使用
既然有module那麼使用就需要有依賴,因爲編寫註解處理器需要使用到我們定義的註解,所以apt_compiler需要依賴apt_annotation,我們app模塊需要使用到該註解處理器提供出來給外部使用的api,所以app需要依賴apt_api,apt_api同樣使用到註解所以需要依賴apt_annotation;
註解類編寫
註解模塊主要用於存放一些註解類,也就是我們自己編寫的註解類BindView。本文編寫的View注入所以定義一個註解類即可。
圖2.註解類
我們編寫的是View注入,所以只需要傳入View的id即可,用於成員變量View,所以@Target此處爲ElementType.FIELD,並且是保留到編譯時期,所以使用RetentionPolicy.CLASS,因爲id是int型,並且我們只需要獲取到id的值即可,所以使用int value()來進行設置。
方法詳解
編寫完註解類之後就需要編寫註解處理器來處理該註解,這裏是比較複雜的,講解之前需要先導入一個auto-service的第三方庫,如果不使用這個庫也可以,但是需要手動去META-INF中編寫文件對註解進行設置。
圖3.導入依賴
如果我們不使用auto-service第三方庫就需要手動編寫下圖這個文件。
圖4.META-INF文件
我們編寫的是,不會對性能有任何影響的:編譯時註解。也有人叫它代碼生成,其實他們還是有些區別的,在編譯時對註解做處理,通過註解,獲取必要信息,在項目中生成代碼,運行時調用,和直接運行手寫代碼沒有任何區別。而更準確的叫法:APT - Annotation Processing Tool。
首先我們瞭解一下註解處理器的基本使用方法:
自定義註解處理器必須繼承AbstractProcessor抽象類,這個類有一個重要的方法process();除此之外還有三個方法需要我們關注包括init(),getSupportedSpurceVersion()以及getSupportedAnnotationTypes();
我們先看process()方法所有的註解處理都是從process()方法開始,你可以理解爲,當APT找到所有需要處理的註解後,會回調這個方法,通過這個方法的參數,能夠拿到所需要的信息。這個方法有兩個參數我們先看一下。
圖5.process()方法參數
參數Set<? Extents TypeElement> annotations,這個參數代表返回所有的由該Processor處理,並且待處理的註解Annotations。
我們先來介紹一下註解處理器處理過程,註解處理過程是一個有序的循環過程,每次循環中,一個處理器要求去處理那些在上一次循環中產生的源文件和類文件中的註解,第一次循環的輸入是運行此方法的初始化輸入,是默認產生的,這些初始輸入,可以看成是虛擬的第0次循環的輸出,也就是說我們事先的process方法有可能會被調用多次,因爲我們生產的文件也有可能會包含我們process中設置接收的註解,例如我們項目中MainActivity第一次循環之後會調用process方法處理我們指定的註解並且產生一個MainActivity_BindView(該註解處理器自定義生成的文件在app->build->generated->source->apt文件夾下面),這個時候就會再一次循環輸入MainActivity_BindView,如果MainActivity_BindView裏面也包含有註解(比如我們在生成MainActivity_BindView時在裏面手動加上Retrofit的註解,但是這個註解必須是getSupportAnnotationTypes中支持的)那麼會再次調用process方法來處理這個註解(處理這個註解的前提是需要getSupportedAnnotationTypes()方法指定了該註解),這次輸入的輸出沒有產生新文件,第三次輸入爲空,輸出爲空。
介紹了過程那麼這兩個參數就很好理解了,第一個就是我們指定返回的註解元素,第二個就是上一次循環的信息和環境。返回值表示這些註解是否由此Processor聲明,如果返回true,則這些註解已聲明並且不再要求後續Processor處理他們,如果返回false,則這些註解未聲明並且可能要求後續Processor處理他們。
代碼編寫過程
初始化設置
註解處理器一般是繼承與AbstractProcessor,一般這裏有四個方法是固定的格式,我們看一下代碼:
圖6.Processor代碼
在實現AbstractProcessor後,process()方法時必須實現的,也是我們編寫代碼的最重要部分,我們先看一般來說比較格式化書寫的三個方法。我們首先會複寫init()方法,這個方法傳入了一個processingEnv參數,這個參數可以幫助我們初始化一些重要的變量,mFiler是後面用來創建生成的java文件的輔助類,mMessager用來打印日誌,mElementUtls是跟元素相關的輔助類,通過這個類我們可以獲取到元素相關的信息,比如可以通過代表成員變量的元素來獲取到代表包的元素。
Process實現
Process的實現,會複雜很多,一般爲了便於記憶我們將他們分爲兩個部分
- 收集信息
- 生成代理文件(編譯時生成的文件)
收集信息其實就是根據你的註解聲明,拿到對應的元素Element獲取到我們所需要的信息,這個信息肯定是爲了後面生成Java文件所準備的。在我們編寫的代碼中我們會對每一個使用到註解的Activity生成一個代理類,例如MainActivity我們會生成一個MainActivity_ViewBinding代理類,SeconActivity我們會生成一個SecondActivity_ViewBinding代理類,那麼如果多個類中聲明瞭註解,就對應多個代理類,那麼就需要:
- 一個類對象,代表具體某個類的代理類生成的全部信息,本例中是ClassCreatorProxy.
- 一個集合,存放上述類對象(到時候遍歷生成代理類),本例中爲Map<String,ClassCreatorProxy>
我們先來看一下收集信息的的過程:
收集信息:
圖7.收集信息
文中對每一行代碼的註釋已經解釋的很清楚了,我們簡單介紹一下這個過程,首先我們會調用一下mProxyMap.clear();因爲process可能會多次調用所以爲了避免生成重複的代理類,我們會先清空一下這個存放代理類的集合,然後通過roundEnv.getElementAnnotatedWith()拿到我們通過@BindView註解的元素,這裏返回值因爲我們提前知道是用於變量上的所以就是VariableElement集合。
接下來for循環我們的元素,然後拿到對應的類信息TypeElement,繼而生成ClassCreatorProxy對象,這裏通過一個mProxyMap進行檢查,key爲fullClassName即類的全路徑,如果沒有生成纔會去生成一個新的,ClassCreatorProxy與類是一一對應。
接下來,會將與該類對應的且被@BindView聲明的VariableElement加入到ClassCreatorProxy中去,key爲我們聲明時填寫的Id,即View的id。這樣就完成了信息的收集,收集完成信息後,應該就可以去生成代理類了。
生成代理類
圖8.生成代理類
可以看到生成代理類的代碼非常的簡短,主要就是遍歷我們的mProxyMap,然後取得每一個ClassCreatorProxy,最後通過mFiler來創建文件對象,類名爲creatorProxy.getProxyClassFullName(),寫入的內容爲createProxy.generateJavaCode()。
生成Java代碼
我們看一下Java代碼生成的方式。
圖9.Java代碼生成
生成代碼這裏比較簡單,我們可以看見我們拿到了包名,以及類名,類名加上“_ViewBinding”就是我們生成的代理文件。
我們來看一下這個編譯時生成的代理文件,在app->build->generated->source->apt中需要我們編譯之後纔會生成。
圖10.生成的代理文件
這裏需要注意一下,在我們的ClassCreatorProxy中generateJavaCode()方法中的builder.append(“import com.hikvision.lg.demo.*\n”)必須是MainActivity所在的包,不然會報找不到這個包,因爲在這個代理類中我們需要傳入MainActivity的實例。
提供外部使用
最後我們需要編一個提供給外部使用的api,我們先看一下這個提供外部使用的工具類。
圖11.提供給外部使用的類
這裏是使用到了反射,因爲是編譯時期生成的,所以是不會影響性能的,我們看到先是獲取到傳入的類的類名MainActivity再加上“_ViewBinding”找到我們生成的代理文件,然後調用代理文件中bind的方法,並且調用該方法完成綁定操作。
最後我們看一下在外部是怎麼調用的註解:
圖12.註解的使用
外部使用註解就很簡單,只需要先綁定一下所在的類,然後調用註解使用即可。
案例總結
本文通過具體的實例來描述瞭如何編寫一個基於編譯時註解的項目,主要步驟爲:項目結構的劃分、註解模塊的實現、註解處理器的編寫以及對外公佈的API模塊的編寫。用註解處理器的好處就是可以自動幫我們生成一些重複大量的代碼,並且能讓我們的類變得乾淨、邏輯清晰。
Demo GitHub地址:https://github.com/lg2179/AnnotationDemo