簡單分析 Butter Knife 源碼

1. Butter Knife 介紹

Butter Knife 是一個開源的依賴注入框架, 主要起到一個語法糖的效果, 好比 Button button = (Button) findViewById(R.id.button);, 就能夠簡化成 @BindView(R.id.button) Button button;. 詳細的能夠看這裏. 顯然能夠看出是使用註解作到的, 至因而編譯時註解仍是運行時註解, 下面會開始分析. 這裏使用的版本是 8.6.0, 簡單起見, 本篇只分析 View 的注入.java

2. 注入原理分析

這裏是一個 activityandroid

public class LoginActivity extends Activity {
    @BindView(R.id.loginButton)
    Button button;
    
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.login_layout);
        ButterKnife.bind(this);
    }
    ......
}

因此是怎麼注入的呢 ? 感受應該和 ButterKnife.bind(this); 有關.
該方法以下git

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

繼續點開 createBindinggithub

private static Unbinder createBinding(@NonNull Object target, @NonNull View source) {
    Class<?> targetClass = target.getClass();
    if (debug) Log.d(TAG, "Looking up binding for " + targetClass.getName());
    Constructor<? extends Unbinder> constructor = findBindingConstructorForClass(targetClass);

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

    //noinspection TryWithIdenticalCatches Resolves to API 19+ only type.
    try {
      return constructor.newInstance(target, source);
    } catch (IllegalAccessException e) {
      throw new RuntimeException("Unable to invoke " + constructor, e);
    } catch (InstantiationException e) {
      throw new RuntimeException("Unable to invoke " + constructor, e);
    } catch (InvocationTargetException e) {
      Throwable cause = e.getCause();
      if (cause instanceof RuntimeException) {
        throw (RuntimeException) cause;
      }
      if (cause instanceof Error) {
        throw (Error) cause;
      }
      throw new RuntimeException("Unable to create binding instance.", cause);
    }
}

簡化一下也就是這樣segmentfault

private static Unbinder createBinding(Object target, View source) {
    Class<?> targetClass = target.getClass();
    Constructor<? extends Unbinder> constructor = findBindingConstructorForClass(targetClass);
    
    if (constructor == null) {
      return Unbinder.EMPTY;
    }
    return constructor.newInstance(target, source);
    // 異常處理 ...
}

Unbinder 是一個接口, 聲明以下緩存

public interface Unbinder {
  @UiThread void unbind();

  Unbinder EMPTY = new Unbinder() {
    @Override public void unbind() { }
  };
}

大概邏輯就是找到要綁定的那個類的構造函數, 而後利用反射創造出實例, 這裏的聲明是 Constructor<? extends Unbinder>, 因此返回的是一個 Unbinder 實例. 若是沒找到構造函數, 就返回一個 new 出來的 Unbinder.app

繼續點開 findBindingConstructorForClass框架

@Nullable @CheckResult @UiThread
private static Constructor<? extends Unbinder> findBindingConstructorForClass(Class<?> cls) {
    Constructor<? extends Unbinder> bindingCtor = BINDINGS.get(cls);
    if (bindingCtor != null) {
      if (debug) Log.d(TAG, "HIT: Cached in binding map.");
      return bindingCtor;
    }
    String clsName = cls.getName();
    if (clsName.startsWith("android.") || clsName.startsWith("java.")) {
      if (debug) Log.d(TAG, "MISS: Reached framework class. Abandoning search.");
      return null;
    }
    try {
      Class<?> bindingClass = cls.getClassLoader().loadClass(clsName + "_ViewBinding");
      //noinspection unchecked
      bindingCtor = (Constructor<? extends Unbinder>) bindingClass.getConstructor(cls, View.class);
      if (debug) Log.d(TAG, "HIT: Loaded binding class and constructor.");
    } catch (ClassNotFoundException e) {
      if (debug) Log.d(TAG, "Not found. Trying superclass " + cls.getSuperclass().getName());
      bindingCtor = findBindingConstructorForClass(cls.getSuperclass());
    } catch (NoSuchMethodException e) {
      throw new RuntimeException("Unable to find binding constructor for " + clsName, e);
    }
    BINDINGS.put(cls, bindingCtor);
    return bindingCtor;
  }

稍微簡化一下ide

