Butterknife深入理解之自己動手編寫B(tài)utterknife

Demo的源碼地址在 mini-butterknife, 有興趣的可以下載源碼來看.

Butterknife 框架介紹

butterknife是一款View注入的框架,在android開發(fā)時省去我們重復(fù)的敲打findViewById,setOnclickListener等方法.

Field and method binding for Android views which uses annotation processing to generate boilerplate code for you.
通過注解處理器 (Annotation Processor) 自動生成樣板代碼,來綁定 Android視圖中的 字段 和方法.

從方法的介紹中,我們可以看出, Butterknife 使用 APT(Annotation Processing Tool)編譯時解析技術(shù)來實現(xiàn)代碼的生成。

(注:我們也可以在 運行時, 通過反射的方式拿到 注解信息進行處理, 但是這種方式效率比較低,不是本文討論的問題.)

而代碼的生成, Butterknife 則使用 JavaPoet 庫來生成java文件.

APT 是需要你 繼承 AbstractProcessor類, 在編譯期的時候會運行 AbstractProcessorprocess方法來解析注解, 以及處理生成樣板代碼.

而樣板代碼,則通過 反射調(diào)用來進行.

所以, 可以總結(jié)成三個步驟

  1. 繼承AbstractProcessor解析注解數(shù)據(jù)
  2. 根據(jù)解析的注解數(shù)據(jù),通過JavaPoet生成java樣板代碼
  3. 運行時,通過反射獲取到樣板代碼的實例,進行調(diào)用

注解知識

網(wǎng)上有很多java注解相關(guān)的文章, 這里就不對注解的基礎(chǔ)知識進行展開,推薦 Java注解處理器 進行閱讀.

這里主要對自定義注解的解析做一些說明.

AbstractProcessor

需要關(guān)注注解處理器的以下方法 :

  • void init(ProcessingEnvironment processingEnv)

init方法中,主要是為了 獲取以下 輔助的方法類.

env.getFiler() : 注解的文件處理類
env.getMessager() : 日志輸出類
env.getElementUtils() : 元素輔助類
env.getTypeUtils() : 類型輔助類等.

  • Set<String> getSupportedAnnotationTypes()

這里需要你指定 你關(guān)注的注解. 或者說 你關(guān)注的注解類型,需要在這里注冊,process方法才能夠正常處理你關(guān)注的注解.

  • SourceVersion getSupportedSourceVersion()

指定你使用的Java版本。通常這里返回SourceVersion.latestSupported().

  • boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv)

這里是你的掃描峦萎、評估和處理注解,以及生成Java文件的邏輯代碼.

模板代碼:

// 使用@AutoService注解,避免了在resource文件夾中注冊的步驟
@AutoService(Processor.class)
public class MyProcessor extends AbstractProcessor {
    private Filer filer;
    private Messager messager;

    /**
     * 初始化, 獲取各種工具類
     */
    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        filer = processingEnv.getFiler();
        messager = processingEnv.getMessager();
    }

    /**
     * 注冊感興趣的注解類
     */
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> annotations = new LinkedHashSet<>();
        annotations.add(BindView.class.getCanonicalName());
        annotations.add(OnClick.class.getCanonicalName());
        return annotations;
    }

    /**
     * 返回使用的java版本
     */
    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }

    /**
     * 掃描判族、評估和處理注解瓜喇,以及生成Java文件的邏輯代碼
     */
    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        return handleProcess(annotations, roundEnv);
    }
}

MODEL API

注解元素解析相關(guān)的api,稱其為MODEL API是因為 注解解析的類存放在 javax.lang.model包下.

process方法中,我們通過 for (Element element : roundEnv.getElementsAnnotatedWith(BindView.class)) 來遍歷我們感興趣的注解元素.

  1. 判斷注解是否使用在字段上
    // 判斷注釋 是否在 字段上
    if (!(element instanceof VariableElement) ||  element.getKind() != ElementKind.FIELD) {
        throw new IllegalStateException("@BindView annotation must be on a field.");
    }
  1. 判斷注解是否使用在方法上
            // 判斷注釋 是否在 方法上
            if (!(element instanceof ExecutableElement) || element.getKind() != ElementKind.METHOD) {
                throw new IllegalStateException("@OnClick annotation must be on a method.");
            }
  1. 獲取被注解所在的類的名稱
            // 獲取目標(biāo)類的類型
            // 如(xxx.MainActivity)
            TypeElement typeElement = (TypeElement) element.getEnclosingElement();
            String targetClassName = typeElement.getQualifiedName().toString();
  1. 獲取注解實例,以及注解中的值
