Android進(jìn)階之自定義注解

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中buildscriptdependencies添加

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

Annotation實(shí)戰(zhàn)【自定義AbstractProcessor】

android 中運(yùn)用apt自定義一個(gè)AbstractProcessor

打造一個(gè) Android 的注解庫(kù)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末鸦难,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子员淫,更是在濱河造成了極大的恐慌合蔽,老刑警劉巖,帶你破解...
    沈念sama閱讀 210,978評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件介返,死亡現(xiàn)場(chǎng)離奇詭異拴事,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)映皆,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,954評(píng)論 2 384
  • 文/潘曉璐 我一進(jìn)店門(mén)挤聘,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人捅彻,你說(shuō)我怎么就攤上這事组去。” “怎么了步淹?”我有些...
    開(kāi)封第一講書(shū)人閱讀 156,623評(píng)論 0 345
  • 文/不壞的土叔 我叫張陵从隆,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我缭裆,道長(zhǎng)键闺,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,324評(píng)論 1 282
  • 正文 為了忘掉前任澈驼,我火速辦了婚禮辛燥,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己挎塌,他們只是感情好徘六,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,390評(píng)論 5 384
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著榴都,像睡著了一般待锈。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上嘴高,一...
    開(kāi)封第一講書(shū)人閱讀 49,741評(píng)論 1 289
  • 那天竿音,我揣著相機(jī)與錄音,去河邊找鬼拴驮。 笑死春瞬,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的莹汤。 我是一名探鬼主播快鱼,決...
    沈念sama閱讀 38,892評(píng)論 3 405
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼纲岭!你這毒婦竟也來(lái)了抹竹?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 37,655評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤止潮,失蹤者是張志新(化名)和其女友劉穎窃判,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體喇闸,經(jīng)...
    沈念sama閱讀 44,104評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡袄琳,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,451評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了燃乍。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片唆樊。...
    茶點(diǎn)故事閱讀 38,569評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖刻蟹,靈堂內(nèi)的尸體忽然破棺而出逗旁,到底是詐尸還是另有隱情,我是刑警寧澤舆瘪,帶...
    沈念sama閱讀 34,254評(píng)論 4 328
  • 正文 年R本政府宣布片效,位于F島的核電站,受9級(jí)特大地震影響英古,放射性物質(zhì)發(fā)生泄漏淀衣。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,834評(píng)論 3 312
  • 文/蒙蒙 一召调、第九天 我趴在偏房一處隱蔽的房頂上張望膨桥。 院中可真熱鬧蛮浑,春花似錦、人聲如沸国撵。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,725評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)介牙。三九已至,卻和暖如春澳厢,著一層夾襖步出監(jiān)牢的瞬間环础,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,950評(píng)論 1 264
  • 我被黑心中介騙來(lái)泰國(guó)打工剩拢, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留线得,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,260評(píng)論 2 360
  • 正文 我出身青樓徐伐,卻偏偏與公主長(zhǎng)得像贯钩,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子办素,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,446評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 171,734評(píng)論 25 707
  • 整體Retrofit內(nèi)容如下: 1角雷、Retrofit解析1之前哨站——理解RESTful 2、Retrofit解析...
    隔壁老李頭閱讀 6,417評(píng)論 4 31
  • 給我一只手 讓我跟你走 給我一朵花 一顆向日葵 讓我種下整片藍(lán)天 給你最明亮性穿、最干凈勺三、最純粹的 眼神與期待 彩色泥...
    寐不成若閱讀 238評(píng)論 6 2
  • 汨羅江畔一躍兮,屈子縱身成魂需曾。 仿離騷以自比兮吗坚,繼而九歌天問(wèn)。 感夏日之炎炎兮呆万,如舊緒繞彤云商源。 天青色等煙雨兮,粽...
    許士健閱讀 219評(píng)論 0 2