Android編譯時注解

Android編譯時注解

[TOC]

前言

相信大家經(jīng)常都使用到注解冯勉,如果使用過AndroidAnnotations,Dagger2,EventBus,RxJava,BufferKnife等開源項目,對注解應該更為深刻盔几,這些項目的原理基本都是基于編譯時注解動態(tài)生成代碼,效果等同于手寫代碼掩幢,因此相對反射來說性能基本無影響逊拍。

另外,已經(jīng)實現(xiàn)了注解輕松實現(xiàn)線程切換開源項目际邻,歡迎fork&star.

了解注解

注解的概念

注解(Annotation)芯丧,也叫元數(shù)據(jù)(Metadata),是Java5的新特性世曾,JDK5引入了Metadata很容易的就能夠調(diào)用Annotations缨恒。注解與類、接口度硝、枚舉在同一個層次肿轨,并可以應用于包、類型蕊程、構(gòu)造方法、方法驼唱、成員變量藻茂、參數(shù)、本地變量的聲明中玫恳,用來對這些元素進行說明注釋辨赐。

注解的語法與定義

  1. 以@interface關(guān)鍵字定義
  2. 注解可以包含成員,成員以無參數(shù)的方法的形式被聲明京办,其方法名和返回值定義了該成員的名字和類型掀序。
  3. 成員賦值是通過@Annotation(name=value)的形式。
  4. 注解需要標明注解的生命周期惭婿,注解的修飾目標等信息不恭,這些信息是通過元注解實現(xiàn)叶雹。

以 java.lang.annotation 中定義的 Target 注解為例:

@Retention(value = RetentionPolicy.RUNTIME)
@Target(value = { ElementType.ANNOTATION_TYPE } )
public @interface Target {
    ElementType[] value();
}

源碼分析如下:
第一:元注解@Retention,成員value的值為RetentionPolicy.RUNTIME换吧。
第二:元注解@Target折晦,成員value是個數(shù)組,用{}形式賦值沾瓦,值為ElementType.ANNOTATION_TYPE
第三:成員名稱為value满着,類型為ElementType[]
另外,需要注意一下贯莺,如果成員名稱是value风喇,在賦值過程中可以簡寫。如果成員類型為數(shù)組缕探,但是只賦值一個元素魂莫,則也可以簡寫。如上面的簡寫形式為:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Target {
    ElementType[] value();
}

注解的分類

1 基本內(nèi)置注解撕蔼,是指Java自帶的幾個Annotation豁鲤,如@Override、Deprecated鲸沮、@SuppressWarnings等琳骡;
2 元注解(meta-annotation),是指負責注解其他注解的注解讼溺,JDK 1.5及以后版本定義了4個標準的元注解類型楣号,如下:

@Target
@Retention
@Documented
@Inherited

3 自定義注解,根據(jù)需要可以自定義注解怒坯,自定義注解需要用到上面的meta-annotation

元注解

  • Java定義了4個標準的元注解( <span style="color:#F00">java8之后新增了@Repeatable元注解</span>):
  • @Documented:標記注解炫狱,注解表明這個注解應該被 javadoc工具記錄. 默認情況下,javadoc是不包括注解的. 但如果聲明注解時指定了 @Documented,則它會被 javadoc 之類的工具處理, 所以注解類型信息也會被包括在生成的文檔中。
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Documented {
}

  • @Inherited:標記注解剔猿,允許子類繼承父類的注解视译,此注解理解有難度,可以參考這里归敬,總的來說就是子類在繼承父類時如果父類上的注解有此@Inherited標記酷含,那么子類就能把父類的這個注解繼承下來,如果沒有@Inherited標記汪茧,那么子類在繼承父類之后并沒有繼承父類的注解(不知道有沒有說明白了椅亚,不明白就還是點進鏈接去看下吧)。
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Inherited {
}

  • @Retention:指Annotation被保留的時間長短舱污,標明注解的生命周期
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Retention {
    RetentionPolicy value();
}

注解需要標明注解的生命周期呀舔,這些信息是通過元注解 @Retention 實現(xiàn),注解的值是 enum 類型的 RetentionPolicy扩灯,包括以下幾種情況:

public enum RetentionPolicy {
    /**
     * 注解只保留在源文件媚赖,當Java文件編譯成class文件的時候霜瘪,注解被遺棄.
     * 這意味著注解僅存在于編譯器處理期間,編譯器處理完之后省古,該注解就沒用了粥庄,在class文件找不到了
     */
    SOURCE,

    /**
     * 注解被保留到class文件,但jvm加載class文件時候被遺棄豺妓,這是默認的生命周期.
     * 簡單來說就是你在class文件中還能看到注解
     */
    CLASS,

    /**
     * 注解不僅被保存到class文件中惜互,jvm加載class文件之后,仍然存在琳拭,
     * 保存到class對象中训堆,可以通過反射來獲取
     */
    RUNTIME
}

  • @Target:標明注解的修飾目標( <span style="color:#F00">java8為ElementType枚舉增加了TYPE_PARAMETERTYPE_USE兩個枚舉值</span>)
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Target {
    ElementType[] value();
}

// ElementType取值
public enum ElementType {
    /** 類白嘁、接口(包括注解類型)或枚舉 */
    TYPE,
    /** field屬性坑鱼,也包括enum常量使用的注解 */
    FIELD,
    /** 方法 */
    METHOD,
    /** 參數(shù) */
    PARAMETER,
    /** 構(gòu)造函數(shù) */
    CONSTRUCTOR,
    /** 局部變量 */
    LOCAL_VARIABLE,
    /** 注解上使用的元注解 */
    ANNOTATION_TYPE,
    /** 包 */
    PACKAGE
}

注解處理器(Annotation Processor)

概述

注解處理器是javac的一個工具,它用來在編譯時掃描和處理注解(Annotation)絮缅。你可以自定義注解鲁沥,并注冊到相應的注解處理器,由注解處理器來處理你的注解耕魄。一個注解的注解處理器画恰,以Java代碼(或者編譯過的字節(jié)碼)作為輸入,生成文件(通常是.java文件)作為輸出吸奴。這些生成的Java代碼是在生成的.java文件中允扇,所以你不能修改已經(jīng)存在的Java類,例如向已有的類中添加方法则奥。這些生成的Java文件考润,會同其他普通的手動編寫的Java源代碼一樣被javac編譯。

簡單來說读处,在源代碼編譯階段糊治,通過注解處理器,將標記了注解的類罚舱、方法等作為輸入內(nèi)容俊戳,經(jīng)過注解處理器進行處理,產(chǎn)生需要的java代碼馆匿。

Android Gradle插件2.2版本發(fā)布后,Android 官方提供了annotationProcessor來代替android-apt燥滑,annotationProcessor支持 javac 和 jack 編譯方式渐北,而android-apt只支持 javac 編譯方式。

使用

  • 直接在Module中使用铭拧,比之前Android-apt使用方式更加簡單赃蛛。
dependencies {
            compile 'com.github.huweijian5:AwesomeTool:latest_version'
        annotationProcessor 'com.github.huweijian5:AwesomeTool-compiler:latest_version'
}

實例說明

接下來以本人寫的一個注解實現(xiàn)線程切換的項目為例恃锉,講解下編譯時注解的編碼過程。

項目結(jié)構(gòu)

本項目主要分為三個Module呕臂,分別為lib_api破托,lib_annotation,lib_compiler歧蒋。
其中l(wèi)ib_api主要存放供外界使用的接口土砂,是對外開放的
lib_annotation里指定了自定義注解的定義
lib_compiler里實現(xiàn)注解處理器,是本項目的核心

  • 項目目錄結(jié)構(gòu)如下圖:


    這里寫圖片描述
  • 依賴關(guān)系圖如下:

app->lib_api:dependence
lib_api->lib_annotation: dependence
lib_compiler->lib_annnotaion: dependence

值得注意的是谜洽,lib_annotation和lib_compiler都是java工程(apply plugin: 'java')萝映,而lib_api是android工程(apply plugin: 'com.android.library')

lib_annotation

此Module主要實現(xiàn)自定義的注解定義

/**
 * 注入對象實例
 * Created by HWJ on 2017/3/12.
 */