// 獲取注解實例
BindView bindView = element.getAnnotation(BindView.class);
// 獲取注解值
// @BindView(R.id.btn) 中 R.id.btn對應(yīng)的int值
int value = bindView.value();
  1. 獲取字段或者方法的名稱
// 獲取字段或者方法的名稱
// Button btn; 中的 btn
// void handleClick(View v); 中的 handleClick
String name = element.getSimpleName().toString();
  1. 獲取字段或者方法中的參數(shù)或者返回值的類型

由于原生中,確定類型的判斷方法比較繁瑣(需要根據(jù)各種getKind判斷基礎(chǔ)類型,引用類型,數(shù)組類型,泛型等),這里可以直接通過 JavaPoet庫中的 TypeName類來確定類型

        // 可以根據(jù) javapoet 中的 TypeName 來確定類型
        // @BindView(R.id.btn) Button btn; 中 得到 android.view.Button
        TypeMirror mirror = element.asType();
        TypeName typeName = TypeName.get(mirror);
  1. 獲取方法的返回類型
        // 獲取返回值的類型
        ExecutableElement executableElement = (ExecutableElement) element;
        TypeMirror returnType = executableElement.getReturnType();
        TypeName returnClass = TypeName.get(returnType);
  1. 獲取方法的參數(shù)信息
        ExecutableElement executableElement = (ExecutableElement) element;
        // 獲取方法的參數(shù)信息
        List<? extends VariableElement> parameters = executableElement.getParameters();
        // 獲取傳入?yún)?shù)的個數(shù)
        int parameterSize = parameters.size();
        // 獲取參數(shù)類型以及參數(shù)名稱
        for (VariableElement parameter : parameters) {
            TypeMirror paramType = parameter.asType();
            // 參數(shù)類型
            TypeName paramClass = TypeName.get(paramType);
            // 參數(shù)名
            Name paramName = parameter.getSimpleName();
        }
  1. 判斷是否 該類型是否是某個類型的子類型
    /**
     * 遞歸向上查找 是否是其對應(yīng)的子類型
     */
    static boolean isSubtypeOfType(TypeMirror typeMirror, String otherType) {
        if (otherType.equals(typeMirror.toString())) {
            return true;
        }
        if (typeMirror.getKind() != TypeKind.DECLARED) {
            return false;
        }
        DeclaredType declaredType = (DeclaredType) typeMirror;
        List<? extends TypeMirror> typeArguments = declaredType.getTypeArguments();
        if (typeArguments.size() > 0) {
            StringBuilder typeString = new StringBuilder(declaredType.asElement().toString());
            typeString.append('<');
            for (int i = 0; i < typeArguments.size(); i++) {
                if (i > 0) {
                    typeString.append(',');
                }
                typeString.append('?');
            }
            typeString.append('>');
            if (typeString.toString().equals(otherType)) {
                return true;
            }
        }
        Element element = declaredType.asElement();
        if (!(element instanceof TypeElement)) {
            return false;
        }
        TypeElement typeElement = (TypeElement) element;
        TypeMirror superType = typeElement.getSuperclass();
        if (isSubtypeOfType(superType, otherType)) {
            return true;
        }
        for (TypeMirror interfaceType : typeElement.getInterfaces()) {
            if (isSubtypeOfType(interfaceType, otherType)) {
                return true;
            }
        }
        return false;
    }

JavaPoet

JavaPoet 是一個使用可編程的方式來生成java源文件.

GitHub地址 : JavaPoet

readme 里面詳細的介紹了 它的用法,這里就不再贅述.

動手編寫B(tài)utterKnife

一個示例

public class MainActivity extends AppCompatActivity {
    // 解綁器
    private Unbinder unbinder;

    @BindView(R.id.btn2)
    Button btn2;
    @BindView(R.id.btn)
    Button button;

    int id = 0;

