Android 註解快速入門和實用解析
首先什麼是註解?@Override
就是註解,它的作用是:
1、檢查是否正確的重寫了父類中的方法。 2、標明程式碼,這是一個重寫的方法。
1、體現在於:檢查子類重寫的方法名與引數型別是否正確;檢查方法private/final/static等不能被重寫。實際上@Override
對於應用程式並沒有實際影響,從它的原始碼中可以出來。
2、主要是表現出程式碼的可讀性。
作為Android開發中熟知的註解,Override只是註解的一種體現,更多時候,註解還有以下作用:
- 降低專案的耦合度。
- 自動完成一些規律性的程式碼。
- 自動生成java程式碼,減輕開發者的工作量。
一、註解基礎快讀
1、元註解
元註解是由java提供的基礎註解,負責註解其它註解,如上圖Override被@Target
和@Retention
修飾,它們用來說明解釋其它註解,位於sdk/sources/android-25/java/lang/annotation
路徑下。
元註解有:
- @Retention:註解保留的生命週期
- @Target:註解物件的作用範圍。
- @Inherited:@Inherited標明所修飾的註解,在所作用的類上,是否可以被繼承。
- @Documented:如其名,javadoc的工具文件化,一般不關心。
@Retention
Retention說標明瞭註解被生命週期,對應RetentionPolicy的列舉,表示註解在何時生效:
- SOURCE:只在原始碼中有效,編譯時拋棄,如上面的
@Override
。 - CLASS:編譯class檔案時生效。
- RUNTIME:執行時才生效。
如下圖X1,com.android.support:support-annotations
中的Nullable註解,會在編譯期判斷,被註解的引數是否會空,具體後續分析。
@Target
Target標明瞭註解的適用範圍,對應ElementType列舉,明確了註解的有效範圍。
- TYPE:類、介面、列舉、註解型別。
- FIELD:類成員(構造方法、方法、成員變數)。
- METHOD:方法。
- PARAMETER:引數。
- CONSTRUCTOR:構造器。
- LOCAL_VARIABLE:區域性變數。
- ANNOTATION_TYPE:註解。
- PACKAGE:包宣告。
- TYPE_PARAMETER:型別引數。
- TYPE_USE:型別使用宣告。
如上圖X1所示,@Nullable
可用於註解方法,引數,類成員,註解,包宣告中,常用例子如下所示:
/** * Nullable表明 * bind方法的引數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 執行時註解
瞭解了元註解後,看看如何實現和使用自定義註解。這裡我們簡單介紹下執行時註解RUNTIME,編譯時註解CLASS留著後面分析。
首先,建立一個註解遵循: public @interface 註解名 {方法引數},如下方@getViewTo
註解:
@Target({ElementType.FIELD}) @Retention(RetentionPolicy.RUNTIME) public @interface getViewTo { int value() default -1; }
然後如下方所示,我們將註解描述在Activity的成員變數mTv
和mBtn
中,在App執行時,通過反射將findViewbyId得到的控制元件,注入到mTv
和mBtn
中。
是不是很熟悉,有點ButterKnife的味道?當然,ButterKnife比這個高階多,畢竟反射多了影響效率,不過我們明白了,可以通過註解來注入和建立物件,這樣可以在一定程度節省程式碼量。
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所示,大多數時候實在執行時使用反射來實現所需效果,這很大程度上影響效率,如果BufferKnife的每個View注入不可能如何實現。實際上,ButterKnife使用的是編譯時註解CLASS,如下圖X2.2,是ButterKnife的@BindView
註解,它是一個編譯時註解,在編譯時生成對應java程式碼,實現注入。
說到編譯時註解,就不得不說註解處理器 AbstractProcessor,如果你有注意,一般第三方註解相關的類庫,如bufferKnike、ARouter,都有一個Compiler命名的Module,如下圖X2.3,這裡面一般都是註解處理器,用於編譯時處理對應的註解。
註解處理器(Annotation Processor)是javac的一個工具,它用來在編譯時掃描和處理註解(Annotation)。你可以對自定義註解,並註冊相應的註解處理器,用於處理你的註解邏輯。
如下所示,實現一個自定義註解處理器,至少重寫四個方法,並且註冊你的自定義Processor,詳細可參考下方程式碼CustomProcessor
。
- @AutoService(Processor.class),谷歌提供的自動註冊註解,為你生成註冊Processor所需要的格式檔案(
com.google.auto
相關包)。 - 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可以建立檔案 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或內部類等就不一樣了。 //getName返回的是[[Ljava.lang.String之類的表現形式, //getCanonicalName返回的就是跟我們宣告類似的形式。 sets(BindView.class.getCanonicalName()); return sets; } //指定Java版本,一般返回最新版本即可 @Override public SourceVersion getSupportedSourceVersion() { return SourceVersion.latestSupported(); } }
首先,我們梳理下一般處理器處理邏輯:
- 1、遍歷得到原始碼中,需要解析的元素列表。
- 2、判斷元素是否可見和符合要求。
- 3、組織資料結構得到輸出類引數。
- 4、輸入生成java檔案。
- 5、錯誤處理。
然後,讓我們理解一個概念:Element
,因為它是我們獲取註解的基礎。
Processor處理過程中,會掃描全部Java原始碼,程式碼的每一個部分都是一個特定型別的Element,它們像是XML一層的層級機構,比如類、變數、方法等,每個Element代表一個靜態的、語言級別的構件,如下方程式碼所示。
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
獲取。你可以通過呼叫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
)是一個根據指定引數,生成java檔案的開源庫,有興趣瞭解javapoet的可以看下javapoet——讓你從重複無聊的程式碼中解放出來,在處理器中,按照引數建立出 JavaFile
之後,通Filer
利用javaFile.writeTo(filer);
就可以生成你需要的java檔案。
4、錯誤處理,在處理器中,我們不能直接丟擲一個異常,因為在process()中丟擲一個異常,會導致執行註解處理器的JVM崩潰,導致跟蹤棧資訊十分混亂。因此,註解處理器就有一個Messager類,一般通過messager.printMessage( Diagnostic.Kind.ERROR, StringMessage, element)
即可正常輸出錯誤資訊。
至此,你的註解處理器完成了所有的邏輯。可以看出,編譯時註解實在編譯時生成java檔案,然後將生產的java檔案注入到原始碼中,在執行時並不會像執行時註解一樣,影響效率和資源。
總結
我們就利用ButterKnife的流程,簡單舉例做個總結吧。
- 1、
@BindView
在編譯時,根據Acitvity生產了XXXActivity$$ViewBinder.java。 - 2、Activity中呼叫的
ButterKnife.bind(this);
,通過this的類名字,加$$ViewBinder,反射得到了ViewBinder
,和編譯處理器生產的java檔案關聯起來了,並將其存在map中快取,然後呼叫ViewBinder.bind()
。 - 3、在ViewBinder的bind方法中,通過id,利用ButterKnife的
butterknife.internal.Utils
工具類中的封裝方法,將findViewById()控制元件注入到Activity的引數中。
好了,通過上面的流程,是不是把編譯時註解的生成和使用連線起來了呢?有問題還請各位留言談論。
相關文章
- 快速入門——深度學習理論解析與實戰應用深度學習
- Android外掛化快速入門與例項解析(VirtualApk)AndroidAPK
- android dev概念快速入門Androiddev
- Android.bp快速入門Android
- Android2.2快速入門Android
- Realm for Android快速入門教程Android
- Android SQLite快速入門教程AndroidSQLite
- Java註解(入門級)Java
- Apache Doris 輕鬆入門和快速實踐Apache
- ElasticSearch實戰系列八: Filebeat快速入門和使用---圖文詳解Elasticsearch
- Axon框架快速入門和DDD專案實踐框架
- Python3網路爬蟲快速入門實戰解析Python爬蟲
- 快應用快速入門教程
- ElasticSearch實戰系列六: Logstash快速入門和實戰Elasticsearch
- Laravel5.8 入門系列二,快速實現使用者註冊登入功能Laravel
- Solon詳解(一)- 快速入門
- Mybatis註解開發案例(入門)MyBatis
- 效能診斷利器JProfiler快速入門和最佳實踐
- Util應用框架快速入門(5) - 許可權入門框架
- java自定義註解學習(三)_註解解析及應用Java
- 快速排序快速入門排序
- Elasticsearch快速入門和環境搭建Elasticsearch
- webservice快速入門-SOAP和WSDL(三)Web
- YOLOv5快速入門和使用YOLO
- GitOps快速入門與實踐Git
- webpack 快速入門 系列 —— 實戰一Web
- Android跨平臺入門:手把手帶你快速入門Flutter!AndroidFlutter
- Android OkHttp原始碼解析入門教程(一)AndroidHTTP原始碼
- Android OkHttp原始碼解析入門教程(二)AndroidHTTP原始碼
- nginx實用入門Nginx
- MQMQ的快速入門+應用場景MQ
- Logback 快速入門 / 使用詳解
- SpringBoot詳解(一)-快速入門Spring Boot
- 自學前端如何快速入門?怎麼快速入門前端?前端
- Util應用框架快速入門(4) - 整合測試開發入門框架
- SQL快速入門 ( MySQL快速入門, MySQL參考, MySQL快速回顧 )MySql
- 機器學習PAI快速入門與業務實戰機器學習AI
- MySQL 快速入門MySql