注解和注解處理器

0.導(dǎo)語

Java 作為一門低語法糖的語言嚎杨,核心在其虛擬機(jī)的實(shí)現(xiàn),語言層面提供的“黑科技”并不多氧腰,而注解就是其中比較重要的一點(diǎn)枫浙。注解在 Java5 中開始加入,在 Java6 中對(duì)外暴露出注解處理器的接口供程序員來按需處理注解」潘現(xiàn)如今箩帚,不論是安卓客戶端還是Java后端技術(shù)棧都使用到了大量注解相關(guān)的庫和框架,甚至可以說是到了“泛濫成災(zāi)”的地步。在其中相對(duì)比較重要的便是自定義注解處理器了,本文將從兩個(gè)方面來進(jìn)行介紹:

  • 注解的通俗解釋及使用
  • 注解的處理方式惜姐,以安卓中的 ButterKnife 庫為例厢洞,通過運(yùn)行時(shí)期處理注解和編譯時(shí)期通過注解處理器來處理注解這兩種方式,實(shí)現(xiàn)和 ButterKnife 庫相似的功能

1.什么是注解

注解,通俗的來說,就是像注釋一樣,是由程序員在代碼中加入的一種“標(biāo)注”叠纷,不影響所編寫的原有代碼的執(zhí)行。而這種標(biāo)注(注解)可以被編碼用的IDE潦嘶、編譯器涩嚣、類加載器的代理程序、其他第三方工具以及原有代碼運(yùn)行期間讀取和處理掂僵,生成一些新的輔助代碼或是提示航厚,從而節(jié)省時(shí)間,提升效率锰蓬。這些工具讀取注解的時(shí)機(jī)是根據(jù)注解的生命周期來定的,注解的生命周期就是其“存在壽命”幔睬,分為三種:

  • 源代碼時(shí)期注解,即注解只出現(xiàn)在 .java 文件中芹扭,編譯后便不再出現(xiàn)在生成的.class 文件中麻顶,這一階段赦抖,對(duì)注解的處理有兩種方式:

    • 被 IDE 中代碼檢查工具讀取,如圖 1-1中的 1 處辅肾,實(shí)時(shí)地提示程序員縮寫代碼中的錯(cuò)誤队萤,例如 @Override 注解就是用來檢查程序員所復(fù)寫的父類方法的簽名一致性的;
    • 在原代碼編譯期間矫钓,被程序員在編譯器中所注冊(cè)的注解處理器所讀取要尔,如圖 1-1中的 2 處,用于生成新的源代碼文件參與編譯新娜,省去重復(fù)地書寫樣板代碼的成本赵辕,提升效率,編譯之后便被去除杯活,不再出現(xiàn)在 .class 文件中匆帚;
  • 字節(jié)碼時(shí)期注解,即出現(xiàn)在 .class 文件中的注解旁钧,對(duì)這類注解的處理主要涉及字節(jié)碼的修改,需要使用ASM等類庫互拾,根據(jù)處理時(shí)機(jī)的不同歪今,也可分為兩種方式:

    • 在源代碼編譯后處理:如果當(dāng)前工程采用了Gradle、Maven等構(gòu)建工具來構(gòu)建颜矿,則在源代碼被編譯為 .class 文件后寄猩,可以通過相應(yīng)地腳本調(diào)用使用了ASM庫編寫的第三方工具來讀取文件中的注解并修改 .class 文件中的虛擬機(jī)指令,沒有采用構(gòu)建工具骑疆,也可以通過命令行來手動(dòng)更新 .class 文件田篇,虛擬機(jī)加載.class 文件時(shí)不會(huì)加載字節(jié)碼級(jí)注解,如圖 1-1中的 3 處所示箍铭;
    • 在類加載時(shí)處理:通過代理程序泊柬,在類加載器加載.class 文件前,讀取文件中的注解修改字節(jié)碼诈火,但并不保存兽赁,即原有的.class文件內(nèi)容不變,內(nèi)存中處理過的字節(jié)碼的類被類加載器直接加載到虛擬機(jī)中冷守,同樣的并不加載.class 文件中的字節(jié)碼級(jí)注解刀崖,如圖 1-1中的 4 處所示;
  • 運(yùn)行時(shí)期注解拍摇,即注解被類加載器加載到內(nèi)存當(dāng)中亮钦,和類的其他構(gòu)成元素一樣被放置于元數(shù)據(jù)區(qū),供堆區(qū)中相應(yīng)的 class 對(duì)象訪問充活,換句話說蜂莉,此時(shí)的注解可以通過反射的方式來讀取和處理蜡娶,這時(shí)的處理過程往往是由程序員在編碼期間就確定的,是運(yùn)行效率最低但卻是最容易實(shí)現(xiàn)的一種注解處理方式巡语,如圖 1-1中的 5 處所示翎蹈;

