APT技術(shù)

社會(huì)猶如一條船绸吸,每個(gè)人都要有掌舵的準(zhǔn)備。 — 易卜生

寫在前面

在上一篇文章《注解入門》最后提出了一個(gè)問題編譯時(shí)注解是什么甫菠?

注解的生命周期為RUNTIME稱為運(yùn)行時(shí)注解,注解的生命周期為CLASS就稱為編譯時(shí)注解。

APT(Annotation Processing Tool)技術(shù)就是通過編譯期解析注解,并且生成Java代碼的一種技術(shù),一般會(huì)結(jié)合Javapoet技術(shù)生成Java代碼哈误。

前期準(zhǔn)備

在正式講解APT技術(shù)之前菜谣,先認(rèn)識(shí)兩個(gè)知識(shí)點(diǎn)鸣皂,分別是Element和SPI機(jī)制荆陆,這是APT技術(shù)的基礎(chǔ)泡挺,一定要掌握。

1.Element

Element指的是一系列與之相關(guān)的接口集合缅帘,存在javax.lang.model.element包下失暂。Element代表的是程序的一個(gè)元素,可以是包扩借,類戒突,接口,屬性變量实牡,方法总滩,方法形參,泛型參數(shù)等元素。Element所代表的元素只在編譯器可見奸攻,用于保存元素在編譯期間的各種狀態(tài)新思。

Element.png
2.SPI機(jī)制

SPI(Service Provider Interface)的作用是為接口尋找服務(wù)實(shí)現(xiàn),在項(xiàng)目中通過配置文件為接口尋找服務(wù)實(shí)現(xiàn)。

下面就介紹如何配置文件:

SPI配置文件.png
  • 首先在main目錄下創(chuàng)建resources目錄洁灵。
  • 然后在resources目錄下創(chuàng)建META-INF目錄牍汹。
  • 接下來在META-INF目錄下創(chuàng)建services目錄有决。
  • 最后創(chuàng)建一個(gè)文件名為javax.annotaion.processing.Processor的文件篱瞎,其中的內(nèi)容就是為接口尋找服務(wù)實(shí)現(xiàn)的全類名赠幕。

正式講解

現(xiàn)在通過一個(gè)例子介紹APT技術(shù),在Android應(yīng)用開發(fā)中产雹,ButterKnife是一個(gè)很好用的第三方框架,不知道的同學(xué)請(qǐng)自行百度赢笨,這里就通過寫一個(gè)簡單的ButterKnife實(shí)現(xiàn)自動(dòng)綁定View的框架。

1.創(chuàng)建注解

新建一個(gè)Java Library Module毕匀,名稱為apt_annotation躁垛,并創(chuàng)建一個(gè)注解BindView土铺。

@Documented
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface BindView {

    int viewId() default 0;
}
2.創(chuàng)建注解處理器

新建一個(gè)Java Library Module,名稱為apt_processor拷况,并創(chuàng)建一個(gè)注解處理器AptProcessor,用來生成java代碼套菜。

public class AptProcessor extends AbstractProcessor {

