Android自定義注解原理及使用技巧

現(xiàn)在分析使用各種第三方庫,諸如ARouter钾唬、DBFlowDagger2侠驯、ButterKnife等抡秆,自定義注解都是繞不過去的點(diǎn)。所以本文在此重新說叨一下Android的自定義注解吟策,并分享一些自定義注解使用技巧給大家儒士。

[TOC]

注解概念

注解是代碼里的特殊標(biāo)記,這些標(biāo)記可以在編譯檩坚、類加載着撩、運(yùn)行時(shí)被讀取,并執(zhí)行相應(yīng)的處理匾委。這些額外的工作包含但不限于比如用于生成Java doc拖叙,比如編譯時(shí)進(jìn)行格式檢查,比如自動(dòng)生成代碼等赂乐。

定義注解用的關(guān)鍵字是@interface

JDK定義的元注解

Java提供了四種元注解薯鳍,專門負(fù)責(zé)新注解的創(chuàng)建工作,即注解其他注解踱讨。

@Target

定義了Annotation所修飾的對象范圍桅打,取值:

? ElementType.CONSTRUCTOR: 用于描述構(gòu)造器

? ElementType.FIELD: 用于描述域

? ElementType.LOCAL_VARIABLE: 用于描述局部變量

? ElementType.METHOD: 用于描述方法

? ElementType.PACKAGE: 用于描述包

? ElementType.PARAMETER: 用于描述參數(shù)

? ElementType.TYPE: 用于描述類晚缩、接口(包括注解類型) 或enum聲明

@Retention

定義了該Annotation作用時(shí)機(jī),及生成的文件的保留時(shí)間斩松,取值:

? RetentionPoicy.SOURCE: 注解保留在源代碼中,編譯過程中可見担租,編譯后會(huì)被編譯器所丟棄砸民,所以用于一些檢查性操作,編譯過程可見性分析等奋救。比如@Override, @SuppressWarnings

? RetentionPoicy.CLASS: 這是默認(rèn)的policy岭参。注解會(huì)被保留在class文件中,但是在運(yùn)行時(shí)期間就不會(huì)識(shí)別這個(gè)注解尝艘。用于生成一些輔助代碼演侯,輔助代碼生成之后,該注解的任務(wù)就結(jié)束了背亥。如ARouter秒际、ButterKnife等

? RetentionPoicy.RUNTIME: 注解會(huì)被保留在class文件中悬赏,同時(shí)運(yùn)行時(shí)期間也會(huì)被識(shí)別,和CLASS的差別也在此娄徊。所以可以在運(yùn)行時(shí)使用反射機(jī)制獲取注解信息闽颇。比如@Deprecated

@Inherited

是否可以被繼承,默認(rèn)為false寄锐。即子類自動(dòng)擁有和父類一樣的注解兵多。

@Documented

是否會(huì)保存到 Javadoc 文檔中。

Android SDK內(nèi)置的注解

Android SDK 內(nèi)置的注解都在包c(diǎn)om.android.support:support-annotations里橄仆,如:

? 資源引用限制類:用于限制參數(shù)必須為對應(yīng)的資源類型

@AnimRes @AnyRes @ArrayRes @AttrRes @BoolRes @ColorRes等

? 線程執(zhí)行限制類:用于限制方法或者類必須在指定的線程執(zhí)行

@AnyThread @BinderThread @MainThread @UiThread @WorkerThread

? 參數(shù)為空性限制類:用于限制參數(shù)是否可以為空

@NonNull @Nullable

? 類型范圍限制類:用于限制標(biāo)注值的值范圍

@FloatRang @IntRange

? 類型定義類:用于限制定義的注解的取值集合

@IntDef @StringDef

? 其他的功能性注解:

@CallSuper @CheckResult @ColorInt @Dimension @Keep @Px @RequiresApi @RequiresPermission @RestrictTo @Size @VisibleForTesting

自定義注解實(shí)例

假定要實(shí)現(xiàn)這樣的注解功能:使用注解在Android Activity上指定path剩膘,然后根據(jù)類名獲取相應(yīng)的path并實(shí)現(xiàn)跳轉(zhuǎn)。

具體實(shí)現(xiàn)步驟如下

1) 創(chuàng)建Processor Module

File -- New Module -- Choose Java Library

確保該processor module package命名為 {base}.annotationprocessor

本例中module名定位為:compiler

2) 設(shè)置Processor Module Build Gradle