圖1-1.java注解處理流程

2.處理注解

由前所述,對(duì)注解的處理由處理時(shí)機(jī)的不同可以有不同的實(shí)現(xiàn)方式男公。這里介紹開發(fā)中最常用的兩種方式荤堪,運(yùn)行時(shí)期處理注解和編譯時(shí)期通過自定義注解處理器來處理,口說無憑枢赔,先設(shè)立一個(gè)目標(biāo)澄阳,ButterKnife 是安卓中很有名的利用注解來進(jìn)行控件綁定的庫,我將通過上述兩種注解處理方式來實(shí)現(xiàn)類似 ButterKnife 庫的功能踏拜。

2.1 ButterKnife 的簡單介紹

ButterKnife 這個(gè)庫主要用于安卓端控件綁定的問題碎赢。在很多流程類似的GUI程序中,都是先將 xml 等標(biāo)簽語言書寫的界面文件讀入并解析速梗,在內(nèi)存中生成視圖界面中所有控件所對(duì)應(yīng)的控件樹對(duì)象肮塞,之后將控件樹中的控件實(shí)體對(duì)象和邏輯控制類(安卓中的Activity)中的對(duì)象引用進(jìn)行綁定,需要由程序員手工進(jìn)行姻锁,如下所示:

public class SourceTestActivity extends AppCompatActivity {

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

    // 控件引用
    private TextView mTextView;
    private Button mButton;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // 讀入文件枕赵,解析生成對(duì)象樹
        setContentView(R.layout.activity_source_test);
        
        // 根據(jù)控件id在控件樹中找到對(duì)應(yīng)的實(shí)體控件對(duì)象并綁定
        mTextView = findViewById(R.id.tv);
        mButton = findViewById(R.id.btn);
        
        // 綁定按鍵控件的點(diǎn)擊事件處理回調(diào)
        mButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Toast.makeText(v.getContext(), ((TextView) v).getText(), Toast.LENGTH_SHORT).show();
            }
        });
    }
}

界面中有一個(gè)文本控件和一個(gè)按鈕控件,都需要程序員通過 findViewById 和 setOnClickListner 方法來綁定實(shí)體 View 和其對(duì)應(yīng)的回調(diào)方法位隶。通過 ButterKnife 庫可以將代碼簡化成如下所示:

public class SourceTestActivity extends AppCompatActivity {

    private static final String TAG = SourceTestActivity.class.getSimpleName();
    
    // 用注解標(biāo)記引用需要綁定的控件的id
    @BindView(R.id.tv)
    TextView mTextView;
    @BindView(R.id.btn)
    Button mButton;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_source_test);
        // 執(zhí)行綁定過程
        ButterKnife.bind(this);
    }

    // 用注解標(biāo)記事件回調(diào)需要綁定的控件id
    @OnClick(R.id.btn)
    public void test(View v) {
        Toast.makeText(v.getContext(), ((TextView) v).getText(), Toast.LENGTH_SHORT).show();
    }
}

在需要綁定實(shí)體 View 的引用上加上 @BindView 注解拷窜,在需要綁定的回調(diào)方法上加上 @OnClick 注解,最后在代碼中通過 ButterKnife.bind() 方法來實(shí)現(xiàn)綁定涧黄。上述例子由于需要綁定的控件和方法較少篮昧,看不出優(yōu)勢,但當(dāng)需要綁定的對(duì)象很多是笋妥,ButterKnife 庫可以幫我們節(jié)省很多重復(fù)的 findViewById 和 setOnClickListner 方法的書寫懊昨。