private static Constructor<? extends Unbinder> findxxx(Class<?> cls) {
    // 首先在緩存中尋找
    Constructor<? extends Unbinder> bindingCtor = BINDINGS.get(cls);
    if (bindingCtor != null) { return bindingCtor; }
    
    // 緩存中沒有
    String clsName = cls.getName();
    // 是不是 android 源碼中的文件
    if (clsName.startsWith("android.") || clsName.startsWith("java.")) {
      if (debug) Log.d(TAG, "MISS: Reached framework class. Abandoning search.");
      return null;
    }
    try {
      Class<?> bindingClass = 
                    cls.getClassLoader().loadClass(clsName + "_ViewBinding");
      bindingCtor = (Constructor<? extends Unbinder>)
                    bindingClass.getConstructor(cls, View.class);
    } catch (ClassNotFoundException e) {
      // 繼續在父類中尋找
      bindingCtor = findBindingConstructorForClass(cls.getSuperclass());
    }
    // 異常處理
    
    // 放入緩存
    BINDINGS.put(cls, bindingCtor);
    return bindingCtor;
  }

BINDINGS 是一個 map, 聲明以下函數

static final Map<Class<?>, Constructor<? extends Unbinder>> BINDINGS = 
       new LinkedHashMap<>();

做用是緩存類對應的構造函數

因此 findBindingConstructorForClass 的主要過程是這樣的:

  1. 在緩存中尋找某個類的構造函數

    1. 有 -> 返回構造函數

    2. 沒有 -> 轉 2

  2. 是不是 android 源碼中的文件

    1. 是 -> 返回 null

    2. 沒有 -> 轉 3

  3. 取得某個類的構造器, 這個類的名字是 clsName + "_ViewBinding"

  4. 出異常則將參數換爲父類, 返回 1

  5. 將構造函數放入緩存

  6. 返回構造函數

這裏要注意的有 3 個:

  1. 這裏用 map 做爲緩存, 是爲了加快速度, 由於反射的效率是很低的, 而移動設備對性能的要求比較高

  2. 之因此要添加第 2 步, 是由於要在父類中尋找, 可能找到 android 源碼裏去.

  3. 這裏找了好久的構造函數, 並非傳入的類的, (啊這不是廢話嗎), 是 clsName_ViewBinding 這個類的. 在這裏的話應該叫 LoginActivity_ViewBinding.java

終於找到和 View 注入相關的東西了.如今有 1 個問題, 這是啥 ?
intellij idea 中, 使用 ctrl + shift + n 組合鍵, 能夠找到這個文件, 路徑以下: app\build\generated\source\apt\debug\...\LoginActivity_ViewBinding.java

public class LoginActivity_ViewBinding implements Unbinder {
  private LoginActivity target;
  private View view2131558546;

  @UiThread
  public LoginActivity_ViewBinding(LoginActivity target) {
    this(target, target.getWindow().getDecorView());
  }

  @UiThread
  public LoginActivity_ViewBinding(final LoginActivity target, View source) {
    this.target = target;

    View view;
    // findRequiredView 中利用 findViewById 找到 view
    view = Utils.findRequiredView(source, R.id.loginButton, "field 'button' and method 'login'");
    // castView 中利用 cls.cast(view); 將 view 強轉成 button
    target.button = Utils.castView(view, R.id.loginButton, "field 'button'", Button.class);
    view2131558546 = view;

    // other
  }

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

咱們如今來捋一捋

  1. 以前調用的 constructor.newInstance(target, source); 其實調用了 LoginActivity_ViewBinding(final LoginActivity target, View source), 返回的 Unbinder 實例也就是 LoginActivity_ViewBinding 實例, 因此一開始的 ButterKnife.bind(this); 方法的返回值就是這個實例, 這個實例只有一個方法 unbind(), 用於解除當前 activity 的綁定.

