安卓進階指南Annotation自定義編譯時注解(四)

曾經(jīng)看過一篇使用運行時注解來實現(xiàn)類似 ButterKnife 功能的文章另假。直到后來我自己看了ButterKnife 源碼后才發(fā)現(xiàn)并不是這樣。推薦閱讀 ButterKnife 原理解析,這是 Butterknife 源碼地址,不妨 clone 下來看一看瞧一瞧励幼。

ACBgkflEgD.png

代碼地址:android-annotation-tutorial

反射是一個我們在運行時讀取一個類及其成員屬性,并嘗試修改這些屬性的過程口柳。 這個過程雖然有助于創(chuàng)建一個通用或獨立于實現(xiàn)的程序苹粟,但是由于我們不知道運行時的確切條件,因此也容易出現(xiàn)大量異常跃闹。 通過反射進行類掃描和修改是一個緩慢的過程嵌削,也是一種孤立代碼的丑陋方式毛好。

一、示例:

為了更好的理解編譯時注解苛秕,我們先使用運行時注解來實現(xiàn)綁定控件

  • 定義注釋BindView以進行映射
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface BindView {
    int value();
}
  • 將BindView注釋放在具有視圖ID的View類變量上
public class MainActivity extends AppCompatActivity {
    ...
    @BindView(R.id.txtView)
    TextView txtView;
    ...
}
  • 創(chuàng)建一個類肌访,該類使用id tv_name將XML中定義的TextView對象賦值給變量tvName
public class ViewBinder {
    /*
     * annotations for activity class
     * @param target is the activity with annotations
     */
    public static void bind(final Activity target){
        bindViews(target, target.getClass().getDeclaredFields(),
    }

    /*
     * initiate the view for the annotated public fields
     * @param obj is any class instance with annotations
     * @param fields list of methods in the class with annotation
     * @param rootView is the inflated view from the XML
     */
    private static void bindViews(final Object obj, Field[] fields, View rootView){
        for(final Field field : fields) {
            Annotation annotation = field.getAnnotation(BindView.class);
            if (annotation != null) {
                BindView bindView = (BindView) annotation;
                int id = bindView.value();
                View view = rootView.findViewById(id);
                try {
                    field.setAccessible(true);
                    field.set(obj, view);
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}
  • 將Activity實例發(fā)送到ViewBinder。
public class MainActivity extends AppCompatActivity {

    @BindView(R.id.txtView)
    private TextView txtView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ViewBinder.bind(this);
        txtView.setText("Testing");
    }
    ...
}

這種方法能正常運行艇劫,但我們剛談到反射的性能限制吼驶,使它變得不可取。
那么店煞,我們怎樣才能改進呢蟹演?

  • 我們必須消除MainActivity類的運行時掃描并將其替換為方法調(diào)用。
  • 我們不希望為每個Activity編寫這些方法顷蟀,并希望它們自動生成酒请。
  • 我們希望消除任何運行時異常,并希望在編譯期間移動此類檢查衩椒。
    編譯時注解能滿足這些情況蚌父。

二、編譯時注解如何工作毛萌?

編譯時注解在編譯周期中進行苟弛。 在每個循環(huán)遍歷中,編譯器在讀取java源文件時找到注冊用于處理的注釋并調(diào)用相應的注釋處理器阁将。 如果在該循環(huán)中沒有生成文件膏秫,則該循環(huán)繼續(xù)生成任何文件或終止。

好吧做盅。我們將學習過程分為四個部分:

  1. 為注釋處理創(chuàng)建一個Android項目缤削。
  2. 理解用于處理的注釋的定義。
  3. 編寫一個編譯器模塊吹榴,通過注釋處理生成代碼亭敢。
  4. 使用通過注釋處理生成的代碼。

三图筹、項目結(jié)構(gòu)

該項目有四個模塊:

  1. app:這是Android應用程序項目帅刀。
  2. binder:此模塊提供一個類,該類將給Activity帶注釋的視圖對象和單擊回調(diào)方法映射到XML視圖远剩。
  3. binder-annotations:此模塊定義注釋以便于視圖和單擊回調(diào)方法的映射扣溺。
  4. binder-compiler:此模塊定義生成類以幫助上述映射的處理器。
  • 定義注釋

BindView:它將視圖引用映射到其XML定義瓜晤。 示例:帶有id tv_content的TextView將映射到變量tvContent锥余。

@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.FIELD)
public @interface BindView {
    @IdRes int value();
}

OnClick:它將映射一個方法,當單擊具有提供的id的視圖時將調(diào)用該方法痢掠。

@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.METHOD)
public @interface OnClick {
    @IdRes int value();
}

在這里你可以注意到@IdRes注釋驱犹。 此注釋由support-annotations庫提供嘲恍。

  • 創(chuàng)建注釋處理器

方法一:

注釋處理器循環(huán)運行并與應用程序編譯并行運行。 在每個周期中着绷,處理器都提供有關(guān)正在編譯的應用程序源代碼的信息蛔钙。

處理器必須注冊到編譯器,以便在編譯應用程序時可以運行它荠医。 我們將看到如何定義這樣的編譯器吁脱。

現(xiàn)在,我們將創(chuàng)建一個類似于binder-annotations的Java庫binder-compiler彬向。 在這個模塊中兼贡,我們將不得不創(chuàng)建目錄結(jié)構(gòu):

binder-compiler/src/main/resources/META-INF/services

在services目錄中,我們將創(chuàng)建一個名為javax.annotation.processing.Processor的文件娃胆。
此文件將列出編譯器在注釋處理時編譯應用程序源代碼時將調(diào)用的類遍希。

還有一種方法是(我采用了這種)

使用 Google 提供的庫 auto-service ,gradle 添加依賴

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

Processor 添加如下注解

@AutoService(javax.annotation.processing.Processor.class)
public class Processor extends AbstractProcessor {
...
}

所有注釋處理器都繼承AbstractProcessor里烦,它定義了處理的基本方法凿蒜。 我們將在此庫中創(chuàng)建一個繼承AbstractProcessor的類Processor。 我們必須覆蓋三種方法來提供處理的實現(xiàn)胁黑。

  • init:這里我們將獲得Filer废封,Messager和Elements。
  • process:調(diào)用此方法來處理應用程序的源代碼丧蘸。 在這里漂洋,我們將定義一個類并編寫Java源代碼。
  • getSupportedAnnotationTypes:它列出了我們在處理應用程序的Java文件時要查詢的注釋力喷。

另外還要了解如下類:

  • Filer:它提供API來編寫生成的源代碼文件刽漂。
  • Messager:用于在編譯時打印消息。 我們發(fā)送可能通過Messager處理的錯誤消息弟孟。 由于注釋處理器在其自己的獨立環(huán)境中運行贝咙,因此我們無法通過任何其他方式與應用程序通信。
  • Elements:它提供了utils方法拂募,用于過濾處理器中不同類型的元素颈畸。
public class Processor extends AbstractProcessor {

    private Filer filer;
    private Messager messager;
    private Elements elementUtils;

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

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        // all the magic happens in this block  
        return true;
    }

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        return new TreeSet<>(Arrays.asList(
                BindView.class.getCanonicalName(),
                OnClick.class.getCanonicalName()
                ));
    }
}