設(shè)置Java編譯版本盆顾,如主app和processor module都采用java 1.7怠褐,則設(shè)置如下:


// 主app module

compileOptions {

 sourceCompatibility JavaVersion.VERSION_1_7

 targetCompatibility JavaVersion.VERSION_1_7

}


// processor module

sourceCompatibility = "1.7"

targetCompatibility = "1.7"

添加谷歌Auto-Service支持


dependencies {

 implementation fileTree(dir: 'libs', include: ['*.jar'])

 implementation 'com.google.auto.service:auto-service:1.0-rc2'

}

3) 創(chuàng)建Annotation


@Retention(RetentionPolicy.CLASS)

@Target(ElementType.TYPE)

public @interface TrackName {

 String name() default "";

}

4) 創(chuàng)建自定義Processor

這一步是最重要的一個(gè)步驟,自定義注解之所以能實(shí)現(xiàn)相應(yīng)的功能您宪,就是看自定義Processor如何解析了奈懒。針對TrackName,我們需要做的就是在編譯期生成一個(gè)Java文件蚕涤,自動(dòng)將@TrackName標(biāo)注的類和標(biāo)注的信息記錄下來筐赔。

CustomProcessor必須繼承AbstractProcessor,并且需要使用Java提供的注解標(biāo)注:


@AutoService(Processor.class)

@SupportedSourceVersion(SourceVersion.RELEASE_7)

@SupportedAnnotationTypes({ANNOTATION_TYPE_TRACKNAME})

先定義一個(gè)通用接口


public interface IData {

 /**

 * 載入數(shù)據(jù)

 */

 void loadInto(Map<String, String> map);

}

TrackNameProcessor的實(shí)現(xiàn)如下:

@AutoService(Processor.class)
@SupportedSourceVersion(SourceVersion.RELEASE_7)
@SupportedAnnotationTypes({ANNOTATION_TYPE_TRACKNAME})
public class TrackNameProcessor extends AbstractProcessor {

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        if (set != null && !set.isEmpty()) {
            generateJavaClassFile(set, roundEnvironment);
            return true;
        }
        return false;
    }

    // 生成Java源文件
    private void generateJavaClassFile(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        // set of track
        Map<String, String> trackMap = new HashMap<>();
        // print on gradle console
        Messager messager = processingEnv.getMessager();

        // 遍歷annotations獲取annotation類型 @SupportedAnnotationTypes
        for (TypeElement te : annotations) {
            for (Element e : roundEnv.getElementsAnnotatedWith(te)) { // 獲取所有被annotation標(biāo)注的元素
                // 打印
                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());
                // 獲取注解
                TrackName annotation = e.getAnnotation(TrackName.class);
                // 獲取名稱
                String name = "".equals(annotation.name()) ? e.getSimpleName().toString() : annotation.name();
                // 保存映射信息
                trackMap.put(e.getSimpleName().toString(), name);
                messager.printMessage(Diagnostic.Kind.NOTE, "映射關(guān)系:" + e.getSimpleName().toString() + "-" + name);
            }
        }

        try {
            // 生成的包名
            String genaratePackageName = "com.xud.annotationprocessor";
            // 生成的類名
            String genarateClassName = "TrackManager$Helper";

            // 創(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 + ";\n");
                pw.println("import java.util.Map;");
                pw.println("import com.xud.annotationprocessor.IData;\n");
                pw.println("/**");
                pw.println("* this file is auto-create by compiler,please don`t edit it");
                pw.println("* 頁面路徑映射關(guān)系表");
                pw.println("*/");
                pw.println("public class " + genarateClassName + " implements IData {");

                pw.println("\n    @Override");
                pw.println("    public void loadInto(Map<String, String> map) {");
                Iterator<String> keys = trackMap.keySet().iterator();
                while (keys.hasNext()) {
                    String key = keys.next();
                    String value = trackMap.get(key);
                    pw.println("        map.put(\"" + key + "\",\"" + value + "\");");
                }
                pw.println("    }");
                pw.println("}");
                pw.flush();
            } finally {
                w.close();
            }
        } catch (IOException x) {
            processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, x.toString());
        }
    }
}

這里我把編譯中生成的類貼出來揖铜,如下:

package com.xud.annotationprocessor;

import java.util.Map;
import com.xud.annotationprocessor.IData;