  2. 在構造方法中, 用 findViewById 找到對應的 view 並強轉成 button, 而後直接賦給 target.button, 因此用 Butter Knife 時, 用註解標註的內容不能是 private 的, 不然會拿不到這個成員變量.

因此 Butter Knife 實現依賴注入的方法就是額外生成一個類, 在類的構造函數中寫入咱們偷懶沒寫的 findViewById 等方法, 並添加一個解除綁定的方法.
在調用 ButterKnife.bind(xxx); 方法時會利用反射生成額外類的實例, 此時綁定便完成了.

到這裏就解釋完了是怎麼實現依賴注入的, 接下來研究這個類是怎麼生成的.

3. 輔助類的生成

1. 信息構建

既然是額外生成輔助類實現的, 那麼能夠確定是利用編譯時註解. 其實直接看 BindView 的定義, 會發現是 CLASS 級別的, 因此固然是用註解處理器處理的. 關於編譯時註解參見上一篇, 其中提到了接下來比較重要的 element.

github 上看 Butter Knife 的源碼, 能夠發現目錄結構以下

  • butterknife // 上面分析的代碼都在這裏

  • butterknife-annotations // 自定義註解

  • butterknife-compiler // 註解處理

  • butterknife-gradle-plugin // gradle 插件

  • butterknife-lint // lint 檢查

butterknife-compiler 包下有 ButterKnifeProcessor.java 文件, 這就是咱們要找的註解處理器了.

類的聲明以下 public final class ButterKnifeProcessor extends AbstractProcessor
咱們先看 init() 方法

private Elements elementUtils; // element 輔助類
private Types typeUtils; // 操做 TypeMirror 的輔助類
private Filer filer; // 文件操做輔助類
@Override public synchronized void init(ProcessingEnvironment env) {
    super.init(env);

    String sdk = env.getOptions().get(OPTION_SDK_INT);
    if (sdk != null) {
      try {
        this.sdk = Integer.parseInt(sdk);
      } catch (NumberFormatException e) {
        env.getMessager()
            .printMessage(Kind.WARNING, "Unable to parse supplied minSdk option '"
                + sdk
                + "'. Falling back to API 1 support.");
      }
    }

    elementUtils = env.getElementUtils();
    typeUtils = env.getTypeUtils();
    filer = env.getFiler();
    try {
      trees = Trees.instance(processingEnv);
    } catch (IllegalArgumentException ignored) {
    }
}

能夠看到主要是對一些輔助類進行了初始化.

接着看最重要的 process 方法

@Override public boolean process(Set<? extends TypeElement> elements, RoundEnvironment env) {
    // 解析每一個註解
    // TypeElement 表示類或接口級別的元素
    // BindingSet 是自定義的一個類, 表示全部須要綁定的元素的信息的集合
    Map<TypeElement, BindingSet> bindingMap = findAndParseTargets(env);

    for (Map.Entry<TypeElement, BindingSet> entry : bindingMap.entrySet()) {
      TypeElement typeElement = entry.getKey();
      BindingSet binding = entry.getValue();
      
      // 利用 javapoet 生成 java 文件, 也就是 xxx_ViewBinding.java
      JavaFile javaFile = binding.brewJava(sdk);
      try {
        javaFile.writeTo(filer);
      } catch (IOException e) {
        error(typeElement, "Unable to write binding for type %s: %s", typeElement, e.getMessage());
      }
    }
    return false;
}

這裏利用 javapoet 進行代碼生成, 這是同做者寫的另外一個開源的代碼生成框架.
這裏先無論這個, 往下看 findAndParseTargets 實現

private Map<TypeElement, BindingSet> findAndParseTargets(RoundEnvironment env) {
    // 這個 map 的 key 是 類或者接口 類型的 element, value 是對應的 builder, 用於存儲要生成的輔助類的一些信息
    Map<TypeElement, BindingSet.Builder> builderMap = new LinkedHashMap<>();
    Set<TypeElement> erasedTargetNames = new LinkedHashSet<>();
    scanForRClasses(env);
    
    // 其餘註解的處理    

    // 處理每一個用 BindView 註解標註的元素, 這裏的 Element 都是 Field 級別的
    for (Element element : env.getElementsAnnotatedWith(BindView.class)) {
      try {
        parseBindView(element, builderMap, erasedTargetNames);
      } catch (Exception e) {
        logParsingError(element, BindView.class, e);
      }
    }
    
    // Associate superclass binders with their subclass binders. This is a queue-based tree walk
    // which starts at the roots (superclasses) and walks to the leafs (subclasses).
    Deque<Map.Entry<TypeElement, BindingSet.Builder>> entries =
        new ArrayDeque<>(builderMap.entrySet());
    Map<TypeElement, BindingSet> bindingMap = new LinkedHashMap<>();
    while (!entries.isEmpty()) {
      Map.Entry<TypeElement, BindingSet.Builder> entry = entries.removeFirst();

      TypeElement type = entry.getKey();
      BindingSet.Builder builder = entry.getValue();

      TypeElement parentType = findParentType(type, erasedTargetNames);
      if (parentType == null) {
        bindingMap.put(type, builder.build());
      } else {
        BindingSet parentBinding = bindingMap.get(parentType);
        if (parentBinding != null) {
          builder.setParent(parentBinding);
          bindingMap.put(type, builder.build());
        } else {
          // Has a superclass binding but we haven't built it yet. Re-enqueue for later.
          entries.addLast(entry);
        }
      }
    }    

    return bindingMap;
}

主要邏輯寫在了 parseBindView 裏.

private void parseBindView(Element element, Map<TypeElement, BindingSet.Builder> builderMap, Set<TypeElement> erasedTargetNames) {
    // 獲得父元素, 這裏通常是 activity 類
    TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();

    // 檢查元素是否可達(private? public? 是否寫在類的成員變量裏?) 是否在錯誤的包裏 (安卓源碼 ?)
    boolean hasError = isInaccessibleViaGeneratedCode(BindView.class, "fields", element) || isBindingInWrongPackage(BindView.class, element);

    // 類型檢查這裏被我省略了

    if (hasError) {
      return;
    }

    // 獲得 view 的 id
    int id = element.getAnnotation(BindView.class).value();
    // 檢查緩存中是否有這個 activity
    BindingSet.Builder builder = builderMap.get(enclosingElement);
    // QualifiedId 就是帶上 activity 所在包名的信息的 id
    QualifiedId qualifiedId = elementToQualifiedId(element, id);
    // 檢查這個 id 是否已經被綁定過了
    if (builder != null) {
      String existingBindingName =
              builder.findExistingBindingName(getId(qualifiedId));
      // 若是不爲 null 說明已經被綁定過了, 直接返回
      if (existingBindingName != null) {
        error(element, "Attempt to use @%s for an already bound ID %d on '%s'. (%s.%s)", BindView.class.getSimpleName(), id, existingBindingName, enclosingElement.getQualifiedName(), element.getSimpleName());
        return;
      }
    } else {
      // 若是 builder 不存在, 就建立一個新的, 並放入 map 中
      builder = getOrCreateBindingBuilder(builderMap, enclosingElement);
    }
    
    String name = simpleName.toString();
    TypeName type = TypeName.get(elementType);
    boolean required = isFieldRequired(element);
    
    // 在該 builder 中添加新的 field 信息, 即關於綁定的信息
    builder.addField(getId(qualifiedId), new FieldViewBinding(name, type, required));

    // Add the type-erased version to the valid binding targets set.
    erasedTargetNames.add(enclosingElement);
  }

這裏比較重要的是 builder 的建立過程, 即 getOrCreateBindingBuilder

private BindingSet.Builder getOrCreateBindingBuilder(
      Map<TypeElement, BindingSet.Builder> builderMap, TypeElement enclosingElement) {
    BindingSet.Builder builder = builderMap.get(enclosingElement);
    if (builder == null) {
      // 傳入父元素, 即類信息建立 builder
      builder = BindingSet.newBuilder(enclosingElement);
      builderMap.put(enclosingElement, builder);
    }
    return builder;
}

其中 BindingSet.newBuilder 以下

static Builder newBuilder(TypeElement enclosingElement) {
    // 利用 typemirror 能夠得到元素的詳細類型信息
    TypeMirror typeMirror = enclosingElement.asType();

    boolean isView = isSubtypeOfType(typeMirror, VIEW_TYPE);
    boolean isActivity = isSubtypeOfType(typeMirror, ACTIVITY_TYPE);
    boolean isDialog = isSubtypeOfType(typeMirror, DIALOG_TYPE);

    TypeName targetType = TypeName.get(typeMirror);
    if (targetType instanceof ParameterizedTypeName) {
      targetType = ((ParameterizedTypeName) targetType).rawType;
    }
    
    String packageName = getPackage(enclosingElement).getQualifiedName().toString();
    String className = enclosingElement.getQualifiedName().toString().substring(
        packageName.length() + 1).replace('.', '$');
    // 這裏是 javapoet 的語法, 後一個參數是類名, 即咱們要找的 xxx_ViewBinding
    ClassName bindingClassName = ClassName.get(packageName, className + "_ViewBinding");

    boolean isFinal = enclosingElement.getModifiers().contains(Modifier.FINAL);
    return new Builder(targetType, bindingClassName, isFinal, isView, isActivity, isDialog);
}

綜上, 每處理一個 @BindView 註解標註的元素, 會經歷以下過程

