Android編譯時注解

之前寫了注解基礎(chǔ)和運(yùn)行時注解這篇文章弧烤,里面使用運(yùn)行時注解來模仿ButterKnife綁定控件ID的功能昏翰,運(yùn)行時注解主要是運(yùn)行時使用反射來找到注解進(jìn)行一些操作棒卷;反射存在一定的性能問題衔肢,而且一般使用了注解的框架都是使用編譯時注解

仔細(xì)看了一下這篇文章久信,Android 如何編寫基于編譯時注解的項(xiàng)目,自己嘗試寫了代碼理解了一番蝴猪,感覺還是比較有意思的调衰;

實(shí)現(xiàn)原理

1.注解處理器(Annotation Processor):用來在編譯時掃描和處理注解,我們需要實(shí)現(xiàn)自己的注解處理器自阱,去處理我們自己的注解嚎莉,一般就是去生成我們需要的代碼文件;
2.我們實(shí)現(xiàn)的注解處理器會被打包成jar在編譯的過程中調(diào)用沛豌,為了讓java編譯器識別出這個自定義的注解處理器趋箩,我們需要注冊一下

  1. 需要使用到注解處理的插件,因?yàn)锳ndroid Studio原本是不支持注解處理器的

整個流程大概就是加派,我們先創(chuàng)建注解叫确,創(chuàng)建注解處理器,然后代碼中使用注解芍锦;在編譯的時候注解處理插件會使用我們的注解處理器去處理注解竹勉,生成相應(yīng)的代碼;

具體實(shí)現(xiàn)

創(chuàng)建注解

還是以實(shí)現(xiàn)一個ViewById注解為例娄琉,在項(xiàng)目中新創(chuàng)建一個Java Library次乓,模塊名為annotation用來保存所有注解吓歇,然后創(chuàng)建一個編譯時注解

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.CLASS)
public @interface ViewById {
    int value() default -1;
}

注解處理器的創(chuàng)建和注冊

注解處理器是必須放在一個Java Library中,所以創(chuàng)建一個annotator模塊票腰,用來實(shí)現(xiàn)注解處理器城看,這里通過創(chuàng)建類IocProcessor繼承AbstractProcessor重寫其中方法來實(shí)現(xiàn)一個注解處理器

@AutoService(Processor.class)
public class IocProcessor extends AbstractProcessor {
...

注冊有比較簡單的方法,只需要給IocProcessor加一個AutoService注解就可以實(shí)現(xiàn)注冊杏慰,這個注解需要依賴一個庫

implementation 'com.google.auto.service:auto-service:1.0-rc4'
implementation project(':annotation')

重寫init方法测柠,獲取一些有用的變量

    /**
     * 生成代碼用的
     */
    private Filer mFileUtils;

    /**
     * 跟元素相關(guān)的輔助類,幫助我們?nèi)カ@取一些元素相關(guān)的信息
     * - VariableElement  一般代表成員變量
     * - ExecutableElement  一般代表類中的方法
     * - TypeElement  一般代表代表類
     * - PackageElement  一般代表Package
     */
    private Elements mElementUtils;
    
    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        mFileUtils = processingEnv.getFiler();
        mElementUtils = processingEnv.getElementUtils();
    }

重寫getSupportedAnnotationTypes方法逃默,支持自己的注解

    /**
     * 添加需要支持的注解
     *
     * @return
     */
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> annotationTypes = new LinkedHashSet<String>();
        //添加需要支持的注解
        annotationTypes.add(ViewById.class.getCanonicalName());
        return annotationTypes;
    }

這個固定寫法是設(shè)置支持的版本

    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }

然后是最重要的process方法鹃愤,這個方法就是開始處理注解,這里是先保存獲取到的被注解的元素完域,以外部類為單元软吐,用ProxyInfo對象去保存一個類里面的所有被注解的元素;用mProxyMap去保存所有的ProxyInfo吟税;然后再一個個拿出來凹耙,使用了ProxyInfo對象去實(shí)現(xiàn)生成代碼

mProxyMap.clear();
        //獲取被注解的元素
        Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(ViewById.class);
        for (Element element : elements) {
            //檢查element類型
            if (!checkAnnotationValid(element, ViewById.class)) {
                return false;
            }
            //獲取到這個成員變量
            VariableElement variableElement = (VariableElement) element;
            //獲取到這個變量的外部類,所在的類
            TypeElement typeElement = (TypeElement) variableElement.getEnclosingElement();
            //獲取外部類的類名
            String qualifiedName = typeElement.getQualifiedName().toString();
            //一個類里面的注解都在一個ProxyInfo中處理
            ProxyInfo proxyInfo = mProxyMap.get(qualifiedName);
            if (proxyInfo == null) {
                proxyInfo = new ProxyInfo(mElementUtils, typeElement);
                mProxyMap.put(qualifiedName, proxyInfo);
            }
            //把這個注解保存到proxyInfo里面肠仪,用于實(shí)現(xiàn)功能
            ViewById annotation = variableElement.getAnnotation(ViewById.class);
            int id = annotation.value();
            proxyInfo.injectVariables.put(id, variableElement);
        }


        //生成類
        for (String key : mProxyMap.keySet()) {
            ProxyInfo proxyInfo = mProxyMap.get(key);
            try {
                //創(chuàng)建一個新的源文件肖抱,并返回一個對象以允許寫入它
                JavaFileObject jfo = mFileUtils.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());
            }
        }
        return true;