四、 生成 Java 源代碼

Note: 如果有些方法不明白可以參照 Android Studio中調(diào)試自定義AbstractProcessor方法 把程序跑起來没讲,打斷點看下就一目了然了。

現(xiàn)在我們提供Processor的流程方法的完整實現(xiàn)礁苗,并學習使用JavaPoet定義類及其成員爬凑。
注釋處理在處理Java注釋源代碼時提供的內(nèi)容:

  • Set<? extends TypeElement>:它提供注釋列表作為正在處理的Java文件中包含的元素试伙。
  • RoundEnvironment:它提供對處理環(huán)境的訪問嘁信,其中包含查詢元素的工具于样。 我們將在這個環(huán)境中使用的兩個主要功能是:processingOver(它的最后一輪處理)和getRootElements(它提供了一個將被處理的元素列表。這些元素中將包含一些我們感興趣的注釋潘靖。)
    因此穿剖,我們有一組注釋和一系列元素。 我們的庫將生成一個包裝類卦溢,它將幫助映射視圖并單擊活動的偵聽器糊余。

我們的注釋將映射視圖和按鈕以刪除樣板,就像ButterKnife一樣单寂。

public class MainActivity extends AppCompatActivity {

    @BindView(R.id.tv_content)
    TextView tvContent;

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

    @OnClick(R.id.bt_1)
    void bt1Click(View v) {
        tvContent.setText("Button 1 Clicked");
    }

    @OnClick(R.id.bt_2)
    void bt2Click(View v) {
        tvContent.setText("Button 2 Clicked");
    }
}