  1. 獲得該元素的父元素, 通常爲 activity

  2. 合法性檢查

  3. 檢查 builderMap 中是否有該父元素對應的值 (builerMap 用於緩存, key 是類或者接口 類型的元素, value 是對應的 builder, builder 用於存儲要生成的輔助類的類名, 其中須要綁定的元素(如 field), 以及一些其餘信息 )

  4. 若是沒有, 新建相應的鍵值對, 這時 builder 包含一些類自己的信息, 如類名, 是不是 final, 是不是 activity 等等

  5. 若是有, 獲取 id, 並檢查 id 是否已被綁定, 被綁定則報錯

  6. 在得到的 builder 中添加該元素的相關信息, 該信息用於生成在 xxx_ViewBinding 的構造函數中的查找及賦值的相關代碼.

  7. builderMap 會通過一些處理(父類子類關係的調整等), 最後轉變成 BindingSet, 也就是咱們一開始看到的 Map<TypeElement, BindingSet> bindingMap

這裏完成了類信息的構建, 接下來還有最後一步, 代碼生成

2. 代碼生成

代碼生成是這句話: JavaFile javaFile = binding.brewJava(sdk);, 得到的 javaFile 包含了整個類的信息, 點進 brewJava 看看.

JavaFile brewJava(int sdk) {
    return JavaFile.builder(bindingClassName.packageName(), createType(sdk))
        .addFileComment("Generated code from Butter Knife. Do not modify!")
        .build();
}

這裏重要的是 createType

// 利用 bindingset 中的信息生成最終的輔助類
private TypeSpec createType(int sdk) {
    // 添加 public 修飾
    // TypeSpec 用於生成類
    TypeSpec.Builder result = TypeSpec.classBuilder(bindingClassName.simpleName())
        .addModifiers(PUBLIC);
    // 是不是 final
    if (isFinal) {
      result.addModifiers(FINAL);
    }
   
    if (parentBinding != null) {
      result.superclass(parentBinding.bindingClassName);
    } else {
      // 繼承 Unbinder 的代碼在這
      result.addSuperinterface(UNBINDER);
    }

    if (hasTargetField()) {
      result.addField(targetTypeName, "target", PRIVATE);
    }
    
    // 建立對應的構造函數
    if (isView) {
      result.addMethod(createBindingConstructorForView());
    } else if (isActivity) {
      result.addMethod(createBindingConstructorForActivity());
    } else if (isDialog) {
      result.addMethod(createBindingConstructorForDialog());
    }
    if (!constructorNeedsView()) {
      // Add a delegating constructor with a target type + view signature for reflective use.
      result.addMethod(createBindingViewDelegateConstructor());
    }
    result.addMethod(createBindingConstructor(sdk));

    if (hasViewBindings() || parentBinding == null) {
      result.addMethod(createBindingUnbindMethod(result));
    }

    return result.build();
}

這裏是構造函數的生成

private MethodSpec createBindingConstructor(int sdk) {
    MethodSpec.Builder constructor = MethodSpec.constructorBuilder()
        .addAnnotation(UI_THREAD)
        .addModifiers(PUBLIC);

    if (hasMethodBindings()) {
      constructor.addParameter(targetTypeName, "target", FINAL);
    } else {
      constructor.addParameter(targetTypeName, "target");
    }

    if (constructorNeedsView()) {
      constructor.addParameter(VIEW, "source");
    } else {
      constructor.addParameter(CONTEXT, "context");
    }

    if (hasUnqualifiedResourceBindings()) {
      // Aapt can change IDs out from underneath us, just suppress since all will work at runtime.
      constructor.addAnnotation(AnnotationSpec.builder(SuppressWarnings.class)
          .addMember("value", "$S", "ResourceType")
          .build());
    }

    if (hasOnTouchMethodBindings()) {
      constructor.addAnnotation(AnnotationSpec.builder(SUPPRESS_LINT)
          .addMember("value", "$S", "ClickableViewAccessibility")
          .build());
    }

    if (parentBinding != null) {
      if (parentBinding.constructorNeedsView()) {
        constructor.addStatement("super(target, source)");
      } else if (constructorNeedsView()) {
        constructor.addStatement("super(target, source.getContext())");
      } else {
        constructor.addStatement("super(target, context)");
      }
      constructor.addCode("\n");
    }
    if (hasTargetField()) {
      constructor.addStatement("this.target = target");
      constructor.addCode("\n");
    }

    if (hasViewBindings()) {
      if (hasViewLocal()) {
        // Local variable in which all views will be temporarily stored.
        constructor.addStatement("$T view", VIEW);
      }
      for (ViewBinding binding : viewBindings) {
        addViewBinding(constructor, binding);
      }
      for (FieldCollectionViewBinding binding : collectionBindings) {
        constructor.addStatement("$L", binding.render());
      }

      if (!resourceBindings.isEmpty()) {
        constructor.addCode("\n");
      }
    }

    if (!resourceBindings.isEmpty()) {
      if (constructorNeedsView()) {
        constructor.addStatement("$T context = source.getContext()", CONTEXT);
      }
      if (hasResourceBindingsNeedingResource(sdk)) {
        constructor.addStatement("$T res = context.getResources()", RESOURCES);
      }
      for (ResourceBinding binding : resourceBindings) {
        constructor.addStatement("$L", binding.render(sdk));
      }
    }

    return constructor.build();
}

由於生成的代碼都很相似, 這裏挑 addViewBinding(constructor, binding); 看一下

private void addViewBinding(MethodSpec.Builder result, ViewBinding binding) {
    if (binding.isSingleFieldBinding()) {
      // Optimize the common case where there's a single binding directly to a field.
      FieldViewBinding fieldBinding = binding.getFieldBinding();
      CodeBlock.Builder builder = CodeBlock.builder()
          .add("target.$L = ", fieldBinding.getName());

      boolean requiresCast = requiresCast(fieldBinding.getType());
      if (!requiresCast && !fieldBinding.isRequired()) {
        builder.add("source.findViewById($L)", binding.getId().code);
      } else {
        builder.add("$T.find", UTILS);
        builder.add(fieldBinding.isRequired() ? "RequiredView" : "OptionalView");
        if (requiresCast) {
          builder.add("AsType");
        }
        builder.add("(source, $L", binding.getId().code);
        if (fieldBinding.isRequired() || requiresCast) {
          builder.add(", $S", asHumanDescription(singletonList(fieldBinding)));
        }
        if (requiresCast) {
          builder.add(", $T.class", fieldBinding.getRawType());
        }
        builder.add(")");
      }
      result.addStatement("$L", builder.build());
      return;
    }

    List<MemberViewBinding> requiredBindings = binding.getRequiredBindings();
    if (requiredBindings.isEmpty()) {
      result.addStatement("view = source.findViewById($L)", binding.getId().code);
    } else if (!binding.isBoundToRoot()) {
      result.addStatement("view = $T.findRequiredView(source, $L, $S)", UTILS,
          binding.getId().code, asHumanDescription(requiredBindings));
    }

    addFieldBinding(result, binding);
    addMethodBindings(result, binding);
}

代碼很簡單, 能夠看到 view = $T.findRequiredView(source, $L, $S) 表明生成的代碼中的 view = Utils.findRequiredView(source, R.id.loginButton, "field 'button' and method 'login'");

代碼生成部分與 javapoet 關係比較強, 因此仍是要先了解 javapoet 才能比較深刻地瞭解, 這裏就不繼續分析了.

總結一下, Butter Knife 主要是利用編譯時註解和 javapoet 在編譯時動態生成輔助類, 在 bind 方法運行時運用反射建立輔助類的實例, 起到了語法糖的效果==.

相關文章
相關標籤/搜索