    /**
     * 初始化函數(shù)
     *
     * @param processingEnv
     */
    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
    }

    /**
     * 指定Java版本號(hào)亲善,一般返回最新版本號(hào)
     * 可以使用@SupportedSourceVersion注解代替
     *
     * @return
     */
    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }

    /**
     * 指定可解析的注解類型
     * 可以使用@SupportedAnnotationTypes注解代替
     *
     * @return
     */
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> annotationTypes = new LinkedHashSet<>();
        annotationTypes.add(BindView.class.getCanonicalName());
        return annotationTypes;
    }

    /**
     * 核心函數(shù)
     *
     * @param annotations
     * @param roundEnv
     * @return
     */
    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        Map<String, Map<String, Set<Element>>> elementMap = parseElements(roundEnv.getElementsAnnotatedWith(BindView.class));
        generateJavaFile(elementMap);
        return true;
    }

    /**
     * 解析全部元素
     *
     * @param elements
     * @return
     */
    private Map<String, Map<String, Set<Element>>> parseElements(Set<? extends Element> elements) {
        Map<String, Map<String, Set<Element>>> elementMap = new LinkedHashMap<>();
        // 遍歷全部元素
        for (Element element : elements) {
            // 判斷當(dāng)前元素是否是屬性變量
            if (!element.getKind().isField()) {
                continue;
            }

            // 獲取屬性變量的上一級(jí)元素,即類元素
            TypeElement typeElement = (TypeElement) element.getEnclosingElement();
            // 獲取類名
            String typeName = typeElement.getSimpleName().toString();
            // 獲取類的上一級(jí)元素逗柴,即包元素
            PackageElement packageElement = (PackageElement) typeElement.getEnclosingElement();
            // 獲取包名
            String packageName = packageElement.getQualifiedName().toString();
            
            Map<String, Set<Element>> typeElementMap = elementMap.get(packageName);
            if (typeElementMap == null) {
                typeElementMap = new LinkedHashMap<>();
            }

            Set<Element> variableElements = typeElementMap.get(typeName);
            if (variableElements == null) {
                variableElements = new LinkedHashSet<>();
            }
            variableElements.add(element);

            typeElementMap.put(typeName, variableElements);
            elementMap.put(packageName, typeElementMap);
        }

        return elementMap;
    }

    /**
     * 生成Java文件
     *
     * @param elementMap
     */
    private void generateJavaFile(Map<String, Map<String, Set<Element>>> elementMap) {
        Set<Map.Entry<String, Map<String, Set<Element>>>> packageElements = elementMap.entrySet();
        for (Map.Entry<String, Map<String, Set<Element>>> packageEntry : packageElements) {

            String packageName = packageEntry.getKey();
            Map<String, Set<Element>> typeElementMap = packageEntry.getValue();

            Set<Map.Entry<String, Set<Element>>> typeElements = typeElementMap.entrySet();
            for (Map.Entry<String, Set<Element>> typeEntry : typeElements) {

                String typeName = typeEntry.getKey();
                Set<Element> variableElements = typeEntry.getValue();

                ClassName className = ClassName.get(packageName, typeName);

                FieldSpec.Builder fieldSpecBuilder = FieldSpec.builder(className, "target", Modifier.PRIVATE);

                MethodSpec.Builder bindMethodBuilder = MethodSpec.methodBuilder("bind")
                        .addModifiers(Modifier.PUBLIC)
                        .addParameter(className, "activity")
                        .addStatement("target = activity");

                MethodSpec.Builder unbindMethodBuilder = MethodSpec.methodBuilder("unbind")
                        .addAnnotation(Override.class)
                        .addModifiers(Modifier.PUBLIC);

                for (Element element : variableElements) {

                    VariableElement variableElement = (VariableElement) element;

                    String variableName = variableElement.getSimpleName().toString();
                    BindView bindView = variableElement.getAnnotation(BindView.class);

                    bindMethodBuilder.addStatement("target." + variableName + " = activity.findViewById(" + bindView.viewId() + ")");
                    unbindMethodBuilder.addStatement("target." + variableName + " = null");
                }

                unbindMethodBuilder.addStatement("target = null");

                TypeSpec.Builder typeBuilder = TypeSpec.classBuilder(typeName + "_ViewBinding")
                        .addModifiers(Modifier.PUBLIC)
                        .addSuperinterface(ClassName.get("com.chad.apt.binder", "UnBinder"))
                        .addField(fieldSpecBuilder.build())
                        .addMethod(bindMethodBuilder.build())
                        .addMethod(unbindMethodBuilder.build());

                JavaFile javaFile = JavaFile.builder(packageName, typeBuilder.build()).build();

                try {
                    javaFile.writeTo(processingEnv.getFiler());
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

由于BindView在apt_annotation中蛹头,并且通過Javapoet技術(shù)生成Java代碼,所以需要在apt_processor的build.gradle中引入apt_annotation和javapoet庫戏溺。如下圖:

apt_processor build.gradle.png
3.創(chuàng)建SPI配置文件

接下來在apt_processor中創(chuàng)建SPI配置文件渣蜗,創(chuàng)建步驟在前面已經(jīng)講過,這里不再復(fù)述旷祸,只看javax.annotation.processing.Processor文件內(nèi)容如何定義耕拷,如下圖的文件內(nèi)容為com.chad.apt.processor.AptProcessor,也就是為Processor接口尋找AptProcessor服務(wù)實(shí)現(xiàn)類托享。

javax.annotation.processing.Processor.png
4.創(chuàng)建綁定者

新建一個(gè)Android Library Module斑胜,名稱為apt_binder,并且創(chuàng)建一個(gè)綁定者AptBinder和一個(gè)UnBinder接口嫌吠,AptBinder中的bind函數(shù)通過反射機(jī)制創(chuàng)建注解處理器生成的Java類并綁定View止潘,UnBinder接口用來解綁View。

public class AptBinder {

    public static UnBinder bind(Activity activity) {
        try {
            Class<?> activityClass = activity.getClass();
            Class<?> viewBindingClass = Class.forName(activityClass.getName() + "_ViewBinding");
            Method bindMethod = viewBindingClass.getMethod("bind", activityClass);
            UnBinder unBinder = (UnBinder) viewBindingClass.newInstance();
            bindMethod.invoke(unBinder, activity);
            return unBinder;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
}

public interface UnBinder {

    void unbind();
}
5.如何使用

新建一個(gè)Android Module辫诅,名稱為app凭戴,并且將apt_annotation,apt_processor和apt_binder三個(gè)Library引入到app build.gradle炕矮。如下圖:

app build.gradle.png
  • 創(chuàng)建activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <TextView
        android:id="@+id/id_hello"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="50dp"
        app:layout_constraintBottom_toBottomOf="@+id/id_world"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/id_world"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="50dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="@+id/id_hello" />

</android.support.constraint.ConstraintLayout>
  • 創(chuàng)建MainActivity
public class MainActivity extends AppCompatActivity {

    private static final String TAG = MainActivity.class.getSimpleName();

    @BindView(viewId = R.id.id_hello)
    TextView mTvHello;
    @BindView(viewId = R.id.id_world)
    TextView mTvWorld;

    private UnBinder unBinder;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // 綁定
        unBinder = AptBinder.bind(this);

        mTvHello.setText("Hello!!!");
        mTvWorld.setText("World!!!");
        Log.d(TAG, "bind : mTvHello = " + mTvHello + " , mTvWorld = " + mTvWorld);

        // 解綁
        unBinder.unbind();
        Log.d(TAG, "unbind : mTvHello = " + mTvHello + " , mTvWorld = " + mTvWorld);
    }
}

在MainActivity中通過注解聲明TextView屬性變量么夫,在onCreate()函數(shù)中通過AptBinder.bind()實(shí)現(xiàn)自動(dòng)綁定View并返回一個(gè)UnBinder者冤,UnBinder調(diào)用unbind()函數(shù)用來解綁View。

然后rebuild就會(huì)自動(dòng)生成java代碼档痪,即build > generated > source > apt > debug/release > com.chad.annotation目錄下的MainActivity_ViewBinding.java文件涉枫。

public class MainActivity_ViewBinding implements UnBinder {
  private MainActivity target;

  public void bind(MainActivity activity) {
    target = activity;
    target.mTvHello = activity.findViewById(2131165252);
    target.mTvWorld = activity.findViewById(2131165253);
  }

  @Override
  public void unbind() {
    target.mTvHello = null;
    target.mTvWorld = null;
    target = null;
  }
}
6.工程目錄

下圖就是整個(gè)工程的目錄結(jié)構(gòu):

工程目錄.png

總結(jié)

APT技術(shù)可以在編譯期間生成Java代碼,對(duì)于注解而言腐螟,有效的避免了運(yùn)行時(shí)注解通過反射解析注解信息影響效率的問題愿汰。

SPI機(jī)制通過配置文件的方式使用起來很麻煩,即要?jiǎng)?chuàng)建目錄又要?jiǎng)?chuàng)建文件乐纸。Google的auto-service庫提供了一種簡單的配置方式衬廷,通過注解就可以解決繁瑣的創(chuàng)建過程,使用方式很簡單汽绢,這里就不詳細(xì)講解了吗跋。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市宁昭,隨后出現(xiàn)的幾起案子跌宛,更是在濱河造成了極大的恐慌,老刑警劉巖积仗,帶你破解...
    沈念sama閱讀 216,372評(píng)論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件疆拘,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡斥扛,警方通過查閱死者的電腦和手機(jī)入问,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來稀颁,“玉大人芬失,你說我怎么就攤上這事∝以睿” “怎么了棱烂?”我有些...
    開封第一講書人閱讀 162,415評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長阶女。 經(jīng)常有香客問我颊糜,道長,這世上最難降的妖魔是什么秃踩? 我笑而不...
    開封第一講書人閱讀 58,157評(píng)論 1 292
  • 正文 為了忘掉前任衬鱼,我火速辦了婚禮,結(jié)果婚禮上憔杨,老公的妹妹穿的比我還像新娘鸟赫。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,171評(píng)論 6 388
  • 文/花漫 我一把揭開白布抛蚤。 她就那樣靜靜地躺著台谢,像睡著了一般。 火紅的嫁衣襯著肌膚如雪岁经。 梳的紋絲不亂的頭發(fā)上朋沮,一...
    開封第一講書人閱讀 51,125評(píng)論 1 297
  • 那天,我揣著相機(jī)與錄音缀壤,去河邊找鬼樊拓。 笑死,一個(gè)胖子當(dāng)著我的面吹牛诉位,可吹牛的內(nèi)容都是我干的骑脱。 我是一名探鬼主播菜枷,決...
    沈念sama閱讀 40,028評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼苍糠,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了啤誊?” 一聲冷哼從身側(cè)響起岳瞭,我...
    開封第一講書人閱讀 38,887評(píng)論 0 274
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎蚊锹,沒想到半個(gè)月后瞳筏,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,310評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡牡昆,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,533評(píng)論 2 332
  • 正文 我和宋清朗相戀三年姚炕,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片丢烘。...
    茶點(diǎn)故事閱讀 39,690評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡柱宦,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出播瞳,到底是詐尸還是另有隱情掸刊,我是刑警寧澤,帶...
    沈念sama閱讀 35,411評(píng)論 5 343
  • 正文 年R本政府宣布赢乓,位于F島的核電站忧侧,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏牌芋。R本人自食惡果不足惜蚓炬,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,004評(píng)論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望躺屁。 院中可真熱鬧肯夏,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至余耽,卻和暖如春缚柏,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背碟贾。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評(píng)論 1 268
  • 我被黑心中介騙來泰國打工币喧, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人袱耽。 一個(gè)月前我還...
    沈念sama閱讀 47,693評(píng)論 2 368
  • 正文 我出身青樓杀餐,卻偏偏與公主長得像,于是被迫代替她去往敵國和親朱巨。 傳聞我的和親對(duì)象是個(gè)殘疾皇子史翘,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,577評(píng)論 2 353

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