寫在開頭:最近在翻讀一些開源庫的時候,發現大多使用了註解,於是不得不來仔細瞭解一下Android下的註解知識
什麼是註解
java.lang.annotation
,介面 Annotation,在JDK5.0及以後版本引入。
註解是程式碼裡的特殊標記,這些標記可以在編譯、類載入、執行時被讀取,並執行相應的處理。通過使用Annotation,開發人員可以在不改變原有邏輯的情況下,在原始檔中嵌入一些補充的資訊。程式碼分析工具、開發工具和部署工具可以通過這些補充資訊進行驗證、處理或者進行部署。
Annotation不能執行,它只有成員變數,沒有方法。Annotation跟public、final等修飾符的地位一樣,都是程式元素的一部分,Annotation不能作為一個程式元素使用。
註解的作用
註解將一些本來重複性的工作,變成程式自動完成,簡化和自動化該過程。比如用於生成Java doc,比如編譯時進行格式檢查,比如自動生成程式碼等,用於提升軟體的質量和提高軟體的生產效率。
常見的註解
Android已經定義好的註解大致分為4種,稱之為4大元註解
@Retention:定義該Annotation被保留的時間長度
RetentionPoicy.SOURCE
:註解只保留在原始檔,當Java檔案編譯成class檔案的時候,註解被遺棄;用於做一些檢查性的操作,比如@Override
和@SuppressWarnings
RetentionPoicy.CLASS:
註解被保留到class檔案,但jvm載入class檔案時候被遺棄,這是預設的生命週期;用於在編譯時進行一些預處理操作,比如生成一些輔助程式碼(如ButterKnife
)RetentionPoicy.RUNTIME:
註解不僅被儲存到class檔案中,jvm載入class檔案之後,仍然存在;用於在執行時去動態獲取註解資訊。這個註解大都會與反射一起使用
@Target:定義了Annotation所修飾的物件範圍
ElementType.CONSTRUCTOR
:用於描述構造器ElementType.FIELD
:用於描述域ElementType.LOCAL_VARIABLE
:用於描述區域性變數ElementType.METHOD
:用於描述方法ElementType.PACKAGE
:用於描述包ElementType.PARAMETER
:用於描述引數ElementType.TYPE
:用於描述類、介面(包括註解型別) 或enum宣告
未標註則表示可修飾所有
@Inherited:是否允許子類繼承父類的註解,預設是false
@Documented 是否會儲存到 Javadoc 文件中
自定義註解
自定義註解中使用到較多的是執行時註解和編譯時註解
執行時註解
下面通過一個簡單的動態繫結控制元件的例子來說明
首先定義一個簡單的自定義註解,
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface BindView {
int value() default -1;
}
複製程式碼
然後在app執行時,通過反射將findViewbyId()得到的控制元件,注入到我們需要的變數中。
public class AnnotationActivity extends AppCompatActivity {
@BindView(R.id.annotation_tv)
private TextView mTv;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_annotation);
getAllAnnotationView();
mTv.setText("Annotation");
}
private void getAllAnnotationView() {
//獲得成員變數
Field[] fields = this.getClass().getDeclaredFields();
for (Field field : fields) {
try {
//判斷註解
if (field.getAnnotations() != null) {
//確定註解型別
if (field.isAnnotationPresent(BindView.class)) {
//允許修改反射屬性
field.setAccessible(true);
BindView bindView = field.getAnnotation(BindView.class);
//findViewById將註解的id,找到View注入成員變數中
field.set(this, findViewById(bindView.value()));
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
複製程式碼
最後mTv
上顯示的就是我們想要的“Annotation”文字,這看起來是不是有點像ButterKnife
,但是要注意反射是很消耗效能的,
所以我們常用的控制元件繫結庫ButterKnife
並不是採用執行時註解,而是採用的編譯時註解.
編譯時註解
定義
在說編譯時註解之前,我們得先提一提註解處理器AbstractProcessor
。
它是javac的一個工具,用來在編譯時掃描和處理註解Annotation
,你可以自定義註解,並註冊到相應的註解處理器,由註解處理器來處理你的註解。
一個註解的註解處理器,以Java程式碼(或者編譯過的位元組碼)作為輸入,生成檔案(通常是.java檔案)作為輸出。這些由註解器生成的.java程式碼和普通的.java一樣,可以被javac編譯。
匯入
因為AbstractProcessor
是javac中的一個工具,所以在Android的工程下沒法直接呼叫。下面提供一個本人嘗試可行的匯入方式。
File–>New Module–>java library 新建一個java module,注意一定要是java library,不是Android library
接下來就可以在對應的library中使用AbstractProcessor
了
準備工作完成之後,下面通過一個簡單的註解繫結控制元件的例子來講述
工程目錄
--app (主工程)
--app_annotation (java module 自定義註解)
--annotation-api (Android module)
--app_compiler (java module 註解處理器邏輯)
複製程式碼
在annotation module下建立註解
@Retention(RetentionPolicy.CLASS)
public @interface BindView {
//繫結控制元件
int value();
}
複製程式碼
在compiler module下建立註解處理器 CustomProcessor
public class CustomProcessor extends AbstractProcessor {
//檔案相關的輔助類
private Filer mFiler;
//元素相關的輔助類
private Elements mElements;
//初始化引數
@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
super.init(processingEnvironment);
mElements = processingEnvironment.getElementUtils();
mFiler = processingEnvironment.getFiler();
}
//核心處理邏輯,相當於java中的主函式main(),你需要在這裡編寫你自己定義的註解的處理邏輯
//返回值 true時表示當前處理,不允許後續的註解器處理
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment env) {
return true;
}
//自定義註解集合
@Override
public Set<String> getSupportedAnnotationTypes() {
Set<String> types = new LinkedHashSet<>();
types.add(BindView.class.getCanonicalName());
return types;
}
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
}
複製程式碼
其中核心程式碼process
函式有兩個引數,我們重點關注第二個引數,因為env
表示的是所有註解的集合
首先我們先簡單的說明一下porcess
的處理流程
- 遍歷env,得到我們需要的元素列表
- 將元素列表封裝成物件,方便之後的處理(如同平時解析json資料一樣)
- 通過JavaPoet庫將物件以我們期望的形式生成java檔案
- 遍歷env,得到我們需要的元素列表
for(Element element : roundEnvironment.getElementsAnnotatedWith(BindView.class)){
// todo ....
// 判斷元素的型別為Class
if (element.getKind() == ElementKind.CLASS) {
// 顯示轉換元素型別
TypeElement typeElement = (TypeElement) element;
// 輸出元素名稱
System.out.println(typeElement.getSimpleName());
// 輸出註解屬性值
System.out.println(typeElement.getAnnotation(BindView.class).value());
}
}
複製程式碼
直接通過getElementsAnnotatedWith
函式就能獲取到需要的註解的列表,函式體內加了些element
簡單的使用
2.將元素列表封裝成物件,方便之後的處理
首先,我們需要明確,在繫結控制元件的這個事件下,我們需要的是控制元件的id。
新建類 BindViewField.class
用來儲存自定義註解BindView
相關的屬性
BindViewField.class
public class BindViewField {
private VariableElement mFieldElement;
private int mResId;
public BindViewField(Element element) throws IllegalArgumentException {
if (element.getKind() != ElementKind.FIELD) {
throw new IllegalArgumentException(String.format("Only field can be annotated with @%s",
BindView.class.getSimpleName()));
}
mFieldElement = (VariableElement) element;
BindView bindView = mFieldElement.getAnnotation(BindView.class);
mResId = bindView.value();
if (mResId < 0) {
throw new IllegalArgumentException(String.format("value() in %s for field % is not valid",
BindView.class.getSimpleName(), mFieldElement.getSimpleName()));
}
}
public Name getFieldName() {
return mFieldElement.getSimpleName();
}
public int getResId() {
return mResId;
}
public TypeMirror getFieldType() {
return mFieldElement.asType();
}
}
複製程式碼
上述的BindViewField
只能表示一個自定義註解bindView
物件,而一個類中很可能會有多個自定義註解,所以還需要建立一個物件Annotation.class
來管理自定義註解集合、
AnnotatedClass.class
public class AnnotatedClass {
//類
public TypeElement mClassElement;
//類內的註解變數
public List<BindViewField> mFiled;
//元素幫助類
public Elements mElementUtils;
public AnnotatedClass(TypeElement classElement, Elements elementUtils) {
this.mClassElement = classElement;
this.mElementUtils = elementUtils;
this.mFiled = new ArrayList<>();
}
//新增註解變數
public void addField(BindViewField field) {
mFiled.add(field);
}
//獲取包名
public String getPackageName(TypeElement type) {
return mElementUtils.getPackageOf(type).getQualifiedName().toString();
}
//獲取類名
private static String getClassName(TypeElement type, String packageName) {
int packageLen = packageName.length() + 1;
return type.getQualifiedName().toString().substring(packageLen).replace(`.`, `$`);
}
}
複製程式碼
給上完整的解析流程
//解析過後的目標註解集合
private Map<String, AnnotatedClass> mAnnotatedClassMap = new HashMap<>();
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
mAnnotatedClassMap.clear();
try {
processBindView(roundEnvironment);
} catch (Exception e) {
e.printStackTrace();
return true;
}
return true;
}
private void processBindView(RoundEnvironment env) {
for (Element element : env.getElementsAnnotatedWith(BindView.class)) {
AnnotatedClass annotatedClass = getAnnotatedClass(element);
BindViewField field = new BindViewField(element);
annotatedClass.addField(field);
System.out.print("p_element=" + element.getSimpleName() + ",p_set=" + element.getModifiers());
}
}
private AnnotatedClass getAnnotatedClass(Element element) {
TypeElement encloseElement = (TypeElement) element.getEnclosingElement();
String fullClassName = encloseElement.getQualifiedName().toString();
AnnotatedClass annotatedClass = mAnnotatedClassMap.get(fullClassName);
if (annotatedClass == null) {
annotatedClass = new AnnotatedClass(encloseElement, mElements);
mAnnotatedClassMap.put(fullClassName, annotatedClass);
}
return annotatedClass;
}
複製程式碼
3.通過JavaPoet庫將物件以我們期望的形式生成java檔案
通過上述兩步成功獲取了自定義註解的元素物件,但是還是缺少一步關鍵的步驟,缺少一步findViewById()
,實際上ButterKnife這個很出名的庫也並沒有省略findViewById()
這一個步驟,只是在編譯的時候,在build/generated/source/apt/debug下生成了一個檔案,幫忙執行了findViewById()
這一行為而已。
同樣的,我們這裡也需要生成一個java檔案,採用的是JavaPoet這個庫。具體的使用 參考連結
在process
函式中增加生成java檔案的邏輯
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
mAnnotatedClassMap.clear();
try {
processBindView(roundEnvironment);
} catch (Exception e) {
e.printStackTrace();
return true;
}
try {
for (AnnotatedClass annotatedClass : mAnnotatedClassMap.values()) {
annotatedClass.generateFinder().writeTo(mFiler);
}
} catch (Exception e) {
e.printStackTrace();
}
return true;
}
複製程式碼
其中核心邏輯annotatedClass.generateFinder().writeTo(mFiler);
具體實現在AnnotatedClass
中
public JavaFile generateFinder() {
//構建 inject 方法
MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder("inject")
.addModifiers(Modifier.PUBLIC)
.addAnnotation(Override.class)
.addParameter(TypeName.get(mClassElement.asType()), "host", Modifier.FINAL)
.addParameter(TypeName.OBJECT, "source")
.addParameter(Utils.FINDER, "finder");
//inject函式內的核心邏輯,
// host.btn1=(Button)finder.findView(source,2131427450); ----生成程式碼
// host.$N=($T)finder.findView(source,$L) ----原始程式碼
// 對比就會發現這裡執行了實際的findViewById繫結事件
for (BindViewField field : mFiled) {
methodBuilder.addStatement("host.$N=($T)finder.findView(source,$L)", field.getFieldName()
, ClassName.get(field.getFieldType()), field.getResId());
}
String packageName = getPackageName(mClassElement);
String className = getClassName(mClassElement, packageName);
ClassName bindClassName = ClassName.get(packageName, className);
//構建類物件
TypeSpec finderClass = TypeSpec.classBuilder(bindClassName.simpleName() + "$$Injector")
.addModifiers(Modifier.PUBLIC)
.addSuperinterface(ParameterizedTypeName.get(Utils.INJECTOR, TypeName.get(mClassElement.asType()))) //繼承介面
.addMethod(methodBuilder.build())
.build();
return JavaFile.builder(packageName, finderClass).build();
}
複製程式碼
到這裡,大部分邏輯都已實現,用來繫結控制元件的輔助類也已通關JavaPoet生成了,只差最後一步,宿主註冊,如同ButterKnife一般,ButterKnife.bind(this)
編寫呼叫介面
在annotation-api下新建
注入介面Injector
public interface Injector<T> {
void inject(T host, Object source, Finder finder);
}
複製程式碼
宿主通用介面Finder
(方便之後擴充套件到view和fragment)
public interface Finder {
Context getContext(Object source);
View findView(Object source, int id);
}
複製程式碼
activity實現類 ActivityFinder
public class ActivityFinder implements Finder{
@Override
public Context getContext(Object source) {
return (Activity) source;
}
@Override
public View findView(Object source, int id) {
return ((Activity) (source)).findViewById(id);
}
}
複製程式碼
核心實現類 ButterKnife
public class ButterKnife {
private static final ActivityFinder finder = new ActivityFinder();
private static Map<String, Injector> FINDER_MAP = new HashMap<>();
public static void bind(Activity activity) {
bind(activity, activity);
}
private static void bind(Object host, Object source) {
bind(host, source, finder);
}
private static void bind(Object host, Object source, Finder finder) {
String className = host.getClass().getName();
try {
Injector injector = FINDER_MAP.get(className);
if (injector == null) {
Class<?> finderClass = Class.forName(className + "$$Injector");
injector = (Injector) finderClass.newInstance();
FINDER_MAP.put(className, injector);
}
injector.inject(host, source, finder);
} catch (Exception e) {
e.printStackTrace();
}
}
}
複製程式碼
主工程下呼叫
對應的按鈕可以直接使用,不需要findViewById()
public class MainActivity extends AppCompatActivity {
@BindView(R.id.annotation_tv)
public TextView tv1;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ButterKnife.bind(this);
tv1.setText("annotation_demo");
}
}
複製程式碼
JavaPoet的簡單介紹
常用的幾個類
- MethodSpec 代表一個建構函式或方法宣告。
- TypeSpec 代表一個類,介面,或者列舉宣告。
- FieldSpec 代表一個成員變數,一個欄位宣告。
- JavaFile包含一個頂級類的Java檔案。
常用的佔位符
$L for variable (變數)
$S for Strings
$T for Types
$N for Names(我們自己生成的方法名或者變數名等等)
補充內容
自定義Processor
註解處理器中最主要的處理方法是process()
函式,而process()
函式中重要的是 RoundEnvironment
引數,
通常的使用方式
for (Element element : env.getElementsAnnotatedWith(BindView.class)) {
//todo
}
複製程式碼
通過BindView註解獲取所有的Element物件,而這個Element是什麼呢?
Element表示一個程式元素,可以是包,類或者是方法,所有通過註解取到的元素都將以Element型別處理.準確的來說是Element物件的子類處理。
Element的子類
- ExecutableElement 表示某個類或介面的方法、構造方法或初始化程式(靜態或例項),包括註釋型別元素。
對應註解時的@Target(ElementType.METHOD)
和
@Target(ElementType.CONSTRUCTOR)
- PackageElement 表示一個包程式元素,提供對有關包及其成員的資訊訪問。對應註解時
@Target(ElementType.PACKAGE)
- TypeElement 表示一個類或介面程式元素,提供對有關型別及其成員的資訊訪問。對應
@Target(ElementType.TYPE)
注意:列舉型別是一種類,而註解型別是一種介面。 - TypeParameterElement 表示一般類、介面、方法或構造方法元素的型別引數。
對應@Target(ElementType.PARAMETER)
- VariableElement 表示一個欄位、enum常量、方法或構造方法引數、區域性變數或異常引數。
對應@Target(ElementType.LOCAL_VARIABLE)
不同型別的Element的資訊獲取方式不同
修飾方法的註解和ExecutableElement
當你有一個註解是以@Target(ElementType.METHOD)定義時,表示該註解只能修飾方法。
獲取我們需要的一些基本資訊
//BindClick.class 以 @Target(ElementType.METHOD)修飾
for (Element element : roundEnv.getElementsAnnotatedWith(BindClick.class)) {
//對於Element直接強轉
ExecutableElement executableElement = (ExecutableElement) element;
//非對應的Element,通過getEnclosingElement轉換獲取
TypeElement classElement = (TypeElement) element.getEnclosingElement();
//當(ExecutableElement) element成立時,使用(PackageElement) element.getEnclosingElement();將報錯。
//需要使用elementUtils來獲取
Elements elementUtils = processingEnv.getElementUtils();
PackageElement packageElement = elementUtils.getPackageOf(classElement);
//全類名
String fullClassName = classElement.getQualifiedName().toString();
//類名
String className = classElement.getSimpleName().toString();
//包名
String packageName = packageElement.getQualifiedName().toString();
//方法名
String methodName = executableElement.getSimpleName().toString();
//取得方法引數列表
List<? extends VariableElement> methodParameters = executableElement.getParameters();
//引數型別列表
List<String> types = new ArrayList<>();
for (VariableElement variableElement : methodParameters) {
TypeMirror methodParameterType = variableElement.asType();
if (methodParameterType instanceof TypeVariable) {
TypeVariable typeVariable = (TypeVariable) methodParameterType;
methodParameterType = typeVariable.getUpperBound();
}
//引數名
String parameterName = variableElement.getSimpleName().toString();
//引數型別
String parameteKind = methodParameterType.toString();
types.add(methodParameterType.toString());
}
}
複製程式碼
修飾屬性、類成員的註解和VariableElement
for (Element element : roundEnv.getElementsAnnotatedWith(IdProperty.class)) {
//ElementType.FIELD註解可以直接強轉VariableElement
VariableElement variableElement = (VariableElement) element;
TypeElement classElement = (TypeElement) element
.getEnclosingElement();
PackageElement packageElement = elementUtils.getPackageOf(classElement);
//類名
String className = classElement.getSimpleName().toString();
//包名
String packageName = packageElement.getQualifiedName().toString();
//類成員名
String variableName = variableElement.getSimpleName().toString();
//類成員型別
TypeMirror typeMirror = variableElement.asType();
String type = typeMirror.toString();
}
複製程式碼
修飾類的註解和TypeElement
for (Element element : roundEnv.getElementsAnnotatedWith(BindView.class)) {
//ElementType.TYPE註解可以直接強轉TypeElement
TypeElement classElement = (TypeElement) element;
PackageElement packageElement = (PackageElement) element
.getEnclosingElement();
//全類名
String fullClassName = classElement.getQualifiedName().toString();
//類名
String className = classElement.getSimpleName().toString();
//包名
String packageName = packageElement.getQualifiedName().toString();
//父類名
String superClassName = classElement.getSuperclass().toString();
}
複製程式碼