到這里其實(shí)都很好理解,拿到了注解的這個元素(對象)异旧,就知道它的一切信息意述,就可以去生成相應(yīng)的代碼,主要在于生成代碼吮蛹,是在上面代碼中的這個地方實(shí)現(xiàn)的

//創(chuàng)建一個新的源文件荤崇,并返回一個對象以允許寫入它
JavaFileObject jfo = mFileUtils.createSourceFile(
        proxyInfo.getProxyClassFullName(),
        proxyInfo.getTypeElement());
Writer writer = jfo.openWriter();
writer.write(proxyInfo.generateJavaCode());
writer.flush();
writer.close(); 

這里去創(chuàng)建Java對象,寫入代碼潮针,其中創(chuàng)建新的源文件需要傳入兩個參數(shù)术荤,一個是保存的文件的全路徑,你想保存在哪里就哪里隨便寫每篷,我是保存在被注解的這個變量所在類的同一個包下瓣戚,另一個參數(shù)是傳入一個基本元素,但是說實(shí)話焦读,這個基本元素我在網(wǎng)上查了很久子库,不知道這個東西有什么用,刪了也沒問題矗晃,直接刪了好了仑嗅;

代碼是通過proxyInfo.generateJavaCode()來獲取的相應(yīng)生成的代碼去生成對象的,是具體生成代碼的方法

    /**
     * 生成代碼
     *
     * @return
     */
    public String generateJavaCode() {
        StringBuilder builder = new StringBuilder();
        builder.append("http:// Generated code. Do not modify!\n");
        builder.append("package ").append(packageName).append(";\n\n");
        builder.append("import com.dhht.annotation.*;\n");
        builder.append("import com.dhht.annotation.R;\n");
        builder.append("import com.dhht.annotationlibrary.*;\n");
        builder.append('\n');

        builder.append("public class ").append(proxyClassName).append(" implements " + ProxyInfo.PROXY + "<" + typeElement.getQualifiedName() + ">");
        builder.append(" {\n");

        generateMethods(builder);
        builder.append('\n');

        builder.append("}\n");
        return builder.toString();
    }

就是使用字符串組建一下代碼,字符串拼接倒是非常簡單无畔,這里也沒有展示完全,可以看看拼接出來的代碼吠冤,我稍微優(yōu)化了一下格式浑彰;這里有專門生成代碼的庫可以使用,比字符串拼接好用拯辙,叫Javapoet

package com.dhht.annotation.activity;

import com.dhht.annotation.*;
import com.dhht.annotation.R;
import com.dhht.annotationlibrary.*;

public class MainActivity$$ViewInject implements ViewInject<com.dhht.annotation.activity.MainActivity> {
    @Override
    public void inject(com.dhht.annotation.activity.MainActivity host, Object source) {
        if (source instanceof android.app.Activity) {
            host.txtView = (android.widget.TextView) (((android.app.Activity) source).findViewById(R.id.txtView));
        } else {
            host.txtView = (android.widget.TextView) (((android.view.View) source).findViewById(R.id.txtView));
        }
    }
}

可以調(diào)用生成的這個類的inject()方法郭变,為傳入的host對象的.txtView控件進(jìn)行初始化,這里只是在MainActivity里面用了涯保,如果在其他類里面則會生成很多個這樣的文件诉濒,而且當(dāng)這個類不是Activity的時候需要傳入這個控件的根布局進(jìn)去;

調(diào)用生成的代碼

里面還有一些檢查參數(shù)是不是公共的呀夕春,注解的對象屬性是否正確呀未荒,具體的代碼生成可以下載源碼看看,都是比較簡單的及志;現(xiàn)在注解器處理器算是寫完了片排,需要在我們的項(xiàng)目中使用,我們也新建一個Android Library速侈,annotationlibrary專門用于提供API率寡,這樣注解的實(shí)現(xiàn)完全和我們的項(xiàng)目分開;

//這個依賴是用于對外暴露注解的
api project(':annotation')