@Documented
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface InjectObject {

    /**
     * 線程優(yōu)先級-20~19,-20代表優(yōu)先級最高,詳見android.os.Process,默認為THREAD_PRIORITY_DEFAULT(0)
     * @return
     */
    int priority() default 0;
}
/**
 * 后臺線程注解
 * Created by HWJ on 2017/3/12.
 */
@Documented
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.METHOD)
public @interface WorkInBackground {

}
/**
 1. UI線程注解
 2. Created by HWJ on 2017/3/12.
 */
@Documented
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.METHOD)
public @interface WorkInMainThread {
}

lib_api

  • 定義接口
 public interface IObjectInjector<T> {
    void inject(T t);
}
  • 利用反射進行注入實例
public static void inject(Object target) {

        //獲取生成類全稱
        Class<?> clazz = target.getClass();
        String proxyClassFullName = clazz.getName() + ConstantValue.SUFFIX;
        Class<?> proxyClazz = null;
        try {
            //反射生成類實例對象并進行注入
            proxyClazz = Class.forName(proxyClassFullName);
            IObjectInjector objectInjector = (IObjectInjector) proxyClazz.newInstance();
            objectInjector.inject(target);
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException("注入失敳椤:"+e.getMessage());
        }
    }

lib_compiler

lib_compiler中實現(xiàn)注解處理器序臂,是項目的核心,本項目是使用Handler和HandlerThread實現(xiàn)線程切換实束,對于HandlerThread線程切換的使用網(wǎng)絡(luò)文章已經(jīng)有很多奥秆,本文不再贅述。

1 添加以下依賴:

dependencies {

...

//auto-service庫可以幫我們?nèi)ド蒑ETA-INF等信息
compile 'com.google.auto.service:auto-service:1.0-rc4'
//用于生成源代碼
compile 'com.squareup:javapoet:1.9.0'

}

//只有android N支持java8咸灿,如果你寫1.8之后构订,強制要你使用buildToolsVersion為24.0.0
sourceCompatibility = "1.7"
targetCompatibility = "1.7"

...


注意此Module為java工程,而不是android工程析显,如果弄錯了就會報找不到類AbstractProcessor的錯誤


2 繼承AbstractProcessor,并實現(xiàn)方法init,getSupportedAnnotationTypes,getSupportedSourceVersion,process四個方法即可鲫咽,參考代碼如下:

@AutoService(Processor.class)
public class AwesomeToolProcessor extends AbstractProcessor {
    private static final String TAG = "AwesomeToolProcessor";
    private Filer mFileUtils;//跟文件相關(guān)的輔助類,生成JavaSourceCode
    private Elements elementUtils;//跟元素相關(guān)的輔助類谷异,幫助我們?nèi)カ@取一些元素相關(guān)的信息
    private Messager messager;//跟日志相關(guān)的輔助類
    private Map<String, AwesomeToolProxyInfo> proxyInfoMap = new HashMap<String, AwesomeToolProxyInfo>();//key為注解所在類的全名

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        messager = processingEnv.getMessager();
        elementUtils = processingEnv.getElementUtils();
        mFileUtils=processingEnv.getFiler();
    }

    /**
     * 支持的注解類型
     *
     * @return
     */
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        HashSet<String> supportTypes = new LinkedHashSet<>();
        supportTypes.add(InjectObject.class.getCanonicalName());
        supportTypes.add(WorkInBackground.class.getCanonicalName());
        supportTypes.add(WorkInMainThread.class.getCanonicalName());
        return supportTypes;
    }

    /**
     * 注解處理器支持到的JAVA版本
     * @return
     */
    @Override
    public SourceVersion getSupportedSourceVersion() {
        printMessage("SupportedSourceVersion=%s",SourceVersion.latestSupported().name());
        return SourceVersion.latestSupported();
    }

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        printMessage("process:annotations size=%d", annotations.size());
        proxyInfoMap.clear();
        handleInjectObjectAnnotation(roundEnv);
        handleWorkInBackgroundAnnotation(roundEnv);
        handleWorkInMainThreadAnnotation(roundEnv);

        printMessage("AwesomeToolProxyInfo Map size=%d", proxyInfoMap.size());

        generateSourceFiles();
        return false;//如果返回true,當有兩個注解作用在同一方法上分尸,那么第一個處理完了之后就不會再處理第二個
    }

