寫在前面
今年大家都在搞元件化,元件化開發不可避免的需要用到路由(Router)來完成元件之間資料的互動,這就促進了各種路由發展如:阿里的ARouter以及ActivityRouter等優秀的Router框架。為了方便大家的開發這些Router庫以及像ButterKnife這類的庫都用到了註解技術。本篇目的是進行一波掃盲。
本文導讀
- Android Annotation基礎
- 解析Annotation
- 實戰-自己做一個ButterKnife
1 Annotatin基礎
1.1 基本Annotation
Java提供了三個基本的Annotation註解,使用時需要在Annotation前面增加@符號,並把Annotation當成一個修飾符來使用。注:Java提供的基本Annotation註解都在java.lang包下面
- @Override:限定重寫父類方法:這個註解主要是命令編譯器幫助我們檢查程式碼(避免出現方法名寫錯這種低階錯誤),子類使用了Override註解之後,編譯的時候IDE會檢查父類中是否有這個函式,如果沒有這個函式會編譯失敗。
public class Animal {
public void run() {
//TODO
}
}
複製程式碼
public class Monkey extends Animal {
@Override
public void run() {
//使用了OVerride註解之後,必須重寫父類方法
}
}
複製程式碼
- @Deprecated:標記該類或者方法已經過時。修改上面的Animal類,使用Deprecated修飾run方法後,子類在使用run方法是IDE會報警告。
public class Animal {
@Deprecated
public void run() {
//TODO
}
}
複製程式碼
3. @SuppressWarnings:抑制編譯器警告(用的比較少)。Java程式碼編譯時IDE往往會給開發者很多警告資訊,例如變數沒有使用等,這種警告多了之後很大程度上影響我們debug效率。此註解就是來抑制這些警告。舉個例子:
@SuppressWarning("unused")
public void foo() {
String s;
}
複製程式碼
如果不使用@SuppressWarning來抑制編譯器警告,上面的程式碼會被警告變數s從未使用。出了"unused",該註解支援的抑制型別還有下圖的內容(注該圖摘自IBM Knowledge Center)。
1.2 JDK元Annotation
JDK出了在java.lang包中提供了1.1介紹的幾種基本Annotation外還在java.lang.annotation包下面提供了四個Meta Annotation(元Annotation)。這四種元Annotation都是來修飾自定義註解的。(hold住節奏,看完這個小結我們們就可以自定義Annotation了)
- @Retention註解。該註解只能修飾一個Annotation定義,用於指定所修飾的Annotation可以保留多長"時間"(也可是說是保留的週期)。這裡說的“時間”有三種型別
- RetentionPolicy.SOURCE:沒啥用,編譯器會直接忽略這種策略的註釋
- RetentionPolicy.CLASS:自定義註解的預設值,編譯器會把這種策略的註釋儲存在class檔案中。像ButterKnife中的BindView註解就是用的這種方式。
- RetentionPolicy.RUNTIME:編譯器會把該策略的註釋儲存到class檔案中,程式可以通過反射等方式來獲取。
舉個例子,自定義一個BindView註解(看不懂沒關係,現有一個感性的認識,下一節開始做自定義Annotation講解)。
//此註解的作用域是Class,也就是編譯時
@Retention(value = RetentionPolicy.CLASS)
public @interface BindView {
int id() default 0;
}
複製程式碼
當成員變數為value時,可以省略。也就是說上述程式碼可以換成
@Retention(RetentionPolicy.CLASS)
- @Target註解:這貨也是用於修飾一個自定義的Annotation註解,用於指定自定義註解可以修飾哪些程式元素。該註解的成員變數有
- ElementType.PACKAGE 註解作用於包
- ElementType.TYPE 註解作用於型別(類,介面,註解,列舉)
- ElementType.ANNOTATION_TYPE 註解作用於註解
- ElementType.CONSTRUCTOR 註解作用於構造方法
- ElementType.METHOD 註解作用於方法
- ElementType.PARAMETER 註解作用於方法引數
- ElementType.FIELD 註解作用於屬性
- ElementType.LOCAL_VARIABLE 註解作用於區域性變數 同樣的,成員變數名為value時可以省略。我們豐富一下上面用到的自定義的BindView註解:
//此註解修飾的是屬性
@Target(ElementType.FIELD)
//此註解的作用域是Class,也就是編譯時
@Retention(value = RetentionPolicy.CLASS)
public @interface BindView {
int id() default 0;
}
複製程式碼
- @Documented註解,該註解修飾的自定義註解可以使用javac命令提取成API文件。
- @Inherited註解,該註解修飾的自定義具有繼承性。舉個例子Animal類使用了@Inherited修飾的自定義註解,則子類Monkey也具有該自定義註解描述的特性。
1.3 自定義註解
- 定義Annotation,以上面使用的自定義BindView註解為例。可以直接新建Annotation型別的java檔案。
- 根據自己的需要,使用1.2的只是對自定義的註解進行修飾
/**
* Created by will on 2018/2/4.
*/
@Documented
//此註解修飾的是屬性
@Target(ElementType.FIELD)
//此註解的作用域是Class,也就是編譯時
@Retention(value = RetentionPolicy.CLASS)
public @interface BindView {
}
複製程式碼
- 定義成員變數,自定義註解的成員變數以方法的形式來定義。豐富一下上面的BindView,由於這個自定義註解的功能是對Activity中的View進行繫結。所以我們定義一個id成員變數。
/**
* Created by will on 2018/2/4.
*/
@Documented
//此註解修飾的是屬性
@Target(ElementType.FIELD)
//此註解的作用域是Class,也就是編譯時
@Retention(value = RetentionPolicy.CLASS)
public @interface BindView {
int id();
}
複製程式碼
- 使用default關鍵字為成員變數指定預設值。繼續豐富BindView的程式碼。注default關鍵字放到int id() 後面。
@Documented
//此註解修飾的是屬性
@Target(ElementType.FIELD)
//此註解的作用域是Class,也就是編譯時
@Retention(value = RetentionPolicy.CLASS)
public @interface BindView {
int id() default 0;
}
複製程式碼
根據有沒有成員變數,我們可以將Annotation劃分成兩種:
- 沒有成員變數的註解稱為"標記Annotation",這種註解使用自身是否存在為我們提供資訊,例如Override等註解
- 有成員變數的稱謂"後設資料Annotation"。我們可以使用apt等工具對這種Annotation的成員進行二次加工。
注意:只定義了自定義註解沒有任何效果,還需要對Annotation的資訊進行提取與加工!!!
上面我們自定義了BindView註解,你是不是想直接拿到Activity中使用呢?例如:
然後你發現Crash了。。。這就要引入下一節的內容了,使用apt對被註解的程式碼進行二次加工。2. 解析Annotation
完成自定義Annotation後,我們還需要知道,針對這些註解,我們要做哪些相關的處理,這就涉及到了Annotation的解析操作。 解析Annotation,通常分為:對執行時Annotation的解析、對編譯時Annotation的解析; 解析Annotation,其實就是如何從程式碼中找到Annotation,通常我們的做法是:
- 用反射的方式獲取Annotation,執行時Annotation的解析方式
- 藉助apt工具獲取Annotation,編譯時Annotation的解析方式
- 另外如果我們需要生成額外的程式碼、檔案,則還需要藉助JavaPoet API
2.1 利用反射解析Annotation
反射的解析方式,通常運用在執行時Annotation的解析。 反射是指:利用Class、Field、Method、Construct等reflect物件,獲取Annotation
- field.getAnnotation(Annotation.class):獲取某個Annotation
- field.getAnnotations():獲取所有的Annotation
- field.isAnnotationPresent(Annotation.class):是否存在該Annotation
通常使用Runtime修飾的註解需要使用反射來配合解析
@Retention(value = RetentionPolicy.RUNTIME)
- 新建一個test自定義註解
/**
* Created by will on 2018/2/4.
*/
@Documented
//此註解修飾的是屬性
@Target(ElementType.FIELD)
@Retention(value = RetentionPolicy.RUNTIME)
public @interface test {
int id() default 0;
}
複製程式碼
- 新建一個java類Animal,並新增test註解
public class Animal {
@BindView(id = 1000)
String a;
@Deprecated
public void run() {
//TODO
}
}
複製程式碼
- 可以使用反射來獲取a的註解成員屬性值
private void testMethod() {
Class clazz = Animal.class;
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
BindView bindView = field.getAnnotation(BindView.class);
if (bindView != null) {
int id = bindView.id();
Log.e("------", String.valueOf(id));
}
}
}
複製程式碼
2.2 使用apt工具來解析Annotation
APT:是一個註解處理工具 Annotation Processing Tool 作用:利用apt,我們可以找到源代中的註解,並根據註解做相應的處理
- 根據註解,生成額外的原始檔或其他檔案
- 編譯生成的原始檔和原來的原始檔,一起生成class檔案
利用APT,在編譯時生成額外的程式碼,不會影響效能,只是影響專案構建的速度
這裡我們說一下Android中使用apt的步驟 Android中開發自定義的apt學會兩個庫及一個類基本就足夠了
- JavaPoet API 這個庫的主要作用就是幫助我們通過類呼叫的形式來生成程式碼,簡單理解就是利用這個庫可以生成額外的Java程式碼。具體的API可以去github上看下,寫的很詳細。這裡不貼程式碼了。
- AutoService 這個庫是Google開發的,主要的作用是註解 processor 類,並對其生成 META-INF 的配置資訊。可以理解使用這個庫之後編譯的時候IDE會編譯我們的Annotation處理器,只需要在自定義的Processor類上新增註釋
@AutoService(Processor.class)
下面會用到。 - Processor類,我們自定義的Annotation處理器都需要實現該介面,Java為我們提供了一個抽象類實現了該介面的部分功能,我們自定義Annotation處理器的時候大部分只需要繼承AbstractProcessor這個抽象類就行了。
JavaPoet的學習可以直接借鑑官方api,AutoService學習成本較低(只需要用裡面一句程式碼而已,學習成本可以忽略),下面我們重點學習一下AbstractProcessor的使用。
AbstractProcessor介紹
- AbstractProcessor方法介紹:下面新建一個AbstractProcessor來看下這貨的方法
/**
* Created by will on 2018/2/5.
*/
public class CustomProcessor extends AbstractProcessor {
@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
super.init(processingEnvironment);
}
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
return false;
}
@Override
public Set<String> getSupportedAnnotationTypes() {
return super.getSupportedAnnotationTypes();
}
@Override
public SourceVersion getSupportedSourceVersion() {
return super.getSupportedSourceVersion();
}
}
複製程式碼
- init(ProcessingEnvironment processingEnvironment): 每一個註解處理器類都必須有一個空的建構函式。然而,這裡有一個特殊的init()方法,它會被註解處理工具呼叫,並輸入ProcessingEnviroment引數。ProcessingEnviroment提供很多有用的工具類Elements,Types和Filer。
- process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment): 這相當於每個處理器的主函式main()。你在這裡寫你的掃描、評估和處理註解的程式碼,以及生成Java檔案。輸入引數RoundEnviroment,可以讓你查詢出包含特定註解的被註解元素。以前面提到的自定義註解BindView為例,這裡可以查到所有註解了BindView的Activity。
- getSupportedAnnotationTypes(): 這裡必須由開發者指定,該方法返回一個Set,作用是這個註解的處理器支援處理哪些註解。
- getSupportedSourceVersion(): 用來指定你使用的Java版本。通常這裡返回SourceVersion.latestSupported()。然而,如果你有足夠的理由只支援Java 7的話,你也可以返回SourceVersion.RELEASE_7。
- AbstractProcessor基礎工具解析:從AbstractProcessor的init方法中可以獲取一系列的工具來輔助我們解析原始碼
- Elements工具類 在AbstractProcessor的init方法中可以獲取到一個Elements工具類,具體程式碼為
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
Elements elementUtils = processingEnv.getElementUtils();
}
複製程式碼
這個工具類是用來處理原始碼的,在自定義註解處理器的領域裡面,Java原始碼每一個型別都屬於一個Element,具體使用方法可以直接參考Java官方文件
package com.example; // PackageElement
public class Test { // TypeElement
private int a; // VariableElement
private Test other; // VariableElement
public Test () {} // ExecuteableElement
public void setA ( // ExecuteableElement
int newA // TypeElement
) {}
}
複製程式碼
例如,我有一個TypeElement,希望拿到這個class所在的包名就可以使用Elemnts這個工具
private String getPackageName(TypeElement type) {
return elementUtils.getPackageOf(type).getQualifiedName().toString();
}
複製程式碼
再來一個栗子,有一個代表Test的TypeElement,希望獲取所有的子元素可以這麼寫(注意,這個很有用)
TypeElement testClass = ... ;
for (Element e : testClass.getEnclosedElements()){ // iterate over children
Element parent = e.getEnclosingElement(); // parent == testClass
}
複製程式碼
- Types:一個用來處理TypeMirror的工具類; TypeElement並不包含類本身的資訊。你可以從TypeElement中獲取類的名字,但是你獲取不到類的資訊,例如它的父類。這種資訊需要通過TypeMirror獲取。你可以通過呼叫elements.asType()獲取元素的TypeMirror。
- Filer:正如這個名字所示,使用Filer你可以建立檔案。
好了枯燥的基礎知識看完了之後我們一起寫一個簡單的ButterKnife
3. 自己寫一個輕量級的ButterKnife
1. 新建一個Java專案,名字為annotations
- 這個專案用來定義所有自定義的註解,這部分用到了第一節的知識基礎。
- 在這個專案包裡面新建自定義的註解,我們模仿ButterKnife,這裡增加一個BindView的註解
@Documented
//此註解修飾的是屬性
@Target(ElementType.FIELD)
//此註解的作用域是Class,也就是編譯時
@Retention(value = RetentionPolicy.CLASS)
public @interface BindView {
int id() default 0;
}
複製程式碼
2 新建Java專案,名稱為annotations_compiler
- 這個專案是用來處理自定義註解的,這裡姑且叫這個專案為BindView的處理器,這裡需要第二節的知識基礎
- 在build.gradle檔案中新增AutoService與JavaPoet的依賴
implementation 'com.google.auto.service:auto-service:1.0-rc2'
implementation 'com.squareup:javapoet:1.7.0'
複製程式碼
- 新建BindViewProcessor處理器類繼承自AbstractProcessor,對原始碼的註解進行處理(我儘可能的理解有歧義的地方都新增了註釋)
/**
* Created by will on 2018/2/4.
*/
@AutoService(Processor.class)
public class BindViewProcessor extends AbstractProcessor {
/**
* 工具類,可以從init方法的ProcessingEnvironment中獲取
*/
private Elements elementUtils;
/**
* 快取所有子Element
* key:父Element類名
* value:子Element
*/
private HashMap<String, List<Element>> cacheElements = null;
/**
* 快取所有父Element
* key:父Element類名
* value:父Element
*/
private HashMap<String, Element> cacheAllParentElements = null;
@Override
public Set<String> getSupportedAnnotationTypes() {
// 規定需要處理的註解型別
return Collections.singleton(BindView.class.getCanonicalName());
}
@Override
public boolean process(Set<? extends TypeElement> annotations
, RoundEnvironment roundEnv) {
//掃描所有註解了BindView的Field,因為我們所有註解BindView的地方都是一個Activity的成員
Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(BindView.class);
for (Element element : elements) {
//將所有子elements進行過濾
addElementToCache(element);
}
if (cacheElements == null || cacheElements.size() == 0) {
return true;
}
for (String parentElementName : cacheElements.keySet()) {
//判斷一下獲取到的parent element是否是類
try {
//使用JavaPoet構造一個方法
MethodSpec.Builder bindViewMethodSpec = MethodSpec.methodBuilder("bindView")
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
.returns(void.class)
.addParameter(ClassName.get(cacheAllParentElements.get(parentElementName).asType())
, "targetActivity");
List<Element> childElements = cacheElements.get(parentElementName);
if (childElements != null && childElements.size() != 0) {
for (Element childElement : childElements) {
BindView bindView = childElement.getAnnotation(BindView.class);
//使用JavaPoet對方法內容進行新增
bindViewMethodSpec.addStatement(
String.format("targetActivity.%s = (%s) targetActivity.findViewById(%s)"
, childElement.getSimpleName()
, ClassName.get(childElement.asType()).toString()
, bindView.id()));
}
}
//構造一個類,以Bind_開頭
TypeSpec typeElement = TypeSpec.classBuilder("Bind_"
+ cacheAllParentElements.get(parentElementName).getSimpleName())
.addModifiers(Modifier.PUBLIC, Modifier.FINAL)
.addMethod(bindViewMethodSpec.build())
.build();
//進行檔案寫入
JavaFile javaFile = JavaFile.builder(
getPackageName((TypeElement) cacheAllParentElements.get(parentElementName))
, typeElement).build();
javaFile.writeTo(processingEnv.getFiler());
} catch (IOException e) {
e.printStackTrace();
return true;
}
}
return true;
}
private String getPackageName(TypeElement type) {
return elementUtils.getPackageOf(type).getQualifiedName().toString();
}
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
elementUtils = processingEnv.getElementUtils();
}
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
/**
* 快取父Element對應的所有子Element
* 快取父Element
*
* @param childElement
*/
private void addElementToCache(Element childElement) {
if (cacheElements == null) {
cacheElements = new HashMap<>();
}
if (cacheAllParentElements == null) {
cacheAllParentElements = new HashMap<>();
}
//父Element類名
String parentElementName = null;
parentElementName = ClassName.get(childElement.getEnclosingElement().asType()).toString();
if (cacheElements.containsKey(parentElementName)) {
List<Element> childElements = cacheElements.get(parentElementName);
childElements.add(childElement);
} else {
ArrayList<Element> childElements = new ArrayList<>();
childElements.add(childElement);
cacheElements.put(parentElementName, childElements);
cacheAllParentElements.put(parentElementName, childElement.getEnclosingElement());
}
}
}
複製程式碼
3.新建Android專案,使用自定義的註解
- 新增對上述兩個專案的引用
注意:Android Gradle外掛2.2版本釋出後,Android 官方提供了annotationProcessor來代替android-apt,annotationProcessor同時支援 javac 和 jack 編譯方式,而android-apt只支援 javac 方式。同時android-apt作者宣佈不在維護,這裡我直接用了annotationProcessor
implementation project(':annotations')
annotationProcessor project(':annotations_compiler')
複製程式碼
- 在Activity的View中新增@BindView註解,並設定id
public class MainActivity extends AppCompatActivity {
@BindView(id = R.id.tv_test)
TextView tv_test;
@BindView(id = R.id.tv_test1)
TextView tv_test1;
@BindView(id = R.id.iv_image)
ImageView iv_image;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Bind_MainActivity.bindView(this);
tv_test.setText("test_1");
tv_test1.setText("test_2");
iv_image.setImageDrawable(getDrawable(R.mipmap.ic_launcher));
}
}
複製程式碼
- 此時你的IDE可能會報Bind_MainActivity找不到,沒關係,重新Build一下就好了。Build一下後在app/build/generated/source/apt/debug/[你的包名]/annotation/路徑下就回生成apt輸出的檔案了。
其他的問題
- 如果你發現build後沒有apt檔案輸出,呵呵,因為你寫的processor有Bug~~~。這時候你需要debug你的processor。關於如何debug,請移步這篇部落格
- 關於android-apt切換為官方annotationProcessor的問題,請移步android-apt切換為官方annotationProcessor
- 待補充ing...
參考文章
About Me
contact way | value |
---|---|
weixinjie1993@gmail.com | |
W2006292 | |
github | https://github.com/weixinjie |
blog | https://juejin.im/user/57673c83207703006bb92bf6 |