需要實(shí)現(xiàn)的功能很簡單,就是調(diào)用生成的代碼倚搬,首先不同的類里面的注解生成的代碼類是不一樣的冶共,而且生成的代碼是編譯的時候才生成的,肯定只能使用反射來獲取這個生成的類每界,所以肯定需要傳入使用注解的這個類捅僵,然后根據(jù)我們的命名規(guī)則獲取到;

    /**
     * 根據(jù)使用注解的類和約定的命名規(guī)則盆犁,反射獲取注解生成的類
     *
     * @param object
     * @return
     */
    private static ViewInject findProxyActivity(Object object) {
        try {
            Class clazz = object.getClass();
            Class injectorClazz = Class.forName(clazz.getName() + SUFFIX);
            return (ViewInject) injectorClazz.newInstance();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
        throw new RuntimeException(String.format("can not find %s , something when compiler.", object.getClass().getSimpleName() + SUFFIX));
    }

再說我們這個綁定控件命咐,從生成的代碼來看,需要一個Activity或者一個View來調(diào)用findViewById方法谐岁,所以使用注解的不是Activity的類醋奠,在需要加一個Object參數(shù),傳入View進(jìn)來伊佃;

public interface ViewInject<T> {
    /**
     * 提供給生成的代碼去綁定id用的
     *
     * @param t
     * @param source
     */
    void inject(T t, Object source);
}

然后考慮到Activity就不用傳另一個參數(shù)了窜司,所以新建兩個方法完事兒

public static void injectView(Activity activity) {
        ViewInject proxyActivity = findProxyActivity(activity);
        proxyActivity.inject(activity, activity);
    }

    public static void injectView(Object object, View view) {
        ViewInject proxyActivity = findProxyActivity(object);
        proxyActivity.inject(object, view);
    }

然后在項(xiàng)目中依賴一下,并且使用之前說的注解插件

implementation project(':annotationlibrary')
annotationProcessor project(':annotator')

然后就可以在項(xiàng)目中使用注解了

    @ViewById
    TextView txtView;
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ViewInjector.injectView(this);
        txtView.setOnClickListener(v -> Toast.makeText(MainActivity.this, "醉了", Toast.LENGTH_SHORT).show());
    }

使用了lambda 表達(dá)式簡單了不少航揉,在模塊的build.gradle的android節(jié)點(diǎn)下面添加支持

compileOptions {
        sourceCompatibility = '1.8'
        targetCompatibility = '1.8'
    }

就完成了塞祈,整體還是比較好理解的,關(guān)鍵在于得下載代碼自己試試

項(xiàng)目地址:https://github.com/tyhjh/Annotation

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末帅涂,一起剝皮案震驚了整個濱河市议薪,隨后出現(xiàn)的幾起案子尤蛮,更是在濱河造成了極大的恐慌,老刑警劉巖斯议,帶你破解...
    沈念sama閱讀 216,544評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件产捞,死亡現(xiàn)場離奇詭異,居然都是意外死亡哼御,警方通過查閱死者的電腦和手機(jī)坯临,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,430評論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來恋昼,“玉大人看靠,你說我怎么就攤上這事∫杭。” “怎么了挟炬?”我有些...
    開封第一講書人閱讀 162,764評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長嗦哆。 經(jīng)常有香客問我辟宗,道長,這世上最難降的妖魔是什么吝秕? 我笑而不...
    開封第一講書人閱讀 58,193評論 1 292
  • 正文 為了忘掉前任泊脐,我火速辦了婚禮,結(jié)果婚禮上烁峭,老公的妹妹穿的比我還像新娘容客。我一直安慰自己,他們只是感情好约郁,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,216評論 6 388
  • 文/花漫 我一把揭開白布缩挑。 她就那樣靜靜地躺著,像睡著了一般鬓梅。 火紅的嫁衣襯著肌膚如雪供置。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,182評論 1 299
  • 那天绽快,我揣著相機(jī)與錄音芥丧,去河邊找鬼。 笑死坊罢,一個胖子當(dāng)著我的面吹牛续担,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播活孩,決...
    沈念sama閱讀 40,063評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼物遇,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起询兴,我...
    開封第一講書人閱讀 38,917評論 0 274
  • 序言:老撾萬榮一對情侶失蹤乃沙,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后诗舰,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體崔涂,經(jīng)...
    沈念sama閱讀 45,329評論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,543評論 2 332
  • 正文 我和宋清朗相戀三年始衅,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片缭保。...
    茶點(diǎn)故事閱讀 39,722評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡汛闸,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出艺骂,到底是詐尸還是另有隱情诸老,我是刑警寧澤,帶...
    沈念sama閱讀 35,425評論 5 343
  • 正文 年R本政府宣布钳恕,位于F島的核電站别伏,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏忧额。R本人自食惡果不足惜厘肮,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,019評論 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望睦番。 院中可真熱鬧类茂,春花似錦、人聲如沸托嚣。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,671評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽示启。三九已至兢哭,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間夫嗓,已是汗流浹背迟螺。 一陣腳步聲響...
    開封第一講書人閱讀 32,825評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留舍咖,地道東北人煮仇。 一個月前我還...
    沈念sama閱讀 47,729評論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像谎仲,于是被迫代替她去往敵國和親浙垫。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,614評論 2 353