2.2 反射處理運(yùn)行時(shí)注解實(shí)現(xiàn) “ButterKnife”

2.2.1 自定義注解

首先我們需要自定義注解 @BindView 和 @OnClick
BindView

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

@Target 元注解中的ElementType.FIELD參數(shù)設(shè)定了@BindView 注解所適用的場景為 Field ,即屬性字段挽鞠;@Retention 元注解中的RetentionPolicy.RUNTIME 參數(shù)設(shè)定了@BIndView 注解的生命周期為運(yùn)行時(shí)疚颊,即 .java .class 加載到內(nèi)存中三個(gè)時(shí)期,@BindView 注解一直都存在信认。

OnClick

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface OnClick {
    @IdRes int[] value() default {View.NO_ID};
}

OnClick 注解生命周期和 BindView 一致材义,不同在于適用的目標(biāo)為方法(ElementType.METHOD)

2.2.2 運(yùn)行時(shí)反射綁定

public class ButterKnife {

    /**
     * 負(fù)責(zé) 將容器類中的監(jiān)聽方法綁定到容器類中聲明的指定 view 上
     *
     * @param container 容器類,待綁定方法和被綁定的 view 都位于其中, 一般是 Activity or Fragment
     */
    public static void bind(final Activity container) {

        // key: view's resId; value: view
        SparseArray<View> viewsMap = new SparseArray<>(8);

        Class<?> cls = container.getClass();

        // 獲取已注解的 view 域, 并且?guī)推?findViewById
        for (Field viewField: cls.getDeclaredFields()){
            BindView fieldAnnotation = viewField.getAnnotation(BindView.class);
            if (fieldAnnotation != null) {
                viewField.setAccessible(true);
                try {
                    // 進(jìn)行 View 綁定
                    View viewRef = container.findViewById(fieldAnnotation.value());
                    viewField.set(container, viewRef);
                    viewsMap.put(fieldAnnotation.value(), viewRef);
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                }
            }
        }

        // 獲取已注解的方法域, 并且?guī)推渫ㄟ^動(dòng)態(tài)代理 setOnClickListener
        for(final Method onClickMethod: cls.getDeclaredMethods()){
            OnClick methodAnnotation = onClickMethod.getAnnotation(OnClick.class);
            if (methodAnnotation != null) {
                int[] viewResIds = methodAnnotation.value();
                for (int resId : viewResIds) {
                    View viewToAttach;
                    if ((viewToAttach = viewsMap.get(resId)) != null) {
                        onClickMethod.setAccessible(true);
                        viewToAttach.setOnClickListener(
                                (View.OnClickListener) Proxy.newProxyInstance(null, new Class[]{View.OnClickListener.class},
                                        new InvocationHandler() {
                                            @Override
                                            public Object invoke(Object proxy, Method method, Object[] args) throws
                                                    InvocationTargetException, IllegalAccessException {
                                                return onClickMethod.invoke(container, args);
                                            }
                                        })
                        );
                    }
                }
            }
        }
    }
}

綁定方法主要是假的ButterKnife中的靜態(tài)bind方法嫁赏,其中做了兩件事:

  • 首先通過反射遍歷了所有的屬性字段的其掂,找出其中附加了@BindView注解的域,即View的引用潦蝇,通過@BindView注解中設(shè)置的View的resId找到View實(shí)體款熬,并綁定到View引用上深寥,最后將其緩存到SparseArray中。
  • 之后贤牛,通過反射遍歷所有方法域惋鹅,找出其中附加了@OnClick注解的方法,由于Java中方法不是對(duì)象殉簸,不能獨(dú)立存在闰集,必須存在于類中,所以需要通過動(dòng)態(tài)代理機(jī)制來代理View.OnClickListener接口般卑,“包裹”所注解的方法武鲁,最后通過SparseArray的鍵值resId找到View實(shí)體進(jìn)行回調(diào)方法的綁定。
    最后是Activity中的使用,界面中一共是兩個(gè)按鈕控件蝠检,綁定同一個(gè)回調(diào)方法沐鼠,使用的形式上和ButterKnife庫是類似的:
    RuntimeTestActivity.java
