自定義註解框架的那些事
一、前言
距離上次更新已過一個半月,工作太忙也不是停更的理由。我這方面做得很不好,希望大家給予監督。首先會講解【編譯期資源註入】,接著是【下拉刷新註入】(通過註解來實現下拉刷新功能),最後打造一款【特色的註解框架】。
大家準備好公交卡了嗎,開車了 …
二、什麽是註解
每位童鞋對 註解 都有自己的理解,字面上的意思就是【額外的加入】,在項目當中使用的註解的框架已經越來越多,如 : retrofit ,butterknife,androidannotations … 2017年Android百大框架排行榜 基本都會看到註解的身影 。
註解可以減少大量重復的工作,提高開發效率,註解也非常的靈活,可以註解到 類、方法、屬性等
前一篇文章對運行時RUNTIME事件的註解做了一個簡單的講解 初談Android-Annotations(二) , 本篇主要簡單講解編譯時CLASS**res**文件下的資源註入。前者一般是通過反射來實現的,影響效率,接下來一起來了解下編譯時CLASS的實現。
三、編譯時的註解
類似 butterknife,androidannotations,arouter 使用的都是編譯時的註解CLASS。這裏以 butterknife 為例,來看一看以下註解:
/**
* Bind a field to the specified string resource ID.
* <pre><code>
* [email protected]
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
如上定義了資源字符串的註解,那麽怎麽去實現字符串的註入的呢?
這裏就不得不提到註解處理器 AbstractProcessor,註解處理器是 javac
的一個工具,它用來在編譯時掃描和處理註解(Annotation)。以編譯過的字節碼作為輸入,生成文件(一般是使用 javapoet 生成 .Java 文件)作為輸出,生成的 Java 文件一樣被 javac 編譯。同樣運行時註解 (RetentionPolicy.RUNTIME
) 和源碼註解 (RetentionPolicy.SOURCE)
也可以在註解處理器進行處理。
註意:通過註解處理器處理註解是不能修改已經存在的 Java 類。例如增刪一些方法。
四、字符串資源的註入
顏色(color),數組,尺寸的註入與字符串的註入類似,這裏以字符串的註入來講解。
先看看最終的實現效果:
log的打印日誌:
通過日誌可以看出:前面兩個屬性並沒有在註解中引用字符串的資源,最終的結果和引用 R.string.app_name
的結果一樣,這樣可以節省代碼量(為懶人服務)。目前支持駝峰式命名與前綴m
命名,規則由你改寫,隨性打造屬於你自己的註入框架。
基礎知識科普
如下所示,實現一個自定義註解處理器,至少重寫四個方法,並且註冊你的自定義Processor
。
- @AutoService(Processor.class),谷歌提供的自動註冊註解,為你生成註冊
Processor
所需要的格式文件。請在當前庫的gradle
文件添加如下依賴:
dependencies {
compile fileTree(dir: ‘libs‘, include: [‘*.jar‘])
compile ‘com.google.auto.service:auto-service:1.0-rc3‘//添加
compile ‘com.google.auto:auto-common:0.8‘//添加
}
- 1
- 2
- 3
- 4
- 5
- 1
- 2
- 3
- 4
- 5
-
init(ProcessingEnvironment processingEnv) 初始化處理器,一般在這裏獲取我們需要的工具類
-
getSupportedAnnotationTypes() 返回註解器所支持的註解類型集合
-
getSupportedSourceVersion 返回
Java
版本 -
process 當於每個處理器的主函數
main()
,處理註解的邏輯(掃描、檢驗,以及生成Java文件),如果返回 true,則這些註解已聲明並且不要求後續 Processor 處理它們,反之則要求後續 Processor 處理它們
@AutoService(Processor.class)
public class MyProcessor extends AbstractProcessor {
private Types typeUtils; //類型工具
private Elements elementUtils;//元素工具
private Filer filer; //文件管理器
private Messager messager;//處理異常
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
typeUtils = processingEnv.getTypeUtils();//獲取類的信息
elementUtils = processingEnv.getElementUtils();//獲取程序的元素 如 包 類 方法
filer = processingEnv.getFiler();//生成java文件
messager = processingEnv.getMessager();//處理錯誤日誌
}
/**
* @param annotations 請求處理的註解類型
* @param roundEnv 有關當前和以前的信息環境
* @return
*/
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
return false;
}
/**
* @return 返回的是 註解類型的合法全稱集合,如果沒有這樣的類型,則返回一個空集合
*/
@Override
public Set<String> getSupportedAnnotationTypes() {
Set<String> annotations = new LinkedHashSet<String>();
annotations.add(BindString.class.getCanonicalName());
return annotations;
}
/**
*
* @return 通常返回SourceVersion.latestSupported()
*/
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
首先,我們梳理一下註解處理器的處理邏輯:
-
遍歷備註解的元素
-
檢驗元素是否符合要求
-
獲取輸出類參數
-
生成 java 文件
-
錯誤處理
接著來看個簡單示例獲取被註解的元素名稱,與註解值:
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
// roundEnv.getElementsAnnotatedWith()返回使用給定註解類型的元素
for (Element element : roundEnv.getElementsAnnotatedWith(BindString.class)) {
System.out.println("----------------------");
// 判斷元素的類型為Class
if (element.getKind() == ElementKind.FIELD) {
// 顯示轉換元素類型
VariableElement variableElement= (VariableElement) element;
// 輸出元素名稱
System.out.println(""+variableElement.getSimpleName());
// 輸出註解屬性值
System.out.println(""+variableElement.getAnnotation(BindString.class).value());
}
System.out.println("----------------------");
}
return false;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
MainActivity 類:
@BindString(R.string.app_name)
String appName;
- 1
- 2
- 1
- 2
Gradle console 控制臺輸出如下:
看到這裏,你一定會想,怎麽樣才能使 appName
與 2131099681
建立聯系呢?
如果不使用註解是這樣來建立聯系的:
appName = getResources().getString(2131099681); // R.string.app_name = 2131099681
- 1
- 1
說一萬道一千,實現這行代碼的自動註入是我們的最終目的。
最後,我們來理解一下 Element
的概念,因為它是我們獲取註解的基礎。
Processor
處理過程中,會掃描全部的 Java 源碼,代碼的每一個部分都是一個特定類型的 Element
,它們像是XML
一層的層級機構,比如類、變量、方法等,每個Element
代表一個靜態的、語言級別的構件。
Element
有五個直接子接口,它們分別代表一種特定類型的元素,如下:
-
PackageElement 表示一個包程序元素
-
TypeElement 表示一個類,接口或枚舉程序元素 類型
-
VariableElement 表示一個字段、enum 常量、方法或構造方法參數、局部變量或異常參數 屬性
-
ExecutableElement 表示某個類或接口的方法、構造方法或初始化程序方法(靜態或實例),包括註解類型元素 方法
-
TypeParameterElement 表示一般類、接口、方法或構造方法元素的泛型參數 如
public class MainActivity<T>
泛型 T
在開發中Element
可根據實際情況強轉為以上5種中的一種,它們都帶有各自獨有的方法,來看個簡單的例子:
package com.github.wsannotation; // PackageElement
import java.util.List;
/**
* desc:
* author: wens
* date: 2017/8/11.
*/
public class UserBean // TypeElement
<T extends List> { // TypeParameterElement
private int age; // VariableElement
private String name; // VariableElement
public UserBean() { // ExecutableElement
}
public void setName(String name) { // ExecutableElement
String weight; // VariableElement
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
其中 Element
代表的是源代碼,而 TypeElement
代表的是源代碼中的類型元素,例如類。然而,TypeElement
並不包含類本身的信息。你可以從TypeElement
中獲取類的名字,但是你獲取不到類的信息,例如它的父類。這種信息需要通過TypeMirror
獲取。你可以通過調用elements.asType()
獲取元素的TypeMirror
。
相關文章鏈接:
自定義註解之編譯時註解(RetentionPolicy.CLASS)(三)—— 常用接口介紹
字符串註入流程
這裏參考了 ButterKnife
結合 androidannotations 的實現方式:
1、對元素效驗 verify 系列方法
2、獲取註解字段名和註解的資源ID以及處理資源ID為 -1 的情況
處理資源ID為 -1 的情況,我這裏借鑒了 androidannotations 的處理方式:
//處理默認情況
if (resId == -1) {
TypeElement androidRType = elementUtils.getTypeElement("com.github.butterknifelib.R.string");
List<? extends Element> idEnclosedElements = androidRType.getEnclosedElements();
List<VariableElement> idFields = ElementFilter.fieldsIn(idEnclosedElements);
for (VariableElement idField : idFields) {
TypeKind fieldType = idField.asType().getKind();
if (fieldType.isPrimitive() && fieldType.equals(TypeKind.INT)) {
//制定規則
if (idField.getSimpleName().toString().toLowerCase().replaceAll("_", "")
.equals(name.startsWith("m") ? name.substring(1, name.length()).toLowerCase() : name.toLowerCase())) {
resId = (int) idField.getConstantValue();
break;
}
}
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
更好的方式是以 map
鍵值對來存儲資源類 。後期我會做詳細講解。
3、生成 xxxx$$ViewBinder.java (xxxx 代碼 Activity、View、Dialog)文件
4、給生成 xxxx$$ViewBinder.java 添加資源信息
5、通過 ButterKnife.bind(this);
反射得到 xxxx$$ViewBinder.java 類,並調用註入資源的方法
自定義註解框架的那些事