    @OnClick({R.id.btn, R.id.btn2})
    void clickMethod(View view) {
        Toast.makeText(this, "i am a toast !!", Toast.LENGTH_SHORT).show();
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Toolbar toolbar = findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);

        FloatingActionButton fab = findViewById(R.id.fab);
        fab.setOnClickListener(view -> {
            button.setText("i'm button " + (++id));
            btn2.setText("xxx button " + (++id));
        });
        // 綁定
        unbinder = ButterKnife.bind(this);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        // 解綁, 移除監(jiān)聽器等
        if (unbinder != null) unbinder.unbind();
    }
}

這里再說一下 ButterKnife的流程.

  1. 通過 注解處理器 解析注解數(shù)據(jù), 并且生成 java源代碼
public class MainActivity_ViewBinding implements Unbinder {
  private MainActivity target;

  private View view7f080025;

  private View view7f080024;

  @UiThread
  public MainActivity_ViewBinding(MainActivity target) {
    this(target, target.getWindow().getDecorView());
  }

  @UiThread
  public MainActivity_ViewBinding(final MainActivity target, View source) {
    this.target = target;

    View view;
    view = Utils.findRequiredView(source, R.id.btn2, "field 'btn2' and method 'clickMethod'");
    target.btn2 = Utils.castView(view, R.id.btn2, "field 'btn2'", Button.class);
    view7f080025 = view;
    view.setOnClickListener(new DebouncingOnClickListener() {
      @Override
      public void doClick(View p0) {
        target.clickMethod(p0);
      }
    });
    view = Utils.findRequiredView(source, R.id.btn, "field 'button' and method 'clickMethod'");
    target.button = Utils.castView(view, R.id.btn, "field 'button'", Button.class);
    view7f080024 = view;
    view.setOnClickListener(new DebouncingOnClickListener() {
      @Override
      public void doClick(View p0) {
        target.clickMethod(p0);
      }
    });
  }

  @Override
  @CallSuper
  public void unbind() {
    MainActivity target = this.target;
    if (target == null) throw new IllegalStateException("Bindings already cleared.");
    this.target = null;

    target.btn2 = null;
    target.button = null;

    view7f080025.setOnClickListener(null);
    view7f080025 = null;
    view7f080024.setOnClickListener(null);
    view7f080024 = null;
  }
}

這個就是 ButterKnife 編譯時生成代碼.

  1. ButterKnife.bind(this);時, 方法內(nèi)部使用反射生成上述樣板代碼的實例返回.

也就是我們拿到的 Unbinder實例對象.

有了上面的介紹,我們可以很輕松的寫出上述的代碼功能.

模塊劃分

我們將 功能分成3個模塊,來構(gòu)建項目.

  1. 注解模塊(butterknife_annotations)

注解模塊, 存放著所有的注解類,之所以將它 作為一個單獨的模塊存放,是因為 注解類在注解處理器階段(即編譯階段)需要使用到, 并且在運行階段(第三方使用過程中) 也需要用到, 這種兩個階段都需要用到的我們單獨分一個模塊.

  1. 注解處理器模塊(butterknife_processor)

注解處理器模塊,使用來 獲取注解信息和生成java樣板代碼的. 只在編譯器使用, 代碼不會被打包到程序中去.

gradle依賴中使用 annotationProcessor project(':butterknife_processor')來導(dǎo)入.

  1. butterknife模塊

這個模塊負責(zé)綁定 activity等類, 通過反射獲取 樣板代碼的實例,返回給調(diào)用者,這個模塊是 運行時依賴.

注解模塊(butterknife_annotations)

文章的目的是為了 實現(xiàn) @BindView@OnClick注解的功能.

因為, 這個模塊,只有這兩個類的代碼.

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

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

這里由于不考慮,運行時通過反射拿到注解, 所以直接使用 RetentionPolicy.SOURCE策略.

注解處理器模塊(butterknife_processor)

這個模塊是生成樣板代碼的模塊, 我們需要每一個 目標(biāo)類(如MainActivity)中的所有注解, 解析后生成一份樣板代碼.

    @BindView(R.id.btn2)
    Button btn2;

@BindView 是注解在 字段上, 我們需要關(guān)注它 字段名, 和字段類型, 以及注解的值(R.id.btn2)