public class RuntimeTestActivity extends AppCompatActivity {

    @BindView(R.id.btn_one)
    private Button btnTest1;

    @BindView(R.id.btn_two)
    private Button btnTest2;

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

    @OnClick({R.id.btn_one, R.id.btn_two})
    public void onClick(View v) {
        Toast.makeText(v.getContext(), ((TextView)v).getText(), Toast.LENGTH_SHORT).show();
    }

}

綜上,雖然使用的形式上和 ButterKnife 相類似叹谁,但原理上卻完全不同饲梭,我們所寫的偽ButterKnife庫中的注解都是運(yùn)行時(shí)注解,即源文件中的注解也會(huì)出現(xiàn)在最后的運(yùn)行時(shí)內(nèi)存中焰檩,可以通過反射的方式拿到注解排拷,從而獲得被注解的域或方法,最后在運(yùn)行時(shí)再進(jìn)行綁定锅尘,由于待綁定的元素都需要通過反射來拿到,故效率較低布蔗。

2.3 編譯時(shí)注解處理器處理注解實(shí)現(xiàn) “ButterKnife”

反射處理運(yùn)行時(shí)注解的效率較低藤违,那我們有沒有辦法減少反射的調(diào)用呢?回顧真ButterKnife庫的作用纵揍,主要是減少書寫findViewById之類的樣板代碼的書寫顿乒,如果我們可以提前生成這些綁定方法的模板代碼,只是通過反射調(diào)用唯一的bind()方法泽谨,那就可以適當(dāng)提升效率璧榄,實(shí)際上,真ButterKnife庫中也是這么做的吧雹。首先看偽ButterKnife庫工程的結(jié)構(gòu):


圖2-1.項(xiàng)目結(jié)構(gòu)
  • app為安卓的測試module,用于測試自定義的偽ButterKnife庫骨杂;
  • butterknife-annotation 為包含自定義注解的module;
  • butterknife-compiler 為自定義注解處理器的module,這里將注解處理器和所能處理的注解分在兩個(gè)不同的module,主要是因?yàn)樽⒔馓幚砥鲗?shí)際上只是被編譯器在編譯時(shí)調(diào)用,并不屬于工程雄卷,所以在打包時(shí)可以將其不打入apk中以節(jié)省空間搓蚪,如果自定義的注解和注解處理器處在同一module,那么使用時(shí),app module中會(huì)找不到自定義注解丁鹉,所以需要將注解獨(dú)立成butterknife-annotation module 妒潭,在 app 和注解處理器 butterknife-compiler module 中分別引用悴能,自定義注解處理器在編譯時(shí)讀取源文件中的注解,生成新的源代碼文件參與編譯雳灾;
  • butterknife-library module 負(fù)責(zé)通過反射調(diào)用 butterknife-compiler 中自動(dòng)生成的代碼

2.3.1 自定義注解

BindView

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

OnClick

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

可以看到和反射式偽ButterKnife庫中的自定義注解所不同的是注解生命周期的定義漠酿,@Retention(RetentionPolicy.SOURCE)聲明了這些注解都是源碼級(jí)注解,即編譯后在 .class 文件中便不存在了谎亩,注意建立butterknife-annotation 庫時(shí)炒嘲,選擇該庫為 java library,如下圖所示:


圖2-2.java library

最后的module工程結(jié)構(gòu)如下:


圖2-3.annotation目錄結(jié)構(gòu)

2.3.2 自定義注解處理器

自定義注解處理器的基本原理是將程序員自定義的注解處理器注冊(cè)到編譯器中,編譯器在編譯源文件時(shí)調(diào)用該注解處理器處理源文件中的注解团驱,處理這些注解時(shí)是無法修改源文件的摸吠,只能生成新的源文件,生成的新文件會(huì)繼續(xù)調(diào)用注解處理器處理嚎花,這是一個(gè)遞歸的過程寸痢,一輪一輪地處理,直到?jīng)]有新的源文件生成紊选,我們正是利用這點(diǎn)來自動(dòng)生成綁定控件的模板代碼啼止。大致原理如下圖所示:


圖2-4.編譯器原理