我們將使用MainActivity定義使用注釋處理自動生成名為MainActivity$Binding的包裝器類贬芥。處理后,將創(chuàng)建以下類宣决。編譯的時候生成在該目錄下:
[圖片上傳失敗...(image-9b18a6-1536888483860)]
打開后看到如下內(nèi)容:

public class MainActivity$Binding {
  public MainActivity$Binding(MainActivity activity) {
    bindViews(activity);
    bindOnClicks(activity);
  }

  private void bindViews(MainActivity activity) {
    activity.tvContent = (TextView)activity.findViewById(2131165322);
  }

  private void bindOnClicks(final MainActivity activity) {
    activity.findViewById(2131165218).setOnClickListener(new View.OnClickListener() {
      public void onClick(View view) {
        activity.bt1Click(view);
      }
    });
    activity.findViewById(2131165219).setOnClickListener(new View.OnClickListener() {
      public void onClick(View view) {
        activity.bt2Click(view);
      }
    });
  }
}

現(xiàn)在我們知道了我們必須生成什么蘸劈,讓我們分析如何使用我們在處理時可以使用的信息來創(chuàng)建它。

我們將首先過濾掉那些在getRootElements方法提供的元素列表中使用@BindView或@OnClick的類(Type)元素尊沸。
然后威沫,我們將迭代這些過濾的元素,然后掃描其成員和方法洼专,以使用JavaPoet開發(fā)包裝類的類模式棒掠。 最后,我們將該類寫入Java文件壶熏。希望使搜索更有效句柠, 因此,我們將創(chuàng)建一個具有過濾方法的ProcessingUtils類棒假。

public class ProcessingUtils {

    private ProcessingUtils() {
        // not to be instantiated in public
    }

    public static Set<TypeElement> getTypeElementsToProcess(Set<? extends Element> elements,
                                                            Set<? extends Element> supportedAnnotations) {
        Set<TypeElement> typeElements = new HashSet<>();
        for (Element element : elements) {
            if (element instanceof TypeElement) {
                boolean found = false;
                for (Element subElement : element.getEnclosedElements()) {
                    for (AnnotationMirror mirror : subElement.getAnnotationMirrors()) {
                        for (Element annotation : supportedAnnotations) {
                            if (mirror.getAnnotationType().asElement().equals(annotation)) {
                                typeElements.add((TypeElement) element);
                                found = true;
                                break;
                            }
                        }
                        if (found) break;
                    }
                    if (found) break;
                }
            }
        }
        return typeElements;
    }
}

這里有兩件事需要我們理解:

  • element.getEnclosedElements():封閉元素是給定元素中包含的元素溯职。 在我們的例子中,元素將是MainActivity(TypeElement)帽哑,Enclosed元素將是tvContent谜酒,onCreate,bt1Click妻枕,bt2Click和其他繼承的成員僻族。
  • subElement.getAnnotationMirrors():它將提供subElement上使用的所有注釋。
JavaPoet速成課程:

JavaPoet使得定義類結(jié)構(gòu)并在處理時編寫它非常簡單屡谐。 它創(chuàng)建了非常接近手寫代碼的類述么。 它提供了自動推斷導入以及美化代碼的工具。當然也可以使用 JavaFileObject 愕掏,但是這個遠古的笨重且不切實際的東西我們就不多說了度秘,有興趣的還是建議放棄這個興趣,就使用 JavaPoet 吧饵撑。

要使用JavaPoet剑梳,我們需要將以下依賴項添加到binder-compiler模塊中唆貌。

dependencies {
    implementation project(':binder-annotations')
    implementation 'com.squareup:javapoet:1.11.1'
}

本教程所需的JavaPoet的基本用法(可以從其 javapoet 獲得任何提前了解。)

  • TypeSpec.Builder:定義類模式垢乙。
  • addModifiers(Modifier):添加private锨咙,public或protected關(guān)鍵字。
  • addAnnotation:向元素添加注釋追逮。 示例:@Override on methods或@Keep on class in case酪刀。
  • TypeSpec.Builder - > addMethod:向類添加方法。 示例:構(gòu)造函數(shù)或其他方法羊壹。
  • MethodSpec - > addParameter:為方法添加參數(shù)類型及其名稱蓖宦。 示例:在我們的示例中,我們希望將具有變量名稱活動的MainActivity類型傳遞給方法油猫。
  • MethodSpec - > addStatement:它將在方法中添加代碼塊稠茂。 在這個方法中,我們首先定義語句的占位符情妖,然后傳遞參數(shù)來映射那些占位符睬关。 示例