注解@AutoService(Processor.class)可以自動幫我們處理一些工作,簡化代碼

init中可以獲得Messager用來打印信息歹嘹,打印的信息會顯示在AndroidStudio的Gradle Console窗口
同時也可以獲得Elements箩绍,用來獲取元素的相關(guān)信息,還有Filer,可以用來生成代碼

getSupportedAnnotationTypes里需要返回支持的注解類型尺上,就是lib_annotation中定義的注解

getSupportedSourceVersion為注解處理器支持到的java版本

process里處理注解元素作用的類方法等材蛛,根據(jù)自己的業(yè)務邏輯處理并生成相應代碼


3 處理注解

通過RoundEnvironment.getElementsAnnotatedWith()可以獲得注解所在的方法類等,如下

 Set<? extends Element> elesWithBind = roundEnv.getElementsAnnotatedWith(WorkInMainThread.class);
  • 其中Element的類型及說明如下:
類型 說明
ExecutableElement Represents a method, constructor, or initializer (static or instance) of a class or interface, including annotation type elements.
VariableElement Represents a field, {@code enum} constant, method or constructor parameter, local variable, resource variable, or exception parameter.
PackageElement Represents a package program element. Provides access to information about the package and its members.
  • 獲取方法參數(shù),參考代碼如下:
 for (VariableElement variableElement : executableElement.getParameters()) {
                System.out.println("參數(shù)類型及名稱:" + variableElement.asType() + "," + variableElement.getSimpleName());
            }

4 生成代碼

生成代碼的方式可以通過手動拼接字符串怎抛,也可以通過開源庫javapoet實現(xiàn)卑吭。

            try {
                JavaFileObject jfo = processingEnv.getFiler().createSourceFile(
                        proxyInfo.getProxyClassFullName(),//類名全稱
                        proxyInfo.getTypeElement());//類元素
                Writer writer = jfo.openWriter();
                writer.write(proxyInfo.generateJavaCode());
                writer.flush();
                writer.close();
            } catch (IOException e) {
                error(proxyInfo.getTypeElement(),
                        "Unable to write injector for type %s: %s",
                        proxyInfo.getTypeElement(), e.getMessage());
            }


至此已經(jīng)走完了編譯時注解的整個流程,最后貼下生成的代碼:

//Generated code. Do not modify!
//自動生成代碼马绝,請勿修改豆赏!
package com.junmeng.aad;

import android.os.Handler;
import android.os.HandlerThread;
import android.os.Message;

import com.junmeng.api.inter.IObjectInjector;

import java.lang.ref.WeakReference;

import java.util.ArrayList;
import java.util.List;

public class BlankFragmentHelper implements IObjectInjector<BlankFragment> {
    public static final int MESSAGE_NEEDWORKINTHREAD = 1;
    public static final int MESSAGE_NEEDWORKINMAINTHREAD = 2;
    private Handler mainHandler;
    private Handler workHandler;
    private HandlerThread handlerThread;
    private WeakReference<BlankFragment> target;

    @Override
    public void inject(final BlankFragment target) {
        if (target.blankFragmentHelper != null) {
            target.blankFragmentHelper.quit();
        }
        target.blankFragmentHelper = new BlankFragmentHelper();
        target.blankFragmentHelper.init(target);
    }

    public void init(final BlankFragment target) {
        this.target = new WeakReference<BlankFragment>(target);
        handlerThread = new HandlerThread("thread_BlankFragmentHelper", -16);
        handlerThread.start();
        mainHandler = new Handler() {
            @Override
            public void handleMessage(Message msg) {
                List<Object> params;
                switch (msg.what) {
                    case MESSAGE_NEEDWORKINMAINTHREAD:
                        params = (List<Object>) msg.obj;
                        target.needWorkInMainThread();
                        break;
                }
            }
        };
        workHandler = new Handler(handlerThread.getLooper()) {
            @Override
            public void handleMessage(Message msg) {
                List<Object> params;
                switch (msg.what) {
                    case MESSAGE_NEEDWORKINTHREAD:
                        params = (List<Object>) msg.obj;
                        target.needWorkInThread((java.lang.String) params.get(0), (int) params.get(1), (double) params.get(2), (com.junmeng.aad.Test) params.get(3));
                        break;
                }
            }
        };
    }