編譯器編譯源文件后生成語法樹(AST),即結(jié)構(gòu)化后的源文件元素兵罢,并將其傳入自定義的注解處理器献烦,自定義的注解處理器可以通過RoundEnvironment獲得這些語法元素來處理,最后生成字節(jié)碼文件卖词。想要自己實(shí)現(xiàn)注解處理器并注冊(cè)到編譯器中的具體步驟如下:

  1. 創(chuàng)建java lib butterknife-cmplier和創(chuàng)建 butterknife-annotation的方式一致巩那;
  2. 創(chuàng)建Processor,代碼如下:
    BindViewProcessor.java
@SupportedSourceVersion(SourceVersion.RELEASE_7)
@SupportedAnnotationTypes({Constants.FULL_NAME_ANNO_BINDVIEW, Constants.FULL_NAME_ANNO_ONCLICK})
@AutoService(Processor.class)
public class BindViewProcessor extends AbstractProcessor {

    private Messager mMessager;
    private Elements mElementUtils;
    private Map<String, Creator> mProxyMap = new HashMap<>();

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        mMessager = processingEnv.getMessager();
        mElementUtils = processingEnv.getElementUtils();
    }

    // @Override
    // public Set<String> getSupportedAnnotationTypes() {
    //     HashSet<String> supportTypes = new LinkedHashSet<>();
    //     supportTypes.add(BindView.class.getCanonicalName());
    //     return supportTypes;
    // }
    //
    // @Override
    // public SourceVersion getSupportedSourceVersion() {
    //     return SourceVersion.latestSupported();
    // }

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        mMessager.printMessage(Diagnostic.Kind.NOTE, "processing...");
        mProxyMap.clear();
        
        // 獲取源文件中帶有 BindView 注解的域
        Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(BindView.class);
        for (Element element : elements) {
            VariableElement variableElement = (VariableElement) element;
            TypeElement classElement = (TypeElement) variableElement.getEnclosingElement();
            String fullClassName = classElement.getQualifiedName().toString();
            Creator proxy = mProxyMap.get(fullClassName);
            if (proxy == null) {
                proxy = new Creator(mElementUtils, classElement);
                mProxyMap.put(fullClassName, proxy);
            }
            BindView bindAnnotation = variableElement.getAnnotation(BindView.class);
            int id = bindAnnotation.value();
            proxy.putFieldElement(id, variableElement);
        }
        
        // 獲取源文件中帶有 OnClick 注解的方法
        elements = roundEnvironment.getElementsAnnotatedWith(OnClick.class);
        for(Element element: elements){
            ExecutableElement methodElement = (ExecutableElement) element;
            TypeElement classElement = (TypeElement) methodElement.getEnclosingElement();
            String fullClassName = classElement.getQualifiedName().toString();
            Creator proxy = mProxyMap.get(fullClassName);
            if (proxy == null) {
                proxy = new Creator(mElementUtils, classElement);
                mProxyMap.put(fullClassName, proxy);
            }
            OnClick bindAnnotation = methodElement.getAnnotation(OnClick.class);
            int id = bindAnnotation.value();
            proxy.putMethodElement(id, methodElement);
        }
      
        // 生成java代碼
        for (String key : mProxyMap.keySet()) {
            Creator proxyInfo = mProxyMap.get(key);
            JavaFile javaFile = JavaFile.builder(proxyInfo.getPackageName(), proxyInfo.generateJavaCode())
                    .indent("    ")
                    .addFileComment("generate file, do not modify!")
                    .build();
            try {
                javaFile.writeTo(processingEnv.getFiler());
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        mMessager.printMessage(Diagnostic.Kind.NOTE, "process finish ...");
        return true;
    }
}

Constants.java

public class Constants {

    public static final String FULL_NAME_ANNO_BINDVIEW = "com.danielchen.demo.butterknife_annotation.BindView";
    public static final String FULL_NAME_ANNO_ONCLICK = "com.danielchen.demo.butterknife_annotation.OnClick";
}

