注解和注解處理器

0.導(dǎo)語

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

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

1.什么是注解

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

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

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

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

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

2.處理注解

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

2.1 ButterKnife 的簡單介紹

ButterKnife 這個庫主要用于安卓端控件綁定的問題。在很多流程類似的GUI程序中,都是先將 xml 等標(biāo)簽語言書寫的界面文件讀入并解析,在內(nèi)存中生成視圖界面中所有控件所對應(yīng)的控件樹對象,之后將控件樹中的控件實體對象和邏輯控制類(安卓中的Activity)中的對象引用進(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);
        // 讀入文件,解析生成對象樹
        setContentView(R.layout.activity_source_test);
        
        // 根據(jù)控件id在控件樹中找到對應(yīng)的實體控件對象并綁定
        mTextView = findViewById(R.id.tv);
        mButton = findViewById(R.id.btn);
        
        // 綁定按鍵控件的點擊事件處理回調(diào)
        mButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Toast.makeText(v.getContext(), ((TextView) v).getText(), Toast.LENGTH_SHORT).show();
            }
        });
    }
}

界面中有一個文本控件和一個按鈕控件,都需要程序員通過 findViewById 和 setOnClickListner 方法來綁定實體 View 和其對應(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();
    }
}

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

2.2 反射處理運行時注解實現(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 注解的生命周期為運行時,即 .java .class 加載到內(nèi)存中三個時期,@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 運行時反射綁定

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ī)推渫ㄟ^動態(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實體,并綁定到View引用上,最后將其緩存到SparseArray中。
  • 之后,通過反射遍歷所有方法域,找出其中附加了@OnClick注解的方法,由于Java中方法不是對象,不能獨立存在,必須存在于類中,所以需要通過動態(tài)代理機(jī)制來代理View.OnClickListener接口,“包裹”所注解的方法,最后通過SparseArray的鍵值resId找到View實體進(jìn)行回調(diào)方法的綁定。
    最后是Activity中的使用,界面中一共是兩個按鈕控件,綁定同一個回調(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庫中的注解都是運行時注解,即源文件中的注解也會出現(xiàn)在最后的運行時內(nèi)存中,可以通過反射的方式拿到注解,從而獲得被注解的域或方法,最后在運行時再進(jìn)行綁定,由于待綁定的元素都需要通過反射來拿到,故效率較低。

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

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


圖2-1.項目結(jié)構(gòu)
  • app為安卓的測試module,用于測試自定義的偽ButterKnife庫;
  • butterknife-annotation 為包含自定義注解的module;
  • butterknife-compiler 為自定義注解處理器的module,這里將注解處理器和所能處理的注解分在兩個不同的module,主要是因為注解處理器實際上只是被編譯器在編譯時調(diào)用,并不屬于工程,所以在打包時可以將其不打入apk中以節(jié)省空間,如果自定義的注解和注解處理器處在同一module,那么使用時,app module中會找不到自定義注解,所以需要將注解獨立成butterknife-annotation module ,在 app 和注解處理器 butterknife-compiler module 中分別引用,自定義注解處理器在編譯時讀取源文件中的注解,生成新的源代碼文件參與編譯;
  • butterknife-library module 負(fù)責(zé)通過反射調(diào)用 butterknife-compiler 中自動生成的代碼

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)聲明了這些注解都是源碼級注解,即編譯后在 .class 文件中便不存在了,注意建立butterknife-annotation 庫時,選擇該庫為 java library,如下圖所示:


圖2-2.java library

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


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

2.3.2 自定義注解處理器

自定義注解處理器的基本原理是將程序員自定義的注解處理器注冊到編譯器中,編譯器在編譯源文件時調(diào)用該注解處理器處理源文件中的注解,處理這些注解時是無法修改源文件的,只能生成新的源文件,生成的新文件會繼續(xù)調(diào)用注解處理器處理,這是一個遞歸的過程,一輪一輪地處理,直到?jīng)]有新的源文件生成,我們正是利用這點來自動生成綁定控件的模板代碼。大致原理如下圖所示:


圖2-4.編譯器原理

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

  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并實現(xiàn) process() 方法, 可選擇實現(xiàn) init()、getSupportedAnnotationTypes() 、
getSupportedSourceVersion()三個方法,其中,getSupportedAnnotationTypes() 、
getSupportedSourceVersion()是返回本注解處理器所支持的注解和源代碼版本,可以在注解處理器上加上@SupportedSourceVersion()
@SupportedAnnotationTypes()注解實現(xiàn)同樣的效果,init()方法主要是為了初始化一些全局的工具。

  1. 向編譯器注冊本注解處理器,可以手動注冊:在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)即可,這樣會自動創(chuàng)建上述路徑和文件,如下圖所示:
圖2-5.auto-services

4.實現(xiàn)注解處理器的process()方法,形參Set是本處理器可處理的注解類型,就是@SupportedAnnotationTypes()中所設(shè)置的類型,RoundEnvironment 則提供了本輪次的語法樹元素,如果方法返回true,則后續(xù)其他處理器不能處理這些注解,否則可以處理,類似點擊事件的攔截。上述代碼中將讀取到的屬性字段和方法對應(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 庫, 用于注冊自定義注解處理器到編譯器中
    implementation 'com.google.auto.service:auto-service:1.0-rc4'

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

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

sourceCompatibility = "1.7"
targetCompatibility = "1.7"

最終自動生成的源代碼如下:

// 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)用自動生成的 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();
        }
    }
}

由于自動生成的類名和方法名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();
    }
}

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

3.總結(jié)

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 用兩張圖告訴你,為什么你的 App 會卡頓? - Android - 掘金 Cover 有什么料? 從這篇文章中你...
    hw1212閱讀 13,913評論 2 59
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 178,765評論 25 709
  • 什么是注解注解分類注解作用分類 元注解 Java內(nèi)置注解 自定義注解自定義注解實現(xiàn)及使用編譯時注解注解處理器注解處...
    Mr槑閱讀 1,147評論 0 3
  • Java中的注解是個很神奇的東西,還不了解的可以看下一小時搞明白自定義注解(Annotation)?,F(xiàn)在很多And...
    顧明偉閱讀 3,353評論 1 3
  • 下午工作有點倦怠, 問同事鵬哥,你閨女也是一口東北腔嗎? 鵬哥:不是的,來,我讓你聽聽她唱的歌 鵬哥滿臉幸福的從錄...
    璞玉丁閱讀 212評論 0 0

友情鏈接更多精彩內(nèi)容