public class FieldBinding {
    private int value;
    private ClassName fieldType;
    private String fieldName;

    public FieldBinding(int value, ClassName fieldType, String name) {
        this.value = value;
        this.fieldType = fieldType;
        this.fieldName = name;
    }

    public int getValue() {
        return value;
    }

    public ClassName getFieldType() {
        return fieldType;
    }

    public String getFieldName() {
        return fieldName;
    }
}
    @OnClick({R.id.btn, R.id.btn2})
    void click(View view) {
        Toast.makeText(this, "i am a toast !!", Toast.LENGTH_SHORT).show();
    }

@OnClick 是注解在方法上的, 我們需要關(guān)注它的 方法名,和注解的值; 由于 @OnClick 注解的方法,如果有方法參數(shù),必定只能是 android.view.View,所以這里只判斷是否有參數(shù)就行了.

public class MethodBinding {
    private String methodName;
    private int value;
    private boolean hasParam;

    public MethodBinding(String methodName, int value, boolean hasParam) {
        this.methodName = methodName;
        this.hasParam = hasParam;
        this.value = value;
    }

    public String getMethodName() {
        return methodName;
    }

    public int getValue() {
        return value;
    }

    public boolean hasParam() {
        return hasParam;
    }
}

我們需要根據(jù)我們關(guān)注的這些數(shù)據(jù), 使用 javapoet 庫來生成樣板代碼, 這里直接貼出, 生成代碼的類, api的使用 進入 javapoet 官網(wǎng)自行查找.

class ViewBinding {
    private static final ClassName UTILS = ClassName.get("cn.jimmie.learn.butterknife", "Utils");
    private static final ClassName VIEW = ClassName.get("android.view", "View");
    private static final ClassName UNBINDER = ClassName.get("cn.jimmie.learn.butterknife", "Unbinder");
    private static final ClassName LISTENER = ClassName.get("cn.jimmie.learn.butterknife", "DebouncingOnClickListener");
    private static final ClassName ILLEGAL_STATE_EXCEPTION = ClassName.get("java.lang", "IllegalStateException");

    private List<FieldBinding> fieldBindings = new ArrayList<>();
    private List<MethodBinding> methodBindings = new ArrayList<>();
    private ClassName target;

    ViewBinding(ClassName target) {
        this.target = target;
    }

    void addFiled(FieldBinding field) {
        fieldBindings.add(field);
    }

    void addMethod(MethodBinding method) {
        methodBindings.add(method);
    }


    JavaFile brewJava() {
        // 單參數(shù)構(gòu)造函數(shù) $ctor(Object target)
        MethodSpec ctor1 = MethodSpec.constructorBuilder()
                .addModifiers(Modifier.PUBLIC)
                .addParameter(target, "target")
                .addStatement("this(target, target.getWindow().getDecorView())")
                .build();

        // 構(gòu)建兩個參數(shù)的構(gòu)造函數(shù) $ctor(Object target,View source)
        MethodSpec.Builder ctorBuilder = MethodSpec.constructorBuilder()
                .addModifiers(Modifier.PUBLIC)
                .addParameter(target, "target", Modifier.FINAL)
                .addParameter(VIEW, "source")
                .addStatement("this.$N = $N", "target", "target");

        // 解綁器
        MethodSpec.Builder unbind = MethodSpec.methodBuilder("unbind")
                .addModifiers(Modifier.PUBLIC)
                .addAnnotation(Override.class)
                .addStatement("if (target == null) throw new $T(\"Bindings already cleared.\")", ILLEGAL_STATE_EXCEPTION);

        // 類構(gòu)建器
        TypeSpec.Builder clsBuilder = TypeSpec.classBuilder(target.simpleName() + "Unbinder")
                .addSuperinterface(UNBINDER)
                .addModifiers(Modifier.PUBLIC)
                .addField(target, "target", Modifier.PRIVATE)
                .addMethod(ctor1);


        // 方法和字段綁定
        List<Pair<FieldBinding, MethodBinding>> fieldMethods = merge();

        for (Pair<FieldBinding, MethodBinding> fieldMethod : fieldMethods) {
            if (fieldMethod == null) continue;
            FieldBinding field = fieldMethod.getKey();
            MethodBinding method = fieldMethod.getValue();

            // 只綁定字段
            if (field != null && method == null)
                brewOnlyField(field, ctorBuilder, unbind);
                // 只綁定方法
            else if (field == null && method != null)
                brewOnlyMethod(method, ctorBuilder, clsBuilder, unbind);
                // 綁定 字段和方法
            else brewFieldMethod(field, method, ctorBuilder, unbind);
        }

        unbind.addStatement("target = null");

        clsBuilder.addMethod(ctorBuilder.build())
                .addMethod(unbind.build());

        return JavaFile.builder(target.packageName(), clsBuilder.build())
                .build();
    }