從代碼中可以看出,注解處理器主要是繼承AbstractProcessor并實(shí)現(xiàn) process() 方法此蜈, 可選擇實(shí)現(xiàn) init()即横、getSupportedAnnotationTypes() 、
getSupportedSourceVersion()三個(gè)方法裆赵,其中东囚,getSupportedAnnotationTypes() 、
getSupportedSourceVersion()是返回本注解處理器所支持的注解和源代碼版本战授,可以在注解處理器上加上@SupportedSourceVersion()
@SupportedAnnotationTypes()注解實(shí)現(xiàn)同樣的效果,init()方法主要是為了初始化一些全局的工具页藻。

  1. 向編譯器注冊(cè)本注解處理器,可以手動(dòng)注冊(cè):在main文件夾下植兰,創(chuàng)建路徑 META-INF/services/,在其中創(chuàng)建文件javax.annotation.processing.Processor,在文件中加入注解處理器的全限定名份帐;這樣做很麻煩,更簡便的方法是引入google的auto-service庫钉跷,gradle 文件中遠(yuǎn)程依賴 'com.google.auto.service:auto-service:1.0-rc4'弥鹦,在注解處理器類上加上@AutoService(Processor.class)即可,這樣會(huì)自動(dòng)創(chuàng)建上述路徑和文件,如下圖所示:
圖2-5.auto-services

4.實(shí)現(xiàn)注解處理器的process()方法彬坏,形參Set是本處理器可處理的注解類型朦促,就是@SupportedAnnotationTypes()中所設(shè)置的類型,RoundEnvironment 則提供了本輪次的語法樹元素栓始,如果方法返回true,則后續(xù)其他處理器不能處理這些注解务冕,否則可以處理,類似點(diǎn)擊事件的攔截幻赚。上述代碼中將讀取到的屬性字段和方法對(duì)應(yīng)的語法元素存入 Creator,Creator 代碼如下:
Creator.java

public class Creator {

    private String mBindingClassName;
    private String mFullPackageName;
    private ClassName mClassName;

    private Map<Integer, VariableElement> mVariableElementMap;
    private Map<Integer, ExecutableElement> mExecutableElementMap;

    public Creator(PackageElement pkgElement, TypeElement classElement) {
        mVariableElementMap = new HashMap<>(8);
        mExecutableElementMap = new HashMap<>(8);

        this.mClassName = ClassName.get(classElement);
        this.mFullPackageName = pkgElement.getQualifiedName().toString();
        this.mBindingClassName = classElement.getSimpleName().toString() + "_ViewBinding";
    }

    public String getFullPackageName() {
        return mFullPackageName;
    }

    public void putFieldElement(int id, VariableElement element) {
        mVariableElementMap.put(id, element);
    }

    public void putMethodElement(int id, ExecutableElement element) {
        mExecutableElementMap.put(id, element);
    }

    public TypeSpec generateJavaCode() {
        return TypeSpec.classBuilder(mBindingClassName)
                .addModifiers(Modifier.PUBLIC)
                .addMethod(generateBindMethod())
                .build();
    }

    private MethodSpec generateBindMethod() {

        String paramName = "activity";
        MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder("bind")
                .addModifiers(Modifier.PUBLIC)
                .returns(void.class)
                .addParameter(mClassName, paramName, Modifier.FINAL);

        for (int resId : mVariableElementMap.keySet()) {
            VariableElement element = mVariableElementMap.get(resId);
            String viewName = element.getSimpleName().toString();
            String viewType = element.asType().toString();
            methodBuilder.addStatement("$L.$L = ($L)$L.findViewById($L)",
                    paramName, viewName, viewType, paramName, resId);

            if (mExecutableElementMap.containsKey(resId)) {
                ExecutableElement methodElement = mExecutableElementMap.get(resId);
                String methodName = methodElement.getSimpleName().toString();

                ClassName viewClass = ClassName.get("android.view", "View");
                ClassName clickClass = ClassName.get("android.view.View", "OnClickListener");

                TypeSpec onClickListener = TypeSpec.anonymousClassBuilder("")
                        .addSuperinterface(clickClass)
                        .addMethod(MethodSpec.methodBuilder("onClick")
                                .addAnnotation(Override.class)
                                .addModifiers(Modifier.PUBLIC)
                                .addParameter(viewClass, "v")
                                .returns(TypeName.VOID)
                                .addStatement("$L.$L(v)", paramName, methodName)
                                .build())
                        .build();
                methodBuilder.addStatement("$L.$L.setOnClickListener($L)", paramName, viewName, onClickListener);
            }
        }
        return methodBuilder.build();
    }
}