/**
* this file is auto-create by compiler,please don`t edit it
* 頁面路徑映射關(guān)系表
*/
public class TrackManager$Helper implements IData {

    @Override
    public void loadInto(Map<String, String> map) {
        map.put("RxJavaActivity","/page/rxJava");
        map.put("CustomViewActivity","/page/customView");
        map.put("CoordinatorActivity","/page/coordinator");
        map.put("BActivity","/page/b");
        map.put("MainActivity","/main");
    }
}

5) Use

接下來就是如何在主app module中使用了茴丰。由于本例annotation 和 processor都寫在同一個(gè)module中,所以使用時(shí)通過如下方式引入即可:


api project(':compiler')

annotationProcessor project(':compiler')

為方便從統(tǒng)一的地方獲取Activity和path的映射信息天吓,創(chuàng)建TrackManager單例來獲然呒纭:

public interface TrackInfoProvide {

    /**
     * 通過類名查找足跡定義信息
     *
     * @param className
     * @return
     */
    String getTrackNameByClass(String className);

    /**
     * 將所有路徑信息打印出來
     */
    String getAllPagePath();

}

public class TrackManager implements TrackInfoProvide {

    private Map<String, String> trackNameMap;

    private static TrackManager instance;

    public static TrackManager getInstance() {
        if (instance == null) {
            instance = new TrackManager();
        }
        return instance;
    }


