Android注解快速入門和實用解析
首先什么是注解?@Override就是注解,它的作用是:
- 檢查是否正確的重寫了父類中的方法。
- 標明代碼,這是一個重寫的方法。
1、體現(xiàn)在于:檢查子類重寫的方法名與參數(shù)類型是否正確;檢查方法private/final/static等不能被重寫。實際上@Override對于應用程序并沒有實際影響,從它的源碼中可以出來。
2、主要是表現(xiàn)出代碼的可讀性。
作為Android開發(fā)中熟知的注解,Override只是注解的一種體現(xiàn),更多時候,注解還有以下作用:
- 降低項目的耦合度。
- 自動完成一些規(guī)律性的代碼。
- 自動生成java代碼,減輕開發(fā)者的工作量。
一、注解基礎快讀
1、元注解
元注解是由java提供的基礎注解,負責注解其它注解,如上圖Override被@Target和@Retention修飾,它們用來說明解釋其它注解,位于sdk/sources/android-25/java/lang/annotation路徑下。
元注解有:
- @Retention:注解保留的生命周期
- @Target:注解對象的作用范圍。
- @Inherited:@Inherited標明所修飾的注解,在所作用的類上,是否可以被繼承。
- @Documented:如其名,javadoc的工具文檔化,一般不關(guān)心。
@Retention
Retention說標明了注解被生命周期,對應RetentionPolicy的枚舉,表示注解在何時生效:
- SOURCE:只在源碼中有效,編譯時拋棄,如上面的@Override。
- CLASS:編譯class文件時生效。
- RUNTIME:運行時才生效。
如下圖X1,com.android.support:support-annotations中的Nullable注解,會在編譯期判斷,被注解的參數(shù)是否會空,具體后續(xù)分析。
@Target
Target標明了注解的適用范圍,對應ElementType枚舉,明確了注解的有效范圍。
- TYPE:類、接口、枚舉、注解類型。
- FIELD:類成員(構(gòu)造方法、方法、成員變量)。
- METHOD:方法。
- PARAMETER:參數(shù)。
- CONSTRUCTOR:構(gòu)造器。
- LOCAL_VARIABLE:局部變量。
- ANNOTATION_TYPE:注解。
- PACKAGE:包聲明。
- TYPE_PARAMETER:類型參數(shù)。
- TYPE_USE:類型使用聲明。
如上圖X1所示,@Nullable可用于注解方法,參數(shù),類成員,注解,包聲明中,常用例子如下所示:
- /**
- * Nullable表明
- * bind方法的參數(shù)target和返回值Data可以為null
- */
- @Nullable
- public static Data bind(@Nullable Context target) {
- //do someThing and return
- return bindXXX(target);
- }
@Inherited
注解所作用的類,在繼承時默認無法繼承父類的注解。除非注解聲明了 @Inherited。同時Inherited聲明出來的注,只對類有效,對方法/屬性無效。
如下方代碼,注解類@AInherited聲明了Inherited ,而注解BNotInherited 沒有,所在在它們的修飾下:
- 類Child繼承了父類Parent的@AInherited,不繼承@BNotInherited;
- 重寫的方法testOverride()不繼承Parent的任何注解;
- testNotOverride()因為沒有被重寫,所以注解依然生效。
- @Retention(RetentionPolicy.RUNTIME)
- @Inherited
- public @interface AInherited {
- String value();
- }
- @Retention(RetentionPolicy.RUNTIME)
- public @interface BNotInherited {
- String value();
- }
- @AInherited("Inherited")
- @BNotInherited("沒Inherited")
- public class Parent {
- @AInherited("Inherited")
- @BNotInherited("沒Inherited")
- public void testOverride(){
- }
- @AInherited("Inherited")
- @BNotInherited("沒Inherited")
- public void testNotOverride(){
- }
- }
- /**
- * Child繼承了Parent的AInherited注解
- * BNotInherited因為沒有@Inherited聲明,不能被繼承
- */
- public class Child extends Parent {
- /**
- * 重寫的testOverride不繼承任何注解
- * 因為Inherited不作用在方法上
- */
- @Override
- public void testOverride() {
- }
- /**
- * testNotOverride沒有被重寫
- * 所以注解AInherited和BNotInherited依然生效。
- */
- }
2、自定義注解
2.1 運行時注解
了解了元注解后,看看如何實現(xiàn)和使用自定義注解。這里我們簡單介紹下運行時注解RUNTIME,編譯時注解CLASS留著后面分析。
首先,創(chuàng)建一個注解遵循: public @interface 注解名 {方法參數(shù)},如下方@getViewTo注解:
- @Target({ElementType.FIELD})
- @Retention(RetentionPolicy.RUNTIME)
- public @interface getViewTo {
- int value() default -1;
- }
然后如下方所示,我們將注解描述在Activity的成員變量mTv和mBtn中,在App運行時,通過反射將findViewbyId得到的控件,注入到mTv和mBtn中。
是不是很熟悉,有點ButterKnife的味道?當然,ButterKnife比這個高級多,畢竟反射多了影響效率,不過我們明白了,可以通過注解來注入和創(chuàng)建對象,這樣可以在一定程度節(jié)省代碼量。
- public class MainActivity extends AppCompatActivity {
- @getViewTo(R.id.textview)
- private TextView mTv;
- @getViewTo(R.id.button)
- private Button mBtn;
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_main);
- //通過注解生成View;
- getAllAnnotationView();
- }
- /**
- * 解析注解,獲取控件
- */
- 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) {
- }
- }
- }
- }
2.2 編譯時注解
運行時注解RUNTIME如上2.1所示,大多數(shù)時候?qū)嵲谶\行時使用反射來實現(xiàn)所需效果,這很大程度上影響效率,如果BufferKnife的每個View注入不可能如何實現(xiàn)。實際上,ButterKnife使用的是編譯時注解CLASS,如下圖X2.2,是ButterKnife的@BindView注解,它是一個編譯時注解,在編譯時生成對應java代碼,實現(xiàn)注入。
說到編譯時注解,就不得不說注解處理器 AbstractProcessor,如果你有注意,一般第三方注解相關(guān)的類庫,如bufferKnike、ARouter,都有一個Compiler命名的Module,如下圖X2.3,這里面一般都是注解處理器,用于編譯時處理對應的注解。
注解處理器(Annotation Processor)是javac的一個工具,它用來在編譯時掃描和處理注解(Annotation)。你可以對自定義注解,并注冊相應的注解處理器,用于處理你的注解邏輯。
如下所示,實現(xiàn)一個自定義注解處理器,至少重寫四個方法,并且注冊你的自定義Processor,詳細可參考下方代碼CustomProcessor。
- @AutoService(Processor.class),谷歌提供的自動注冊注解,為你生成注冊Processor所需要的格式文件(com.google.auto相關(guān)包)。
- init(ProcessingEnvironment env),初始化處理器,一般在這里獲取我們需要的工具類。
- getSupportedAnnotationTypes(),指定注解處理器是注冊給哪個注解的,返回指定支持的注解類集合。
- getSupportedSourceVersion() ,指定java版本。
- process(),處理器實際處理邏輯入口。
- @AutoService(Processor.class)
- public class CustomProcessor extends AbstractProcessor {
- /**
- * 注解處理器的初始化
- * 一般在這里獲取我們需要的工具類
- * @param processingEnvironment 提供工具類Elements, Types和Filer
- */
- @Override
- public synchronized void init(ProcessingEnvironment env){
- super.init(env);
- //Element代表程序的元素,例如包、類、方法。
- mElementUtils = env.getElementUtils();
- //處理TypeMirror的工具類,用于取類信息
- mTypeUtils = env.getTypeUtils();
- //Filer可以創(chuàng)建文件
- mFiler = env.getFiler();
- //錯誤處理工具
- mMessages = env.getMessager();
- }
- /**
- * 處理器實際處理邏輯入口
- * @param set
- * @param roundEnvironment 所有注解的集合
- * @return
- */
- @Override
- public boolean process(Set<? extends TypeElement> annoations,
- RoundEnvironment env) {
- //do someThing
- }
- //指定注解處理器是注冊給哪個注解的,返回指定支持的注解類集合。
- @Override
- public Set<String> getSupportedAnnotationTypes() {
- Set<String> sets = new LinkedHashSet<String>();
- //大部分class而已getName、getCanonicalNam這兩個方法沒有什么不同的。
- //但是對于array或內(nèi)部類等就不一樣了。
- //getName返回的是[[Ljava.lang.String之類的表現(xiàn)形式,
- //getCanonicalName返回的就是跟我們聲明類似的形式。
- sets(BindView.class.getCanonicalName());
- return sets;
- }
- //指定Java版本,一般返回***版本即可
- @Override
- public SourceVersion getSupportedSourceVersion() {
- return SourceVersion.latestSupported();
- }
- }
首先,我們梳理下一般處理器處理邏輯:
- 遍歷得到源碼中,需要解析的元素列表。
- 判斷元素是否可見和符合要求。
- 組織數(shù)據(jù)結(jié)構(gòu)得到輸出類參數(shù)。
- 輸入生成java文件。
- 錯誤處理。
然后,讓我們理解一個概念:Element,因為它是我們獲取注解的基礎。
Processor處理過程中,會掃描全部Java源碼,代碼的每一個部分都是一個特定類型的Element,它們像是XML一層的層級機構(gòu),比如類、變量、方法等,每個Element代表一個靜態(tài)的、語言級別的構(gòu)件,如下方代碼所示。
- package android.demo; // PackageElement
- // TypeElement
- public class DemoClass {
- // VariableElement
- private boolean mVariableType;
- // VariableElement
- private VariableClassE m VariableClassE;
- // ExecuteableElement
- public DemoClass () {
- }
- // ExecuteableElement
- public void resolveData (Demo data //TypeElement ) {
- }
- }
其中,Element代表的是源代碼,而TypeElement代表的是源代碼中的類型元素,例如類。然而,TypeElement并不包含類本身的信息。你可以從TypeElement中獲取類的名字,但是你獲取不到類的信息,例如它的父類。這種信息需要通過TypeMirror獲取。你可以通過調(diào)用elements.asType()獲取元素的TypeMirror。
1、知道了Element,我們就可以通過process 中的RoundEnvironment去獲取,掃描到的所有元素,如下圖X2.4,通過env.getElementsAnnotatedWith,我們可以獲取被@BindView注解的元素的列表,其中validateElement校驗元素是否可用。
2、因為env.getElementsAnnotatedWith返回的,是所有被注解了@ BindView的元素的列表。所以有時候我們還需要走一些額外的判斷,比如,檢查這些Element是否是一個類:
- @Override
- public boolean process(Set<? extends TypeElement> an, RoundEnvironment env) {
- for (Element e : env.getElementsAnnotatedWith(BindView.class)) {
- // 檢查元素是否是一個類
- if (ae.getKind() != ElementKind.CLASS) {
- ...
- }
- }
- ...
- }
3、javapoet (com.squareup:javapoet)是一個根據(jù)指定參數(shù),生成java文件的開源庫,有興趣了解javapoet的可以看下javapoet——讓你從重復無聊的代碼中解放出來,在處理器中,按照參數(shù)創(chuàng)建出 JavaFile之后,通Filer利用javaFile.writeTo(filer);就可以生成你需要的java文件。
4、錯誤處理,在處理器中,我們不能直接拋出一個異常,因為在process()中拋出一個異常,會導致運行注解處理器的JVM崩潰,導致跟蹤棧信息十分混亂。因此,注解處理器就有一個Messager類,一般通過messager.printMessage( Diagnostic.Kind.ERROR, StringMessage, element)即可正常輸出錯誤信息。
至此,你的注解處理器完成了所有的邏輯。可以看出,編譯時注解實在編譯時生成java文件,然后將生產(chǎn)的java文件注入到源碼中,在運行時并不會像運行時注解一樣,影響效率和資源。
總結(jié)
我們就利用ButterKnife的流程,簡單舉例做個總結(jié)吧。
- @BindView在編譯時,根據(jù)Acitvity生產(chǎn)了XXXActivity$$ViewBinder.java。
- Activity中調(diào)用的ButterKnife.bind(this);,通過this的類名字,加$$ViewBinder,反射得到了ViewBinder,和編譯處理器生產(chǎn)的java文件關(guān)聯(lián)起來了,并將其存在map中緩存,然后調(diào)用ViewBinder.bind()。
- 在ViewBinder的bind方法中,通過id,利用ButterKnife的butterknife.internal.Utils工具類中的封裝方法,將findViewById()控件注入到Activity的參數(shù)中。
好了,通過上面的流程,是不是把編譯時注解的生成和使用連接起來了呢?有問題還請各位留言談論。