Creator 中提供了根據(jù)緩存的語法元素生成源代碼的方法禀忆,這里可以采用用字符串自己拼裝的形式,但這樣太過復(fù)雜且不易維護(hù)落恼,所以推薦使用JavaPoet庫來生成源代碼箩退,可讀性更強(qiáng)。
最終佳谦, butterknife-compiler 的工程結(jié)構(gòu)如下:

圖2-6.butterknife-compiler目錄結(jié)構(gòu)

依賴關(guān)系如下:

apply plugin: 'java-library'

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

    // 遠(yuǎn)程依賴 auto-service 庫, 用于注冊(cè)自定義注解處理器到編譯器中
    implementation 'com.google.auto.service:auto-service:1.0-rc4'

    // 遠(yuǎn)程以來 javapoet 庫戴涝,用于動(dòng)態(tài)生成代碼
    implementation 'com.squareup:javapoet:1.11.1'

    // 本地依賴 聲明了自定義注解的 module
    implementation project(':butterknife-annotation')
}

sourceCompatibility = "1.7"
targetCompatibility = "1.7"

最終自動(dòng)生成的源代碼如下:

// generate file, do not modify!
package com.danielchen.demo.demo_reflect;

import android.view.View;
import android.view.View.OnClickListener;
import java.lang.Override;

public class SourceTestActivity_ViewBinding {
    public void bind(final SourceTestActivity activity) {
        activity.mTextView = (android.widget.TextView)activity.findViewById(2131230914);
        activity.mButton = (android.widget.Button)activity.findViewById(2131230755);
        activity.mButton.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                activity.test(v);
            }
        });
    }
}

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

最終通過butterknife-libraray中的 ButterKnife 類來調(diào)用自動(dòng)生成的 SourceTestActivity_ViewBinding 的 bind() 方法,ButterKnfie 代碼如下:

public class ButterKnife {

    public static void bind(Activity activity) {
        try {
            Class<?> bindViewClass = Class.forName(activity.getClass().getName() + "_ViewBinding");
            Method method = bindViewClass.getMethod("bind", activity.getClass());
            method.invoke(bindViewClass.newInstance(), activity);
        } catch (ClassNotFoundException | IllegalAccessException | InstantiationException | NoSuchMethodException | InvocationTargetException e) {
            e.printStackTrace();
        }
    }
}

由于自動(dòng)生成的類名和方法名bind都是約定好的钻蔑,所以可以這么用啥刻,最后在測試代碼中調(diào)用ButterKnife.bind()即可完成綁定,如下:

public class SourceTestActivity extends AppCompatActivity {

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

    @BindView(R.id.tv)
    TextView mTextView;
    @BindView(R.id.btn)
    Button mButton;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_source_test);
        // butterknife-library bind方法中通過反射調(diào)用 SourceTestActivity_ViewBinding 中的bind方法完成綁定
        ButterKnife.bind(this);
    }

    @OnClick(R.id.btn)
    public void test(View v) {
        Toast.makeText(v.getContext(), ((TextView) v).getText(), Toast.LENGTH_SHORT).show();
    }
}

綜上咪笑,實(shí)際上可帽,仿照真ButterKnife庫的原理,我們是將控件綁定的模板代碼先自動(dòng)生成窗怒,運(yùn)行時(shí)在用過一次反射調(diào)用即完成綁定映跟,比運(yùn)行時(shí)注解處理的方式的多次反射綁定要好那么一點(diǎn),當(dāng)然真ButterKnife庫中的實(shí)際代碼要更復(fù)雜些扬虚,不過大致原理是這樣申窘。

3.總結(jié)