    private TrackManager() {
        trackNameMap = new HashMap<String,String>();
        String classFullName = "com.xud.annotationprocessor.TrackManager$Helper";
        try {
            Class<?> clazz = Class.forName(classFullName);
            IData data = (IData)clazz.newInstance();
            data.loadInto(trackNameMap);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Override
    public String getTrackNameByClass(String className) {
        String output = className;
        if(trackNameMap != null && !trackNameMap.isEmpty()) {
            String value = trackNameMap.get(className);
            output = (value == null?output:value);
        }
        return output;
    }

    @Override
    public String getAllPagePath() {
        if (trackNameMap.isEmpty()) {
            return "";
        }
        StringBuilder builder = new StringBuilder();
        for (Map.Entry<String, String> entry : trackNameMap.entrySet()) {
            builder.append("頁面:" + entry.getKey());
            builder.append("\t");
            builder.append("路徑:" + entry.getValue());
            builder.append("\n");
        }
        return builder.toString();
    }
}

這樣,就可以直接通過以下方法實(shí)現(xiàn)調(diào)用


TrackManager.getInstance().getAllPagePath();

TrackManager.getInstance().getTrackNameByClass("MainActivity");

自定義注解使用技巧

  1. 從結(jié)構(gòu)上來講龄寞,應(yīng)將Annotation定義和Annotation Processor實(shí)現(xiàn)分別寫到不同的module汰规,方便調(diào)用方按需使用;

  2. Processor生成的代碼應(yīng)該面向接口編程物邑,以示例代碼為例溜哮,面向接口IData生成代碼,將信息寫入IData傳入的Map對象中色解,這就無需關(guān)心生成的代碼結(jié)構(gòu)茂嗓,在實(shí)現(xiàn)中應(yīng)用反射創(chuàng)建IData的實(shí)例并依接口調(diào)用。

關(guān)于第二點(diǎn)科阎,這里簡要的說說ARouter的實(shí)現(xiàn):

假設(shè)loginModule中的MainActivity使用了ARouter述吸,其注解為@Route(path = "/loginModule/main"),則ARouter編譯時(shí)會(huì)生成文件

ARouter$$Root$$loginModule
“ARouter$$Group$$loginModule”

public class ARouter$$Root$$loginModule implements IRouteRoot {
  @Override
  public void loadInto(Map<String, Class<? extends IRouteGroup>> routes) {
    routes.put("loginModule", ARouter$$Group$$loginModule.class);
  }
}

public class ARouter$$Group$$loginModule implements IRouteGroup {
  @Override
  public void loadInto(Map<String, RouteMeta> atlas) {
    atlas.put("/loginModule/main", RouteMeta.build(RouteType.ACTIVITY, MainActivity.class, "/loginmodule/main", "loginmodule", null, -1, -2147483648));
  }
}

public interface IRouteRoot {

    /**
     * Load routes to input
     * @param routes input
     */
    void loadInto(Map<String, Class<? extends IRouteGroup>> routes);
}

public interface IRouteGroup {
    /**
     * Fill the atlas with routes in group.
     */
    void loadInto(Map<String, RouteMeta> atlas);
}

ARouter在初始化的時(shí)候需要將這個(gè)映射關(guān)系載入內(nèi)存的锣笨,其載入的方式就是通過反射來操作的蝌矛,具體實(shí)現(xiàn)在源碼中的類 LogisticsCenter道批,其中有一段代碼如下,有興趣的讀者可以詳閱源碼:

// These class was generate by arouter-compiler.
List<String> classFileNames = ClassUtils.getFileNameByPackageName(mContext, ROUTE_ROOT_PAKCAGE);

for (String className : classFileNames) {
    if (className.startsWith(ROUTE_ROOT_PAKCAGE + DOT + SDK_NAME + SEPARATOR + SUFFIX_ROOT)) {
        // This one of root elements, load root.
        ((IRouteRoot) (Class.forName(className).getConstructor().newInstance())).loadInto(Warehouse.groupsIndex);
    } else if (className.startsWith(ROUTE_ROOT_PAKCAGE + DOT + SDK_NAME + SEPARATOR + SUFFIX_INTERCEPTORS)) {
        // Load interceptorMeta
        ((IInterceptorGroup) (Class.forName(className).getConstructor().newInstance())).loadInto(Warehouse.interceptorsIndex);
    } else if (className.startsWith(ROUTE_ROOT_PAKCAGE + DOT + SDK_NAME + SEPARATOR + SUFFIX_PROVIDERS)) {
        // Load providerIndex
        ((IProviderGroup) (Class.forName(className).getConstructor().newInstance())).loadInto(Warehouse.providersIndex);
    }
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末入撒,一起剝皮案震驚了整個(gè)濱河市隆豹,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌茅逮,老刑警劉巖噪伊,帶你破解...
    沈念sama閱讀 217,084評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異氮唯,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)姨伟,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,623評論 3 392
  • 文/潘曉璐 我一進(jìn)店門惩琉,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人夺荒,你說我怎么就攤上這事瞒渠。” “怎么了技扼?”我有些...
    開封第一講書人閱讀 163,450評論 0 353
  • 文/不壞的土叔 我叫張陵伍玖,是天一觀的道長。 經(jīng)常有香客問我剿吻,道長窍箍,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,322評論 1 293
  • 正文 為了忘掉前任丽旅,我火速辦了婚禮椰棘,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘榄笙。我一直安慰自己邪狞,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,370評論 6 390
  • 文/花漫 我一把揭開白布茅撞。 她就那樣靜靜地躺著帆卓,像睡著了一般。 火紅的嫁衣襯著肌膚如雪米丘。 梳的紋絲不亂的頭發(fā)上剑令,一...
    開封第一講書人閱讀 51,274評論 1 300
  • 那天,我揣著相機(jī)與錄音蠕蚜,去河邊找鬼尚洽。 笑死,一個(gè)胖子當(dāng)著我的面吹牛靶累,可吹牛的內(nèi)容都是我干的腺毫。 我是一名探鬼主播癣疟,決...
    沈念sama閱讀 40,126評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼潮酒!你這毒婦竟也來了睛挚?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,980評論 0 275
  • 序言:老撾萬榮一對情侶失蹤急黎,失蹤者是張志新(化名)和其女友劉穎扎狱,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體勃教,經(jīng)...
    沈念sama閱讀 45,414評論 1 313
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡淤击,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,599評論 3 334
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了故源。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片污抬。...
    茶點(diǎn)故事閱讀 39,773評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖绳军,靈堂內(nèi)的尸體忽然破棺而出印机,到底是詐尸還是另有隱情,我是刑警寧澤门驾,帶...
    沈念sama閱讀 35,470評論 5 344
  • 正文 年R本政府宣布射赛,位于F島的核電站,受9級特大地震影響奶是,放射性物質(zhì)發(fā)生泄漏楣责。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,080評論 3 327
  • 文/蒙蒙 一聂沙、第九天 我趴在偏房一處隱蔽的房頂上張望腐魂。 院中可真熱鬧,春花似錦逐纬、人聲如沸蛔屹。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,713評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽兔毒。三九已至,卻和暖如春甸箱,著一層夾襖步出監(jiān)牢的瞬間育叁,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,852評論 1 269
  • 我被黑心中介騙來泰國打工芍殖, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留豪嗽,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,865評論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像龟梦,于是被迫代替她去往敵國和親隐锭。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,689評論 2 354

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