    public void needWorkInThread(java.lang.String str, int i, double d, com.junmeng.aad.Test test) {
        List<Object> params = new ArrayList<>();
        params.add(str);
        params.add(i);
        params.add(d);
        params.add(test);
        workHandler.sendMessage(workHandler.obtainMessage(MESSAGE_NEEDWORKINTHREAD, params));
    }

    public void needWorkInMainThread() {
        List<Object> params = new ArrayList<>();
        mainHandler.sendMessage(mainHandler.obtainMessage(MESSAGE_NEEDWORKINMAINTHREAD, params));
    }

    /**
     * 在不用時務必調(diào)用此方法,防止內(nèi)存泄漏
     */
    public void quit() {
        if (handlerThread != null && handlerThread.isAlive()) {
            handlerThread.quitSafely();
        }
    }
}

另外,說明下google的auto-service實際上會幫助我們生成jar包并添加META-INF信息掷邦,如下圖


這里寫圖片描述

如有錯誤之處請指正白胀,謝謝。

待解決的關(guān)鍵問題

  • 按照上面的實現(xiàn)只能在build的時候才能生成代碼抚岗,有沒有即時生成的技術(shù)呢或杠?還沒找到答案。

參考:

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末宣蔚,一起剝皮案震驚了整個濱河市向抢,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌件已,老刑警劉巖笋额,帶你破解...
    沈念sama閱讀 217,826評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異篷扩,居然都是意外死亡兄猩,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,968評論 3 395
  • 文/潘曉璐 我一進店門鉴未,熙熙樓的掌柜王于貴愁眉苦臉地迎上來枢冤,“玉大人,你說我怎么就攤上這事铜秆⊙驼妫” “怎么了?”我有些...
    開封第一講書人閱讀 164,234評論 0 354
  • 文/不壞的土叔 我叫張陵连茧,是天一觀的道長核蘸。 經(jīng)常有香客問我,道長啸驯,這世上最難降的妖魔是什么客扎? 我笑而不...
    開封第一講書人閱讀 58,562評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮罚斗,結(jié)果婚禮上徙鱼,老公的妹妹穿的比我還像新娘。我一直安慰自己针姿,他們只是感情好袱吆,可當我...
    茶點故事閱讀 67,611評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著距淫,像睡著了一般绞绒。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上榕暇,一...
    開封第一講書人閱讀 51,482評論 1 302
  • 那天处铛,我揣著相機與錄音饲趋,去河邊找鬼。 笑死撤蟆,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的堂污。 我是一名探鬼主播家肯,決...
    沈念sama閱讀 40,271評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼盟猖!你這毒婦竟也來了号杠?” 一聲冷哼從身側(cè)響起育苟,我...
    開封第一講書人閱讀 39,166評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后劳景,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,608評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡烈钞,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,814評論 3 336
  • 正文 我和宋清朗相戀三年先匪,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片你弦。...
    茶點故事閱讀 39,926評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡惊豺,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出禽作,到底是詐尸還是另有隱情尸昧,我是刑警寧澤,帶...
    沈念sama閱讀 35,644評論 5 346
  • 正文 年R本政府宣布旷偿,位于F島的核電站烹俗,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏萍程。R本人自食惡果不足惜幢妄,卻給世界環(huán)境...
    茶點故事閱讀 41,249評論 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望尘喝。 院中可真熱鬧磁浇,春花似錦、人聲如沸朽褪。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,866評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽缔赠。三九已至衍锚,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間嗤堰,已是汗流浹背戴质。 一陣腳步聲響...
    開封第一講書人閱讀 32,991評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人告匠。 一個月前我還...
    沈念sama閱讀 48,063評論 3 370
  • 正文 我出身青樓戈抄,卻偏偏與公主長得像,于是被迫代替她去往敵國和親后专。 傳聞我的和親對象是個殘疾皇子划鸽,可洞房花燭夜當晚...
    茶點故事閱讀 44,871評論 2 354

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