    private void brewOnlyField(FieldBinding field, MethodSpec.Builder ctorBuilder, MethodSpec.Builder unbind) {
        ClassName fieldType = field.getFieldType();
        ctorBuilder.addStatement("target.$L = $T.findRequiredViewAsType(source, $L, \"field '$L'\", $T.class)",
                field.getFieldName(), UTILS, field.getValue(), field.getFieldName(), fieldType);
        unbind.addStatement("target.$L = null", field.getFieldName());
    }

    private void brewOnlyMethod(MethodBinding method, MethodSpec.Builder ctorBuilder, TypeSpec.Builder clsBuilder, MethodSpec.Builder unbind) {
        String methodName = method.getMethodName();
        int value = method.getValue();
        String member = "view" + value;
        TypeSpec anonymous = brewAnonymousClass(methodName, method.hasParam());

        // 添加成員變量
        clsBuilder.addField(VIEW, member, Modifier.PRIVATE);
        // view7f080024 = Utils.findRequiredView(source, R.id.btn, "method 'click'");
        ctorBuilder.addStatement("$L = $T.findRequiredView(source, $L, \"method 'click'\")",
                member, UTILS, value);
        ctorBuilder.addStatement("$L.setOnClickListener($L)", member, anonymous);

        unbind.addStatement("$L.setOnClickListener(null)", member);
        unbind.addStatement("$L = null", member);
    }

    private void brewFieldMethod(FieldBinding field, MethodBinding method, MethodSpec.Builder ctorBuilder, MethodSpec.Builder unbind) {
        ClassName fieldType = field.getFieldType();
        String methodName = method.getMethodName();

        ctorBuilder.addStatement("target.$L = $T.findRequiredViewAsType(source, $L, \"field '$L' and method '$L'\", $T.class)",
                field.getFieldName(), UTILS, field.getValue(), field.getFieldName(), methodName, fieldType);

        TypeSpec anonymous = brewAnonymousClass(methodName, method.hasParam());

        // target.view.setOnClickListener(new DebouncingOnClickListener() {
        ctorBuilder.addStatement("target.$L.setOnClickListener($L)", field.getFieldName(), anonymous);

        unbind.addStatement("target.$L.setOnClickListener(null)", field.getFieldName());
        unbind.addStatement("target.$L = null", field.getFieldName());
    }

    private TypeSpec brewAnonymousClass(String name, boolean hasParam) {
        String statement = hasParam ? "target.$L(v)" : "target.$L()";
        return TypeSpec.anonymousClassBuilder("")
                .addSuperinterface(LISTENER)
                .addMethod(MethodSpec.methodBuilder("doClick")
                        .addAnnotation(Override.class)
                        .addModifiers(Modifier.PUBLIC)
                        .addParameter(VIEW, "v")
                        .addStatement(statement, name)
                        .build())
                .build();
    }

    // 合并 字段綁定 和 方法綁定相關(guān)
    private List<Pair<FieldBinding, MethodBinding>> merge() {
        List<Pair<FieldBinding, MethodBinding>> list = new ArrayList<>();
        boolean hasAdd = false;

        for (FieldBinding fieldBinding : fieldBindings) {
            for (MethodBinding methodBinding : methodBindings) {
                if (fieldBinding.getValue() == methodBinding.getValue()) {
                    list.add(new Pair<>(fieldBinding, methodBinding));
                    hasAdd = true;
                    break;
                }
            }
            if (!hasAdd) list.add(new Pair<>(fieldBinding, null));
            else hasAdd = false;
        }

        for (MethodBinding methodBinding : methodBindings) {
            for (FieldBinding fieldBinding : fieldBindings) {
                if (fieldBinding.getValue() == methodBinding.getValue()) {
                    hasAdd = true;
                    break;
                }
            }
            if (!hasAdd) list.add(new Pair<>(null, methodBinding));
            else hasAdd = false;
        }
        return list;
    }
}