addStatement("$N($N)", "bindViews", "activity") //這將生成代碼bindViews(activity)

占位符:N -> names,T -> type(ClassName), $L -> literals(long etc.)
參考JavaPoet的基本介紹,可以很容易地理解其他內(nèi)容毡证。剩下的就是你自己去完成了电爹。

「百聞不如一見」,百看不如一試料睛。最后再說下源碼地址吧:
android-annotation-tutorial

參考鏈接:

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末丐箩,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子恤煞,更是在濱河造成了極大的恐慌屎勘,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,755評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件居扒,死亡現(xiàn)場離奇詭異概漱,居然都是意外死亡,警方通過查閱死者的電腦和手機喜喂,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評論 3 395
  • 文/潘曉璐 我一進店門瓤摧,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人玉吁,你說我怎么就攤上這事照弥。” “怎么了进副?”我有些...
    開封第一講書人閱讀 165,138評論 0 355
  • 文/不壞的土叔 我叫張陵产喉,是天一觀的道長。 經(jīng)常有香客問我,道長曾沈,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,791評論 1 295
  • 正文 為了忘掉前任鸥昏,我火速辦了婚禮塞俱,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘吏垮。我一直安慰自己障涯,他們只是感情好,可當我...
    茶點故事閱讀 67,794評論 6 392
  • 文/花漫 我一把揭開白布膳汪。 她就那樣靜靜地躺著唯蝶,像睡著了一般。 火紅的嫁衣襯著肌膚如雪遗嗽。 梳的紋絲不亂的頭發(fā)上粘我,一...
    開封第一講書人閱讀 51,631評論 1 305
  • 那天,我揣著相機與錄音痹换,去河邊找鬼征字。 笑死,一個胖子當著我的面吹牛娇豫,可吹牛的內(nèi)容都是我干的匙姜。 我是一名探鬼主播,決...
    沈念sama閱讀 40,362評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼冯痢,長吁一口氣:“原來是場噩夢啊……” “哼氮昧!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起浦楣,我...
    開封第一講書人閱讀 39,264評論 0 276
  • 序言:老撾萬榮一對情侶失蹤袖肥,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后椒振,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體昭伸,經(jīng)...
    沈念sama閱讀 45,724評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年澎迎,在試婚紗的時候發(fā)現(xiàn)自己被綠了庐杨。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,040評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡夹供,死狀恐怖灵份,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情哮洽,我是刑警寧澤填渠,帶...
    沈念sama閱讀 35,742評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響氛什,放射性物質(zhì)發(fā)生泄漏莺葫。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,364評論 3 330
  • 文/蒙蒙 一枪眉、第九天 我趴在偏房一處隱蔽的房頂上張望捺檬。 院中可真熱鬧,春花似錦贸铜、人聲如沸堡纬。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽烤镐。三九已至,卻和暖如春棍鳖,著一層夾襖步出監(jiān)牢的瞬間炮叶,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評論 1 270
  • 我被黑心中介騙來泰國打工鹊杖, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留悴灵,地道東北人。 一個月前我還...
    沈念sama閱讀 48,247評論 3 371
  • 正文 我出身青樓骂蓖,卻偏偏與公主長得像积瞒,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子登下,可洞房花燭夜當晚...
    茶點故事閱讀 44,979評論 2 355

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

  • 用兩張圖告訴你茫孔,為什么你的 App 會卡頓? - Android - 掘金 Cover 有什么料? 從這篇文章中你...
    hw1212閱讀 12,730評論 2 59
  • 現(xiàn)在市面上很多框架都有使用到注解被芳,比如butterknife庫缰贝、EventBus庫、Retrofit庫等等畔濒。...
    tuacy閱讀 5,584評論 1 15
  • 主持狀態(tài)很給力剩晴,控制臺值守不專業(yè),主持訓練不足侵状,主持人數(shù)可以赞弥,致辭時大伙明顯興趣不足,致辭本身非常精彩趣兄,致辭提升了...
    李杰_1d6d閱讀 73評論 0 0
  • 2018.7.27 星期五 天氣:陰 讀經(jīng)進度:第8周第5天 讀經(jīng)人員:仁杰绽左、媽媽 讀經(jīng)...
    翟露露閱讀 150評論 0 0
  • 樓下有兩家快餐店,一家賣米飯快餐鲁纠,一家賣面总棵。賣米飯快餐的那家是一年前開張的,當時因為離得近房交,又是老鄉(xiāng)彻舰,所以常去他們...
    Zerof閱讀 252評論 4 3