1. 程式人生 > >Android Butterknife 框架原始碼解析(3)——Butterknife 8.7.0原始碼分析

Android Butterknife 框架原始碼解析(3)——Butterknife 8.7.0原始碼分析

前文連結地址:

《Android Butterknife 框架原始碼解析(1)——ButterKnife的使用》 http://blog.csdn.net/bit_kaki/article/details/74926076

《Android Butterknife 框架原始碼解析(2)——談談Java的註解》 http://blog.csdn.net/bit_kaki/article/details/75039597

前兩篇分別說了下ButterKnife的用法以及講了下什麼是註解,接下來就是要說一下ButterKnife的原始碼了,通過原始碼來看ButterKnife是如何實現的。本章的ButterKnife基於目前最新的8.7.0.。ButterKnife的github地址為https://github.com/JakeWharton/butterknife

      我們繼續使用第一篇中ButterKnife的使用例子,其程式碼為:

public class MainActivity extends AppCompatActivity {
    @BindView(R.id.button)
    Button button;
    @BindView(R.id.textView)
    TextView textView;

    @OnClick(R.id.button)
    public void click(){
        textView.setText("我被點選了一下");
    }

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

      這段程式碼很簡單,就是執行了兩個Button和一點Button點選事件的繫結。我們就以這段程式碼為例,來看看Butterknife的原始碼和執行的原理。

註解的原始碼分析

  首先我們看看@BindView註解的原始碼:

@Retention(CLASS) @Target(FIELD)
public @interface BindView {
  /** View ID to which the field will be bound. */
  @IdRes int value();
}

   還有@OnClick的原始碼:

@Target(METHOD)
@Retention(CLASS)
@ListenerClass(
    targetType = "android.view.View",
    setter = "setOnClickListener",
    type = "butterknife.internal.DebouncingOnClickListener",
    method = @ListenerMethod(
        name = "doClick",
        parameters = "android.view.View"
    )
)
public @interface OnClick {
  /** View IDs to which the method will be bound. */
  @IdRes int[] value() default { View.NO_ID };
}

   通過上一篇註解的學習,我們可以看到這兩個註解的結構,但是我們也看到了它們的@Retention的值是CLASS,也就是說它們存活時間在編譯器裡,當程式執行時候jvm將會拋棄他們。上一篇我們舉的通過註解實現@BindView的例子是在程式執行時,通過反射找到被註解的控制元件,實現了繫結的方法,很明顯在ButterKnife裡它並不是這樣實現的。接下來我們繼續看程式碼。

ButterKnife的原始碼分析

  我們也知道註解也是一個標記,並不實際執行內容,於是看看ButterKnife的使用裡,真正執行的是ButterKnife.bind(this)這個方法。點開ButterKnife類,我們可以看到它的原始碼是:

/**
 * BindView annotated fields and methods in the specified {@link Activity}. The current content
 * view is used as the view root.
 *
 * @param target Target activity for view binding.
 */
@NonNull @UiThread
public static Unbinder bind(@NonNull Activity target) {
  View sourceView = target.getWindow().getDecorView();
  return createBinding(target, sourceView);
}
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);
  }
}
@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;
}

      這兩個是繫結的具體方法,看著有點亂,於是我們把log日誌和特殊情況判定去掉看就是:

Class<?> targetClass = target.getClass();
Constructor<? extends Unbinder> bindingCtor = BINDINGS.get(targetClass);String clsName = targetClass.getName();Class<?> bindingClass = targetClass.getClassLoader().loadClass(clsName + "_ViewBinding");bindingCtor = (Constructor<? extends Unbinder>) bindingClass.getConstructor(targetClass, View.class);BINDINGS.put(cls, bindingCtor);return bindingCtor;

      可以看到其實就是通過反射去查詢獲取一個名稱為傳入類的類名後加上“_ViewBinder”的類,並獲取對應的ViewBinder。舉例子,我bind的是MainActivity,那麼對應的ViewBinder就是MainAcitivity$$ViewBinder。然後將這個類的例項放入到一個map裡以供獲取。

ButterKnifeProcessor的原始碼分析

      然後呢,好像程式碼裡就沒啥了。但是這個className+“_ViewBinder”是怎麼來的呢。這裡需要說明下,對於註釋在編譯時候解析是通過我們自定義一個繼承 AbstractProcessor的類來完成的。所以我們找找ButterKnife的原始碼,在butterknife-compiler module裡找到了該類,如圖所示:


      接下來看一下該類的核心程式碼:

@Override public boolean process(Set<? extends TypeElement> elements, RoundEnvironment env) {
  Map<TypeElement, BindingSet> bindingMap = findAndParseTargets(env);

  for (Map.Entry<TypeElement, BindingSet> entry : bindingMap.entrySet()) {
    TypeElement typeElement = entry.getKey();
    BindingSet binding = entry.getValue();
    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;
}

         可以看出這個類主要就是做了兩件事,首先是通過findAndParseTargets方法找出該類裡所有註解,然後通過binding.brewJava方法將這些註解進行處理。

      首先來看看findAndParseTargets方法:

private Map<TypeElement, BindingSet> findAndParseTargets(RoundEnvironment env) {
  Map<TypeElement, BindingSet.Builder> builderMap = new LinkedHashMap<>();
  Set<TypeElement> erasedTargetNames = new LinkedHashSet<>();

  scanForRClasses(env);

  // Process each @BindAnim element.
  for (Element element : env.getElementsAnnotatedWith(BindAnim.class)) {
    if (!SuperficialValidation.validateElement(element)) continue;
    try {
      parseResourceAnimation(element, builderMap, erasedTargetNames);
    } catch (Exception e) {
      logParsingError(element, BindAnim.class, e);
    }
  }

  // Process each @BindArray element.
  for (Element element : env.getElementsAnnotatedWith(BindArray.class)) {
    if (!SuperficialValidation.validateElement(element)) continue;
    try {
      parseResourceArray(element, builderMap, erasedTargetNames);
    } catch (Exception e) {
      logParsingError(element, BindArray.class, e);
    }
  }

  // Process each @BindBitmap element.
  for (Element element : env.getElementsAnnotatedWith(BindBitmap.class)) {
    if (!SuperficialValidation.validateElement(element)) continue;
    try {
      parseResourceBitmap(element, builderMap, erasedTargetNames);
    } catch (Exception e) {
      logParsingError(element, BindBitmap.class, e);
    }
  }

  // Process each @BindBool element.
  for (Element element : env.getElementsAnnotatedWith(BindBool.class)) {
    if (!SuperficialValidation.validateElement(element)) continue;
    try {
      parseResourceBool(element, builderMap, erasedTargetNames);
    } catch (Exception e) {
      logParsingError(element, BindBool.class, e);
    }
  }

  // Process each @BindColor element.
  for (Element element : env.getElementsAnnotatedWith(BindColor.class)) {
    if (!SuperficialValidation.validateElement(element)) continue;
    try {
      parseResourceColor(element, builderMap, erasedTargetNames);
    } catch (Exception e) {
      logParsingError(element, BindColor.class, e);
    }
  }

  // Process each @BindDimen element.
  for (Element element : env.getElementsAnnotatedWith(BindDimen.class)) {
    if (!SuperficialValidation.validateElement(element)) continue;
    try {
      parseResourceDimen(element, builderMap, erasedTargetNames);
    } catch (Exception e) {
      logParsingError(element, BindDimen.class, e);
    }
  }

  // Process each @BindDrawable element.
  for (Element element : env.getElementsAnnotatedWith(BindDrawable.class)) {
    if (!SuperficialValidation.validateElement(element)) continue;
    try {
      parseResourceDrawable(element, builderMap, erasedTargetNames);
    } catch (Exception e) {
      logParsingError(element, BindDrawable.class, e);
    }
  }

  // Process each @BindFloat element.
  for (Element element : env.getElementsAnnotatedWith(BindFloat.class)) {
    if (!SuperficialValidation.validateElement(element)) continue;
    try {
      parseResourceFloat(element, builderMap, erasedTargetNames);
    } catch (Exception e) {
      logParsingError(element, BindFloat.class, e);
    }
  }

  // Process each @BindFont element.
  for (Element element : env.getElementsAnnotatedWith(BindFont.class)) {
    if (!SuperficialValidation.validateElement(element)) continue;
    try {
      parseResourceFont(element, builderMap, erasedTargetNames);
    } catch (Exception e) {
      logParsingError(element, BindFont.class, e);
    }
  }

  // Process each @BindInt element.
  for (Element element : env.getElementsAnnotatedWith(BindInt.class)) {
    if (!SuperficialValidation.validateElement(element)) continue;
    try {
      parseResourceInt(element, builderMap, erasedTargetNames);
    } catch (Exception e) {
      logParsingError(element, BindInt.class, e);
    }
  }

  // Process each @BindString element.
  for (Element element : env.getElementsAnnotatedWith(BindString.class)) {
    if (!SuperficialValidation.validateElement(element)) continue;
    try {
      parseResourceString(element, builderMap, erasedTargetNames);
    } catch (Exception e) {
      logParsingError(element, BindString.class, e);
    }
  }

  // Process each @BindView element.
  for (Element element : env.getElementsAnnotatedWith(BindView.class)) {
    // we don't SuperficialValidation.validateElement(element)
    // so that an unresolved View type can be generated by later processing rounds
    try {
      parseBindView(element, builderMap, erasedTargetNames);
    } catch (Exception e) {
      logParsingError(element, BindView.class, e);
    }
  }

  // Process each @BindViews element.
  for (Element element : env.getElementsAnnotatedWith(BindViews.class)) {
    // we don't SuperficialValidation.validateElement(element)
    // so that an unresolved View type can be generated by later processing rounds
    try {
      parseBindViews(element, builderMap, erasedTargetNames);
    } catch (Exception e) {
      logParsingError(element, BindViews.class, e);
    }
  }

  // Process each annotation that corresponds to a listener.
  for (Class<? extends Annotation> listener : LISTENERS) {
    findAndParseListener(env, listener, builderMap, erasedTargetNames);
  }

  // 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;
}

      程式碼挺長的,其實就是將該類裡所有的註解按照不同的註解型別做出對應的處理,然後分類儲存起來。以我們用到的BindView為例子,其對應的程式碼是:

for (Element element : env.getElementsAnnotatedWith(BindView.class)) {
  // we don't SuperficialValidation.validateElement(element)
  // so that an unresolved View type can be generated by later processing rounds
  try {
    parseBindView(element, builderMap, erasedTargetNames);
  } catch (Exception e) {
    logParsingError(element, BindView.class, e);
  }
}

      然後看看parseBindView方法:

private void parseBindView(Element element, Map<TypeElement, BindingSet.Builder> builderMap,
    Set<TypeElement> erasedTargetNames) {
  TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();

  // Start by verifying common generated code restrictions.
  boolean hasError = isInaccessibleViaGeneratedCode(BindView.class, "fields", element)
      || isBindingInWrongPackage(BindView.class, element);

  // Verify that the target type extends from View.
  TypeMirror elementType = element.asType();
  if (elementType.getKind() == TypeKind.TYPEVAR) {
    TypeVariable typeVariable = (TypeVariable) elementType;
    elementType = typeVariable.getUpperBound();
  }
  Name qualifiedName = enclosingElement.getQualifiedName();
  Name simpleName = element.getSimpleName();
  if (!isSubtypeOfType(elementType, VIEW_TYPE) && !isInterface(elementType)) {
    if (elementType.getKind() == TypeKind.ERROR) {
      note(element, "@%s field with unresolved type (%s) "
              + "must elsewhere be generated as a View or interface. (%s.%s)",
          BindView.class.getSimpleName(), elementType, qualifiedName, simpleName);
    } else {
      error(element, "@%s fields must extend from View or be an interface. (%s.%s)",
          BindView.class.getSimpleName(), qualifiedName, simpleName);
      hasError = true;
    }
  }

  if (hasError) {
    return;
  }

  // Assemble information on the field.
  int id = element.getAnnotation(BindView.class).value();

  BindingSet.Builder builder = builderMap.get(enclosingElement);
  QualifiedId qualifiedId = elementToQualifiedId(element, id);
  if (builder != null) {
    String existingBindingName = builder.findExistingBindingName(getId(qualifiedId));
    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 = getOrCreateBindingBuilder(builderMap, enclosingElement);
  }

  String name = simpleName.toString();
  TypeName type = TypeName.get(elementType);
  boolean required = isFieldRequired(element);

  builder.addField(getId(qualifiedId), new FieldViewBinding(name, type, required));

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

      我們依然去掉一些特殊情況的判斷看看主要程式碼為:

private void parseBindView(Element element, Map<TypeElement, BindingSet.Builder> builderMap,
    Set<TypeElement> erasedTargetNames) {
  TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();
  TypeMirror elementType = element.asType();
  
  Name qualifiedName = enclosingElement.getQualifiedName();
  Name simpleName = element.getSimpleName();
    
  int id = element.getAnnotation(BindView.class).value();

  BindingSet.Builder builder = builderMap.get(enclosingElement);
  QualifiedId qualifiedId = elementToQualifiedId(element, id);
  builder = getOrCreateBindingBuilder(builderMap, enclosingElement);

  String name = simpleName.toString();
  TypeName type = TypeName.get(elementType);
  boolean required = isFieldRequired(element);

  builder.addField(getId(qualifiedId), new FieldViewBinding(name, type, required));
  // Add the type-erased version to the valid binding targets set.
  erasedTargetNames.add(enclosingElement);
}

       可以看出這段程式碼的核心就是建立一個BindingSet抽象類,然後將我們註解資訊處理後儲存在裡面。

     findAndParseTargets()方法的分析到此結束,簡單來說就是遍歷該類裡所有的註解,並進行分類預處理儲存。

     接下來是binding.brewJava()方法了,binding就是剛剛findAndParseTargets()方法所獲取的BindingSet,我們看看接下來的程式碼是什麼:

JavaFile brewJava(int sdk) {
  return JavaFile.builder(bindingClassName.packageName(), createType(sdk))
      .addFileComment("Generated code from Butter Knife. Do not modify!")
      .build();
}
private TypeSpec createType(int sdk) {
  TypeSpec.Builder result = TypeSpec.classBuilder(bindingClassName.simpleName())
      .addModifiers(PUBLIC);
  if (isFinal) {
    result.addModifiers(FINAL);
  }

  if (parentBinding != null) {
    result.superclass(parentBinding.bindingClassName);
  } else {
    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();
}

可以看出這一段的程式碼採用了JavaPoet,一個非常強大的程式碼生成工具。根據我們的註解內容,通過TypeSpec類和MethodSpec類構造出對應的方法,然後根據之前建立BindingSet抽象類時候建立的新類名:

ClassName bindingClassName = ClassName.get(packageName, className + "_ViewBinding");

      通過JavaFile創建出新的類。而這個新生成的className+_ViewBinding“”類,我們可以在ButterKnife.bind時候獲取到它,然後就通過這個類裡的方法,實現我們控制元件和監聽方法的綁定了。


新生成的_ViewBinder類

      好了,原始碼分析完了,那讓我們來看看這個新生成類的內容吧。我們先執行下程式,然後可以看到我們生成的MainActivity_ViewBinding類:


      點開看看它的程式碼為:

public class MainActivity_ViewBinding implements Unbinder {
  private MainActivity target;

  private View view2131427415;

  @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.button, "field 'button' and method 'click'");
    target.button = Utils.castView(view, R.id.button, "field 'button'", Button.class);
    view2131427415 = view;
    view.setOnClickListener(new DebouncingOnClickListener() {
      @Override
      public void doClick(View p0) {
        target.click();
      }
    });
    target.textView = Utils.findRequiredViewAsType(source, R.id.textView, "field 'textView'", TextView.class);
  }

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

    target.button = null;
    target.textView = null;

    view2131427415.setOnClickListener(null);
    view2131427415 = null;
  }
}

      其中這個@UiThread是相當於AsyncTask的onPostExecute方法,而裡面的控制元件都是通過內部方法實現findViewById。

總結:

      通過三篇部落格,我們對ButterKnife的原始碼實現進行了具體的分析,可以得到以下一些結論:

  1. ButterKnife使用上是通過註解來實現控制元件和響應事件的繫結,讓程式碼的閱讀性更高,書寫更加方便;
  2. ButterKnife是通過在編譯時候生成對應的className_ViewBinder類來輔助實現className類的控制元件和響應事件的繫結,所有繫結相關方法都在className_ViewBinder類裡實現;
  3. ButterKnife是程式執行到ButterKnife.bind(this)的方法時候執行的,先通過反射獲取對應的className_ViewBinder類,然後實現控制元件和響應事件的繫結;
  4. ButterKnife不能將控制元件和方法設定為private或者static,是因為在className_ViewBinder類會直接呼叫該控制元件和方法進行賦值。
參考文章:      http://blog.csdn.net/Charon_Chui/article/details/51908109
      http://blog.csdn.net/f2006116/article/details/51921288
      http://www.jianshu.com/p/da288e9ee3eb