接下來,我們來解析 注解數(shù)據(jù),一下的代碼出現(xiàn)在

先對 @BindView 的注解進行解析 class ButterKnifeProcessor extends AbstractProcessorprocess方法中.

 // 綁定ViewId 遍歷所有被注解了@BindView的元素
        for (Element element : roundEnv.getElementsAnnotatedWith(BindView.class)) {
            // 判斷注釋 是否在 字段上
            if (!(element instanceof VariableElement) || element.getKind() != ElementKind.FIELD) {
                throw new IllegalStateException("@BindView annotation must be on a field.");
            }

            // 獲取目標(biāo)類的類型 如(xxx.MainActivity)
            TypeElement typeElement = (TypeElement) element.getEnclosingElement();
            String targetClassName = typeElement.getQualifiedName().toString();

            // 根據(jù)類名來生成文件, 一個被注解的類中所有的注解對應(yīng)生成一份類文件
            ViewBinding binding = bindingMap.get(targetClassName);
            if (binding == null) {
                binding = new ViewBinding(ClassName.bestGuess(targetClassName));
                bindingMap.put(targetClassName, binding);
            }

            // 獲取注解上的值(這里是R.id對應(yīng)的int值)
            int value = element.getAnnotation(BindView.class).value();
            // 獲取字段的類型
            ClassName fieldClass = (ClassName) ClassName.get(element.asType());

            // 判斷字段類型是否是View的子類型
            if (!Utils.isSubtypeOfType(element.asType(), "android.view.View")) {
                throw new IllegalStateException("field type must be the child of android.view.View");
            }

            // 獲取字段的名稱
            String name = element.getSimpleName().toString();

            // 添加字段綁定
            FieldBinding field = new FieldBinding(value, fieldClass, name);
            binding.addFiled(field);

            shouldWrite = true;
        }

然后對 @OnClick注解進行解析

        // 綁定監(jiān)聽事件 遍歷所有被注解了@OnClick的元素
        for (Element element : roundEnv.getElementsAnnotatedWith(OnClick.class)) {
            // 判斷注釋 是否在 方法上
            if (!(element instanceof ExecutableElement) || element.getKind() != ElementKind.METHOD) {
                throw new IllegalStateException("@Click annotation must be on a method.");
            }

            ExecutableElement executableElement = (ExecutableElement) element;
            TypeElement typeElement = (TypeElement) element.getEnclosingElement();

            // 獲取目標(biāo)類的類型 如(xxx.MainActivity)
            String targetName = typeElement.getQualifiedName().toString();

            // 添加方法綁定
            ViewBinding binding = bindingMap.get(targetName);
            if (binding == null) {
                binding = new ViewBinding(ClassName.bestGuess(targetName));
                bindingMap.put(targetName, binding);
            }

            // 獲取方法的參數(shù)列表
            List<? extends VariableElement> methodParameters = executableElement.getParameters();
            // 獲取方法參數(shù)的個數(shù)
            int methodParameterSize = methodParameters.size();
            // 參數(shù)校驗
            if (methodParameterSize > 1) {
                throw new IllegalStateException("@Click method only has one param, and must be android.view.View");
            } else if (methodParameterSize == 1) {
                VariableElement param = methodParameters.get(0);
                TypeName paramType = TypeName.get(param.asType());
                if (!paramType.toString().equals("android.view.View")) {
                    throw new IllegalStateException("@Click method only has one param, and must be android.view.View");
                }
            }

            // 獲取返回類型
            TypeName returnType = TypeName.get(executableElement.getReturnType());
            if (!returnType.toString().equals("void")) {
                throw new IllegalStateException("@Click method return type must be void");
            }

            // 獲取注解上的值 [R.id.x1,R.id.x2]
            int[] values = element.getAnnotation(OnClick.class).value();
            if (values.length == 0) continue;

            // 獲取注解上的方法名
            String methodName = element.getSimpleName().toString();

            // 添加 方法綁定
            for (int value : values) {
                MethodBinding method = new MethodBinding(methodName, value, methodParameterSize == 1);
                binding.addMethod(method);
            }

            shouldWrite = true;
        }