本文順著注解的生命周期大致描述了處理注解的不同時(shí)機(jī)和處理方法。參照ButterKnife庫的原理孔轴,對(duì)運(yùn)行時(shí)動(dòng)態(tài)處理注解和編譯時(shí)根據(jù)注解生成代碼這兩種處理注解的方式進(jìn)行了代碼演示,限于本人的水平碎捺,寫的比較冗長路鹰,很多細(xì)節(jié)也沒能描述清楚,之后會(huì)將代碼傳至github,感覺還是看代碼理解更快收厨。晋柱。。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末诵叁,一起剝皮案震驚了整個(gè)濱河市雁竞,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖碑诉,帶你破解...
    沈念sama閱讀 222,464評(píng)論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件彪腔,死亡現(xiàn)場離奇詭異,居然都是意外死亡进栽,警方通過查閱死者的電腦和手機(jī)德挣,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,033評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來快毛,“玉大人格嗅,你說我怎么就攤上這事∵氲郏” “怎么了屯掖?”我有些...
    開封第一講書人閱讀 169,078評(píng)論 0 362
  • 文/不壞的土叔 我叫張陵,是天一觀的道長襟衰。 經(jīng)常有香客問我贴铜,道長,這世上最難降的妖魔是什么右蒲? 我笑而不...
    開封第一講書人閱讀 59,979評(píng)論 1 299
  • 正文 為了忘掉前任阀湿,我火速辦了婚禮,結(jié)果婚禮上瑰妄,老公的妹妹穿的比我還像新娘陷嘴。我一直安慰自己,他們只是感情好间坐,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,001評(píng)論 6 398
  • 文/花漫 我一把揭開白布灾挨。 她就那樣靜靜地躺著,像睡著了一般竹宋。 火紅的嫁衣襯著肌膚如雪劳澄。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,584評(píng)論 1 312
  • 那天蜈七,我揣著相機(jī)與錄音秒拔,去河邊找鬼。 笑死飒硅,一個(gè)胖子當(dāng)著我的面吹牛砂缩,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播三娩,決...
    沈念sama閱讀 41,085評(píng)論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼庵芭,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了雀监?” 一聲冷哼從身側(cè)響起双吆,我...
    開封第一講書人閱讀 40,023評(píng)論 0 277
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后好乐,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體匾竿,經(jīng)...
    沈念sama閱讀 46,555評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,626評(píng)論 3 342
  • 正文 我和宋清朗相戀三年曹宴,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了搂橙。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,769評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡笛坦,死狀恐怖区转,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情版扩,我是刑警寧澤废离,帶...
    沈念sama閱讀 36,439評(píng)論 5 351
  • 正文 年R本政府宣布,位于F島的核電站礁芦,受9級(jí)特大地震影響蜻韭,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜柿扣,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,115評(píng)論 3 335
  • 文/蒙蒙 一肖方、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧未状,春花似錦俯画、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,601評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至埋虹,卻和暖如春猜憎,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背搔课。 一陣腳步聲響...
    開封第一講書人閱讀 33,702評(píng)論 1 274
  • 我被黑心中介騙來泰國打工胰柑, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人爬泥。 一個(gè)月前我還...
    沈念sama閱讀 49,191評(píng)論 3 378
  • 正文 我出身青樓旦事,卻偏偏與公主長得像,于是被迫代替她去往敵國和親急灭。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,781評(píng)論 2 361

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

  • 用兩張圖告訴你谷遂,為什么你的 App 會(huì)卡頓? - Android - 掘金 Cover 有什么料葬馋? 從這篇文章中你...
    hw1212閱讀 12,752評(píng)論 2 59
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,318評(píng)論 25 707
  • 什么是注解注解分類注解作用分類 元注解 Java內(nèi)置注解 自定義注解自定義注解實(shí)現(xiàn)及使用編譯時(shí)注解注解處理器注解處...
    Mr槑閱讀 1,085評(píng)論 0 3
  • Java中的注解是個(gè)很神奇的東西,還不了解的可以看下一小時(shí)搞明白自定義注解(Annotation)。現(xiàn)在很多And...
    顧明偉閱讀 3,148評(píng)論 1 3
  • 下午工作有點(diǎn)倦怠畴嘶, 問同事鵬哥蛋逾,你閨女也是一口東北腔嗎? 鵬哥:不是的窗悯,來区匣,我讓你聽聽她唱的歌 鵬哥滿臉幸福的從錄...
    璞玉丁閱讀 197評(píng)論 0 0