Android進(jìn)階之自定義注解
本篇文章內(nèi)容包括:
如果使用過(guò)ButterKnife, EventBus, Retrofit, Dagger等框架, 你對(duì)注解一定不會(huì)陌生. 但是注解背后究竟有什么魔法, 可以做這么不可思議的事情.
什么是注解
先來(lái)看看Java文檔中的定義
An annotation is a form of metadata, that can be added to Java source code. Classes, methods, variables, parameters and packages may be annotated. Annotations have no direct effect on the operation of the code they annotate.
注解是一種元數(shù)據(jù), 可以添加到j(luò)ava代碼中. 類(lèi)涉瘾、方法、變量、參數(shù)仁热、包都可以被注解蝶缀,注解對(duì)注解的代碼沒(méi)有直接影響.
首先, 明確一點(diǎn): 注解并沒(méi)有什么魔法, 之所以產(chǎn)生作用, 是對(duì)其解析后做了相應(yīng)的處理. 注解僅僅只是個(gè)標(biāo)記罷了.
定義注解用的關(guān)鍵字是@interface
元注解
java內(nèi)置的注解有Override, Deprecated, SuppressWarnings等, 作用相信大家都知道.
現(xiàn)在查看Override注解的源碼
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}
發(fā)現(xiàn)Override注解上面有兩個(gè)注解, 這就是元注解. 元注解就是用來(lái)定義注解的注解.其作用就是定義注解的作用范圍, 使用在什么元素上等等, 下面來(lái)詳細(xì)介紹.
元注解共有四種@Retention
, @Target
, @Inherited
, @Documented
-
@Retention
保留的范圍挣惰,默認(rèn)值為CLASS. 可選值有三種-
SOURCE
, 只在源碼中可用 -
CLASS
, 在源碼和字節(jié)碼中可用 -
RUNTIME
, 在源碼,字節(jié)碼,運(yùn)行時(shí)均可用
-
@Target
可以用來(lái)修飾哪些程序元素廊佩,如TYPE
,METHOD
,CONSTRUCTOR
,FIELD
,PARAMETER
等拴还,未標(biāo)注則表示可修飾所有@Inherited
是否可以被繼承膝宁,默認(rèn)為false@Documented
是否會(huì)保存到 Javadoc 文檔中
其中, @Retention
是定義保留策略, 直接決定了我們用何種方式解析. SOUCE級(jí)別的注解是用來(lái)標(biāo)記的, 比如Override, SuppressWarnings. 我們真正使用的類(lèi)型是CLASS(編譯時(shí))和RUNTIME(運(yùn)行時(shí))
自定義注解
舉個(gè)栗子, 結(jié)合例子講解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface TestAnnotation {
String value();
String[] value2() default "value2";
}
元注解的的意義參考上面的講解, 不再重復(fù), 這里看注解值的寫(xiě)法:
類(lèi)型 參數(shù)名() default 默認(rèn)值;
其中默認(rèn)值是可選的, 可以定義, 也可以不定義.
處理運(yùn)行時(shí)注解
Retention的值為RUNTIME時(shí), 注解會(huì)保留到運(yùn)行時(shí), 因此使用反射來(lái)解析注解.
使用的注解就是上一步的@TestAnnotation
, 解析示例如下:
public class Demo {
@TestAnnotation("Hello Annotation!")
private String testAnnotation;
public static void main(String[] args) {
try {
// 獲取要解析的類(lèi)
Class cls = Class.forName("myAnnotation.Demo");
// 拿到所有Field
Field[] declaredFields = cls.getDeclaredFields();
for(Field field : declaredFields){
// 獲取Field上的注解
TestAnnotation annotation = field.getAnnotation(TestAnnotation.class);
if(annotation != null){
// 獲取注解值
String value = annotation.value();
System.out.println(value);
}
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
此處只演示了解析成員變量上的注解, 其他類(lèi)型與此類(lèi)似.
解析編譯時(shí)注解
解析編譯時(shí)注解需要繼承AbstractProcessor類(lèi), 實(shí)現(xiàn)其抽象方法
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv)
該方法返回ture表示該注解已經(jīng)被處理, 后續(xù)不會(huì)再有其他處理器處理; 返回false表示仍可被其他處理器處理.
處理示例:
// 指定要解析的注解
@SupportedAnnotationTypes("myAnnotation.TestAnnotation")
// 指定JDK版本
@SupportedSourceVersion(SourceVersion.RELEASE_7)
public class MyAnnotationProcesser extends AbstractProcessor {
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
for (TypeElement te : annotations) {
for (Element element : roundEnv.getElementsAnnotatedWith(te)) {
TestAnnotation testAnnotation = element.getAnnotation(TestAnnotation.class);
// do something
}
}
return true;
}
}
這里先大致介紹是怎么個(gè)套路, 接下來(lái)說(shuō)具體實(shí)踐過(guò)程.
Android中使用編譯時(shí)注解
注解是個(gè)什么東西我們已經(jīng)知道了, 也知道了如何解析注解. 我們下一步的目標(biāo)是如ButterKnife一般自動(dòng)生成代碼.
接下來(lái)的操作基于InteliJ IDEA(開(kāi)發(fā)注解及其解析類(lèi), 打出jar包)和Android Studio(實(shí)測(cè)使用情況)
note: AS的Android開(kāi)發(fā)環(huán)境中沒(méi)有
AbstractProcessor
類(lèi), 而我新建了Java Module后遇到了各種各樣的花式錯(cuò)誤(后面的報(bào)錯(cuò)之路會(huì)敘述), 無(wú)奈只能在IDEA中開(kāi)發(fā)并打出jar包
開(kāi)發(fā)注解庫(kù)
在IDEA中新建java項(xiàng)目, 并開(kāi)啟maven支持. 如果新建項(xiàng)目的頁(yè)面沒(méi)有maven選項(xiàng), 建好項(xiàng)目后右鍵項(xiàng)目目錄->"Add Framwork Support...", 選擇maven.
自定義編譯時(shí)注解
@Retention(RetentionPolicy.CLASS)
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.TYPE})
public @interface TestAnnotation {
String value() default "Hello Annotation";
}
解析編譯時(shí)注解
// 支持的注解類(lèi)型, 此處要填寫(xiě)全類(lèi)名
@SupportedAnnotationTypes("myannotation.TestAnnotation")
// JDK版本, 我用的是java7
@SupportedSourceVersion(SourceVersion.RELEASE_7)
public class MyAnnotationProcessor extends AbstractProcessor {
// 類(lèi)名的前綴后綴
public static final String SUFFIX = "AutoGenerate";
public static final String PREFIX = "My_";
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
for (TypeElement te : annotations) {
for (Element e : roundEnv.getElementsAnnotatedWith(te)) {
// 準(zhǔn)備在gradle的控制臺(tái)打印信息
Messager messager = processingEnv.getMessager();
// 打印
messager.printMessage(Diagnostic.Kind.NOTE, "Printing: " + e.toString());
messager.printMessage(Diagnostic.Kind.NOTE, "Printing: " + e.getSimpleName());
messager.printMessage(Diagnostic.Kind.NOTE, "Printing: " + e.getEnclosingElement().toString());
// 獲取注解
TestAnnotation annotation = e.getAnnotation(TestAnnotation.class);
// 獲取元素名并將其首字母大寫(xiě)
String name = e.getSimpleName().toString();
char c = Character.toUpperCase(name.charAt(0));
name = String.valueOf(c+name.substring(1));
// 包裹注解元素的元素, 也就是其父元素, 比如注解了成員變量或者成員函數(shù), 其上層就是該類(lèi)
Element enclosingElement = e.getEnclosingElement();
// 獲取父元素的全類(lèi)名, 用來(lái)生成包名
String enclosingQualifiedName;
if(enclosingElement instanceof PackageElement){
enclosingQualifiedName = ((PackageElement)enclosingElement).getQualifiedName().toString();
}else {
enclosingQualifiedName = ((TypeElement)enclosingElement).getQualifiedName().toString();
}
try {
// 生成的包名
String genaratePackageName = enclosingQualifiedName.substring(0, enclosingQualifiedName.lastIndexOf('.'));
// 生成的類(lèi)名
String genarateClassName = PREFIX + enclosingElement.getSimpleName() + SUFFIX;
// 創(chuàng)建Java文件
JavaFileObject f = processingEnv.getFiler().createSourceFile(genarateClassName);
// 在控制臺(tái)輸出文件路徑
messager.printMessage(Diagnostic.Kind.NOTE, "Printing: " + f.toUri());
Writer w = f.openWriter();
try {
PrintWriter pw = new PrintWriter(w);
pw.println("package " + genaratePackageName + ";");
pw.println("\npublic class " + genarateClassName + " { ");
pw.println("\n /** 打印值 */");
pw.println(" public static void print" + name + "() {");
pw.println(" // 注解的父元素: " + enclosingElement.toString());
pw.println(" System.out.println(\"代碼生成的路徑: "+f.toUri()+"\");");
pw.println(" System.out.println(\"注解的元素: "+e.toString()+"\");");
pw.println(" System.out.println(\"注解的值: "+annotation.value()+"\");");
pw.println(" }");
pw.println("}");
pw.flush();
} finally {
w.close();
}
} catch (IOException x) {
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR,
x.toString());
}
}
}
return true;
}
}
看似代碼很長(zhǎng), 其實(shí)很好理解. 只做了兩件事, 1.解析注解并獲取需要的值 2.使用JavaFileObject
類(lèi)生成java代碼.
向JVM聲明解析器
我們的解析器雖然定義好了, 但是jvm并不知道, 也不會(huì)調(diào)用, 因此我們需要聲明.
如圖所示在java的同級(jí)目錄新建resources目錄, 新建META-INF/services/javax.annotation.processing.Processor
文件, 文件中填寫(xiě)你自定義的Processor全類(lèi)名
然后打出jar包以待使用(打包方式自行百度)
Android中使用
使用apt插件
項(xiàng)目根目錄gradle中buildscript
的dependencies
添加
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
module目錄的gradle中, 添加
apply plugin: 'android-apt'
代碼中調(diào)用
將之前打出的jar包導(dǎo)入項(xiàng)目中, 在MainActivity中寫(xiě)個(gè)測(cè)試方法
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
test();
}
@TestAnnotation("hehe")
public void test(){
}
運(yùn)行一遍項(xiàng)目之后, 代碼就會(huì)自動(dòng)生成.
以下是生成的代碼, 在路徑yourmodule/build/generated/source/apt/debug/yourpackagename
中:
public class My_MainActivityAutoGenerate {
/** 打印值 */
public static void printTest() {
// 注解的父元素: com.example.pan.androidtestdemo.MainActivity
System.out.println("代碼生成的路徑: file:/Users/Pan/AndroidStudioProjects/AndroidTestDemo/app/build/generated/source/apt/debug/My_MainActivityAutoGenerate.java");
System.out.println("注解的元素: test()");
System.out.println("注解的值: hehe");
}
}
然后在test方法中調(diào)用自動(dòng)生成的方法
@TestAnnotation("hehe")
public void test(){
My_MainActivityAutoGenerate.printTest();
}
會(huì)看到以下打印結(jié)果:
代碼生成的路徑: file:/Users/Pan/AndroidStudioProjects/AndroidTestDemo/app/build/generated/source/apt/debug/com/example/pan/androidtestdemo/MainActivityAutoGenerate.java
注解的元素: test()
注解的值: hehe
報(bào)錯(cuò)之路
開(kāi)始時(shí), 我在Android Studio的Java Library中編寫(xiě)解析類(lèi), 然后在Android Module依賴Java庫(kù), 然后報(bào)下面這個(gè)錯(cuò)誤
For more information see https://docs.gradle.org/current/userguide/build_environment.html
Error:Error converting bytecode to dex:
Cause: Dex cannot parse version 52 byte code.
This is caused by library dependencies that have been compiled using Java 8 or above.
If you are using the 'java' gradle plugin in a library submodule add
targetCompatibility = '1.7'
sourceCompatibility = '1.7'
to that submodule's build.gradle file.
我tm本來(lái)就是Java8啊, 一番Google, 需要開(kāi)啟手動(dòng)開(kāi)啟才能支持java8, 步驟如下:
android {
compileSdkVersion 23
// 開(kāi)啟Java8, buildTools版本必須24以上
buildToolsVersion "24"
...
defaultConfig {
...
// Java8需要jack工具鏈支持
jackOptions{
enabled true
}
}
...
// 指定編譯版本
compileOptions{
targetCompatibility = '1.8'
sourceCompatibility = '1.8'
}
}
然而...又報(bào)了這個(gè)錯(cuò)誤
Error: Could not find the property 'options' on the task' : app: compileDebugJavaWithJack '.
來(lái)自JakeWharton大神的回復(fù), jack編譯器目前并不支持apt插件https://github.com/JakeWharton/butterknife/issues/571
摔! 不用java8報(bào)錯(cuò), 用了又尼瑪報(bào). 自動(dòng)生成代碼是必須要用apt插件的. 那就只能用java7在IDEA里開(kāi)發(fā)了.
時(shí)至今日(2016年06月23日), Google并沒(méi)有解決這個(gè)問(wèn)題, 目前jack編譯器還處于預(yù)覽版, 相信以后會(huì)解決吧
總結(jié)
有了本文所述的注解知識(shí), 對(duì)Dagger,ButterKnife等框架就不難理解了. 如果在時(shí)間精力允許的情況下, 我們也完全可以自定義個(gè)注解框架.
本文中自動(dòng)生成代碼的部分十分簡(jiǎn)單, 也隱含bug: 在for循環(huán)中創(chuàng)建了文件, 如果一個(gè)類(lèi)中使用了兩次該注解, 第二次是無(wú)法創(chuàng)建新文件的. 真正的實(shí)際項(xiàng)目中, 肯定是將需要的信息保存起來(lái), 之后統(tǒng)一創(chuàng)建java類(lèi).
更進(jìn)一步的應(yīng)用大家可以查看其他注解框架的源碼, 調(diào)試注解大家可以查看這篇文章如何debug自定義AbstractProcessor, 我這里就不過(guò)多贅述了
水平有限, 如有錯(cuò)誤歡迎指正.
參考鏈接
公共技術(shù)點(diǎn)之 Java 注解 Annotation