最后對解析的數(shù)據(jù)生成 樣板代碼.

 if (shouldWrite) {
            // 生成java文件
            for (String key : bindingMap.keySet()) {
                ViewBinding binding = bindingMap.get(key);
                JavaFile javaFile = binding.brewJava();
                try {
                    javaFile.writeTo(filer);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

這樣我們的 注解解析和生成樣板代碼的過程就結(jié)束.

我們來驗證下, 在 MainActivity中,聲明一下字段和方法.

    @BindView(R.id.btn2)
    Button btn2;
    @BindView(R.id.btn)
    Button button;

    int id = 0;

    @OnClick({R.id.btn, R.id.btn2})
    void click(View view) {
        Toast.makeText(this, "i am a toast !!", Toast.LENGTH_SHORT).show();
    }

然后, 運行 gradlecompileDebugJavaWithJavac腳本, 就會自動生成一份 MainActivityUnbinder的樣板文件.

public class MainActivityUnbinder implements Unbinder {
  private MainActivity target;

  public MainActivityUnbinder(MainActivity target) {
    this(target, target.getWindow().getDecorView());
  }

  public MainActivityUnbinder(final MainActivity target, View source) {
    this.target = target;
    target.btn2 = Utils.findRequiredViewAsType(source, 2131230757, "field 'btn2' and method 'click'", Button.class);
    target.btn2.setOnClickListener(new DebouncingOnClickListener() {
      @Override
      public void doClick(View v) {
        target.click(v);
      }
    });
    target.button = Utils.findRequiredViewAsType(source, 2131230756, "field 'button' and method 'click'", Button.class);
    target.button.setOnClickListener(new DebouncingOnClickListener() {
      @Override
      public void doClick(View v) {
        target.click(v);
      }
    });
  }

  @Override
  public void unbind() {
    if (target == null) throw new IllegalStateException("Bindings already cleared.");
    target.btn2.setOnClickListener(null);
    target.btn2 = null;
    target.button.setOnClickListener(null);
    target.button = null;
    target = null;
  }
}

butterknife模塊

這個模塊是用于反射調(diào)用 樣板代碼的.

public class ButterKnife {
    private static final Map<Class<?>, Constructor<? extends Unbinder>> BINDINGS = new LinkedHashMap<>();

    @UiThread
    public static Unbinder bind(@NonNull Activity target) {
        View sourceView = target.getWindow().getDecorView();
        return bind(target, sourceView);
    }

    @UiThread
    public static Unbinder bind(@NonNull Object target, @NonNull View source) {
        Class<?> targetClass = target.getClass();
        Constructor<? extends Unbinder> constructor = findBindingConstructorForClass(targetClass);

        if (constructor == null) {
            return Unbinder.EMPTY;
        }

        try {
            return constructor.newInstance(target, source);
        } catch (Exception e) {
            throw new RuntimeException("Unable to create binding instance.", e.getCause());
        }
    }

    @UiThread
    private static Constructor<? extends Unbinder> findBindingConstructorForClass(Class<?> cls) {
        Constructor<? extends Unbinder> bindingCtor = BINDINGS.get(cls);
        if (bindingCtor != null) {
            return bindingCtor;
        }
        String clsName = cls.getName();
        try {

            Class<?> bindingClass = Objects.requireNonNull(cls.getClassLoader()).loadClass(clsName + "Unbinder");
            //noinspection unchecked
            bindingCtor = (Constructor<? extends Unbinder>) bindingClass.getConstructor(cls, View.class);
        } catch (Exception e) {
            throw new RuntimeException("Unable to find binding constructor for " + clsName, e);
        }
        BINDINGS.put(cls, bindingCtor);
        return bindingCtor;
    }
}

這種代碼,比較容易理解, 就不做過多說明, 這里說明下 其他兩個類.DebouncingOnClickListenerUnbinder.

DebouncingOnClickListener類的作用是,避免在極短的時間內(nèi)重復(fù)的過快的響應(yīng)點擊事件.

public abstract class DebouncingOnClickListener implements View.OnClickListener {
    static boolean enabled = true;

    private static final Runnable ENABLE_AGAIN = () -> enabled = true;

    @Override public final void onClick(View v) {
        if (enabled) {
            enabled = false;
            v.post(ENABLE_AGAIN);
            doClick(v);
        }
    }

    public abstract void doClick(View v);
}

Unbinder 接口是 樣板代碼 都要實現(xiàn)的接口, 用于監(jiān)聽事件 和 資源的移除.

public interface Unbinder {
   Unbinder EMPTY = () -> {};

    void unbind();
}

至此, 實現(xiàn) ButterKnife@BindView@OnClick功能, 都以實現(xiàn).

感興趣的同學(xué), 可以到 mini-butterknife,下載源碼查看

引用

  1. butterknife
  2. JavaPoet
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市枕扫,隨后出現(xiàn)的幾起案子三妈,更是在濱河造成了極大的恐慌畜埋,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,941評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件畴蒲,死亡現(xiàn)場離奇詭異悠鞍,居然都是意外死亡,警方通過查閱死者的電腦和手機模燥,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,397評論 3 395
  • 文/潘曉璐 我一進店門狞玛,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人涧窒,你說我怎么就攤上這事心肪。” “怎么了纠吴?”我有些...
    開封第一講書人閱讀 165,345評論 0 356
  • 文/不壞的土叔 我叫張陵硬鞍,是天一觀的道長。 經(jīng)常有香客問我戴已,道長固该,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,851評論 1 295
  • 正文 為了忘掉前任糖儡,我火速辦了婚禮伐坏,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘握联。我一直安慰自己桦沉,他們只是感情好每瞒,可當(dāng)我...
    茶點故事閱讀 67,868評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著纯露,像睡著了一般剿骨。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上埠褪,一...
    開封第一講書人閱讀 51,688評論 1 305
  • 那天浓利,我揣著相機與錄音,去河邊找鬼钞速。 笑死贷掖,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的渴语。 我是一名探鬼主播羽资,決...
    沈念sama閱讀 40,414評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼遵班!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起潮改,我...
    開封第一講書人閱讀 39,319評論 0 276
  • 序言:老撾萬榮一對情侶失蹤狭郑,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后汇在,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體翰萨,經(jīng)...
    沈念sama閱讀 45,775評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年糕殉,在試婚紗的時候發(fā)現(xiàn)自己被綠了亩鬼。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,096評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡阿蝶,死狀恐怖雳锋,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情羡洁,我是刑警寧澤玷过,帶...
    沈念sama閱讀 35,789評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站筑煮,受9級特大地震影響辛蚊,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜真仲,卻給世界環(huán)境...
    茶點故事閱讀 41,437評論 3 331
  • 文/蒙蒙 一袋马、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧秸应,春花似錦虑凛、人聲如沸碑宴。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,993評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽墓懂。三九已至,卻和暖如春霉囚,著一層夾襖步出監(jiān)牢的瞬間捕仔,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,107評論 1 271
  • 我被黑心中介騙來泰國打工盈罐, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留榜跌,地道東北人。 一個月前我還...
    沈念sama閱讀 48,308評論 3 372
  • 正文 我出身青樓盅粪,卻偏偏與公主長得像钓葫,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子票顾,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,037評論 2 355

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

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,167評論 25 707
  • Jake Wharton 是 Android 大神础浮,同時也是開源狂魔。他開源的項目特點是小而美奠骄,且應(yīng)用廣泛豆同,比如 ...
    geniusmart閱讀 12,092評論 3 79
  • 用兩張圖告訴你,為什么你的 App 會卡頓? - Android - 掘金 Cover 有什么料含鳞? 從這篇文章中你...
    hw1212閱讀 12,730評論 2 59
  • 如果我們不曾相遇 (Cover 五月天) 如果我們不曾相遇 我會是在哪里 如果我們從不曾相識 不存在這首歌曲 每秒...
    小小同學(xué)兒閱讀 433評論 0 0
  • 我們生活在一個鼓勵標(biāo)榜自我的時代蝉绷,我們每個人盡管渺小鸭廷,但都已然形成了自己很重要、很獨特熔吗、很不可替代的自我意識辆床,這不...
    樂天派大星閱讀 226評論 2 2