ButterKnife -- 源碼分析 -- 在‘編譯期’間生成findViewById等代碼

簡介

在之前簡單分析了xUtils的View模塊注入,其通過註解,在程序運行時去獲取註解的成員及方法,再通過反射及動態代理實現View的注入和監聽器的綁定。這些都是在運行過程中進行的,難免會影響程序的性能。

而今天要分析的ButterKnife也是通過註解實現View模塊的注入,但不同的是,它是在編譯期生成View注入的代碼,從而實現注入。也就是通過註解註釋將要注入的View和方法,在編譯期間生成findViewById(…)和setListener(…)的代碼,在編譯期間做註解處理,而程序運行時的性能消耗也就很小。

關於ButterKnife的使用就不多詳細介紹,可以直接看官方文檔

生成的代碼

我項目中使用ButterKnife的Activity:

public class MainActivity extends AppCompatActivity {

    @Bind(R.id.tv) TextView tv;
    @Bind(R.id.btn)Button btn;

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

    @OnClick(R.id.btn)
    public void onBtnClick(View view) {
        Toast.makeText(MainActivity.this, "Btn click", Toast.LENGTH_SHORT).show();
    }
}

生成的代碼:
位於項目目錄下DemoButterknife\app\build\intermediates\incremental-verifier\debug\com\yzw\demobutterknife

// Generated code from Butter Knife. Do not modify!
package com.yzw.demobutterknife;

import android.view.View;
import butterknife.ButterKnife.Finder;
import butterknife.ButterKnife.ViewBinder;

public class MainActivity$$ViewBinder<T extends com.yzw.demobutterknife.MainActivity> implements ViewBinder<T> {
  @Override 
  public void bind(final Finder finder, final T target, Object source) {
    View view;
    view = finder.findRequiredView(source, 2131492944, "field 'tv'");
    target.tv = finder.castView(view, 2131492944, "field 'tv'");

    view = finder.findRequiredView(source, 2131492945, "field 'btn' and method 'onBtnClick'");
    target.btn = finder.castView(view, 2131492945, "field 'btn'");
    view.setOnClickListener(new butterknife.internal.DebouncingOnClickListener() {
        @Override public void doClick(android.view.View p0) {
          target.onBtnClick(p0);
        }
      });
  }

  @Override 
  public void unbind(T target) {
    target.tv = null;
    target.btn = null;
  }
}

這裏先進行部分解釋:view = finder.findRequiredView(source, 2131492944, "field 'tv'")可以看到根據source即對應Activity或View等目標資源和控件id(來源於R文件中8進制轉換而來),找到對應id的控件,再進行強轉,這裏可以看到爲什麼要進行再次強轉,因爲在找到對應View的時候不知道器類型,所以也就需要通過直接賦值實現強轉,但是可以看到target.tv,target就是保存對應該控件的目標,從MainActivity$$ViewBinder<T extends com.yzw.demobutterknife.MainActivity>可以看出該target爲對應Activity,類似,也可以是對應View和Dialog。直接通過target.tv = ...來進行強制,這也爲什麼通過註解的成員變量不能被private修飾符修飾的原因,想想這可能是一個缺點,可能不符合平時寫代碼的規範,但是在平時注意下即可。

看下Bind註解,可以知道其生存期在編譯期間:

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

看下ViewBinder<T>接口

/** DO NOT USE: Exposed for generated code. */
  public interface ViewBinder<T> {
    void bind(Finder finder, T target, Object source);
    void unbind(T target);
  }

可以看到其生成MainActivity$$ViewBinder類來實現該接口,在bind(…)方法中生成findViewById(…)和setListener(…)的代碼,關於生產的代碼中個參數和變量所代表的意義可以見上面代碼。

看下Finder(只看主要代碼),是個枚舉類:

public enum Finder {
    VIEW {
      @Override protected View findView(Object source, int id) { return ((View) source).findViewById(id);}

      @Override public Context getContext(Object source) { return ((View) source).getContext(); } },
    ACTIVITY {
      @Override protected View findView(Object source, int id) { return ((Activity) source).findViewById(id); }

      @Override public Context getContext(Object source) {  return (Activity) source;} },
    DIALOG {
      @Override protected View findView(Object source, int id) { return ((Dialog) source).findViewById(id);}

      @Override public Context getContext(Object source) {  return ((Dialog) source).getContext(); } };

    /**
     * 相當於findViewById,其中source相當於Activity或者View或者Dialog
     */
    public <T> T findRequiredView(Object source, int id, String who) {
      T view = findOptionalView(source, id, who);
      if (view == null) {
        // id錯誤,拋出參數異常IllegalStateException
        // 代碼省略...
      }
      return view;
    }

    public <T> T findOptionalView(Object source, int id, String who) {
      View view = findView(source, id);
      return castView(view, id, who);
    }

    @SuppressWarnings("unchecked") // That's the point.
    public <T> T castView(View view, int id, String who) {
      try {
        return (T) view;
      } catch (ClassCastException e) {
        // 類型轉換錯誤,拋出參數異常IllegalStateException
        // 代碼省略...
      }
    }

    protected abstract View findView(Object source, int id);

    public abstract Context getContext(Object source);
  }

從上面可以看出ButterKnife在編譯期間生成MainActivity$$ViewBinder類,其中包括findViewById和setListenr等代碼,從而做到真正的簡化操作,性能開銷方面也不是很大

編譯期註解處理

看到這裏,你可能有疑問,ButterKnife是怎麼生成上面代碼的?

這裏主要通過ButterKnifeProcessor來實現,它繼承於AbstractProcessor,是編譯期間的註解處理器,功能非常強大。

主要看下ButterKnifeProcessorprocess(...)方法,該方法在編譯期間執行,可以獲取所有相關注解,並做處理,這裏便是獲取相關@Bind註解並生成代碼

@Override public boolean process(Set<? extends TypeElement> elements, RoundEnvironment env) {
    // K:TypeElement 代表等待注入的類元素
    // V:BindingClass 該類包含待注入元素的集合
    Map<TypeElement, BindingClass> targetClassMap = findAndParseTargets(env);

    // 循環
    for (Map.Entry<TypeElement, BindingClass> entry : targetClassMap.entrySet()) {
      TypeElement typeElement = entry.getKey();
      BindingClass bindingClass = entry.getValue();

      try {
        JavaFileObject jfo = filer.createSourceFile(bindingClass.getFqcn(), typeElement);
        Writer writer = jfo.openWriter();
        // 生成相應代碼
        writer.write(bindingClass.brewJava());
        writer.flush();
        writer.close();
      } catch (IOException e) {
        // 拋出異常...
      }
    }

看下findAndParseTargets方法,主要是找到所有註解,並整合成Map<TypeElement, BindingClass>類型

private Map<TypeElement, BindingClass> findAndParseTargets(RoundEnvironment env) {
    // K:TypeElement 代表等待注入的類元素
    // V:BindingClass 該類包含待注入元素的集合
    Map<TypeElement, BindingClass> targetClassMap = new LinkedHashMap<TypeElement, BindingClass>();
    // 存儲已經解析的TypeElement(只是標記)
    Set<String> erasedTargetNames = new LinkedHashSet<String>();

    // Process each @Bind element.
    for (Element element : env.getElementsAnnotatedWith(Bind.class)) {
      try {
        parseBind(element, targetClassMap, erasedTargetNames);
      } catch (Exception e) {
        logParsingError(element, Bind.class, e);
      }
    }

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

    // Process each @BindBool element.
    for (Element element : env.getElementsAnnotatedWith(BindBool.class)) {
      try {
        parseResourceBool(element, targetClassMap, erasedTargetNames);
      } catch (Exception e) {
        logParsingError(element, BindBool.class, e);
      }
    }

    // Process each @BindColor element.
    // ...

    // Process each @BindDimen element.
    // ...

    // Process each @BindDrawable element.
    // ...

    // Process each @BindInt element.
    // ...

    // Process each @BindString element.
    // ...

    // Try to find a parent binder for each.
    // ...

    return targetClassMap;
  }

可以看到findAndParseTargets方法解析各種註解,
先看下parseBind的主要邏輯

解析@Bind註解

private void parseBind(Element element, Map<TypeElement, BindingClass> targetClassMap,
      Set<String> erasedTargetNames) {
    // 參數判斷...

    TypeMirror elementType = element.asType();
    if (elementType.getKind() == TypeKind.ARRAY) {
      parseBindMany(element, targetClassMap, erasedTargetNames);
    } else if (LIST_TYPE.equals(doubleErasure(elementType))) {
      parseBindMany(element, targetClassMap, erasedTargetNames);
    } else if (isSubtypeOfType(elementType, ITERABLE_TYPE)) {
     // 打出異常信息...
    } else {
      parseBindOne(element, targetClassMap, erasedTargetNames);
    }
  }

可以看到parseBind解析ARRAYList和單個@Bind,這裏主要看下parseBindOne

      Set<String> erasedTargetNames) {

    // 獲取該元素element所在的類enclosingElement
    TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();

    // 參數檢查...

    // Assemble information on the field.
    int[] ids = element.getAnnotation(Bind.class).value();
    // 參數檢查...

    int id = ids[0];
    // 下面代碼邏輯:
    // 根據Map<K,V> targetClassMap來獲取對應的BindingClass
    // 再將帶有@Bind註解的元素信息添加進BindingClass
    BindingClass bindingClass = targetClassMap.get(enclosingElement);
    if (bindingClass != null) {
      ViewBindings viewBindings = bindingClass.getViewBinding(id);
      if (viewBindings != null) {
        Iterator<FieldViewBinding> iterator = viewBindings.getFieldBindings().iterator();
        if (iterator.hasNext()) {
          FieldViewBinding existingBinding = iterator.next();
          error(element, "Attempt to use @%s for an already bound ID %d on '%s'. (%s.%s)",
              Bind.class.getSimpleName(), id, existingBinding.getName(),
              enclosingElement.getQualifiedName(), element.getSimpleName());
          return;
        }
      }
    } else {
      bindingClass = getOrCreateTargetClass(targetClassMap, enclosingElement);
    }

    String name = element.getSimpleName().toString();
    String type = elementType.toString();
    boolean required = isRequiredBinding(element);

    // 創建FieldViewBinding代編一個View的注入,並添加到BindingClass中
    FieldViewBinding binding = new FieldViewBinding(name, type, required);
    bindingClass.addField(id, binding);

    // Add the type-erased version to the valid binding targets set.
    erasedTargetNames.add(enclosingElement.toString());
  }
private BindingClass getOrCreateTargetClass(Map<TypeElement, BindingClass> targetClassMap,
      TypeElement enclosingElement) {
    BindingClass bindingClass = targetClassMap.get(enclosingElement);
    if (bindingClass == null) {
      String targetType = enclosingElement.getQualifiedName().toString();
      String classPackage = getPackageName(enclosingElement);
      String className = getClassName(enclosingElement, classPackage) + SUFFIX;

      bindingClass = new BindingClass(classPackage, className, targetType);
      targetClassMap.put(enclosingElement, bindingClass);
    }
    return bindingClass;
  }

在這裏,看下構造BindingClass的方法getOrCreateTargetClass
結合上面分析,可以看到BindingClass代表的是生成的MainActivity$$ViewBinder類的信息

private BindingClass getOrCreateTargetClass(Map<TypeElement, BindingClass> targetClassMap,
      TypeElement enclosingElement) {
    BindingClass bindingClass = targetClassMap.get(enclosingElement);
    if (bindingClass == null) {
      String targetType = enclosingElement.getQualifiedName().toString();
      String classPackage = getPackageName(enclosingElement);、
      // SUFFIX = "$$ViewBinder"
      String className = getClassName(enclosingElement, classPackage) + SUFFIX;

      bindingClass = new BindingClass(classPackage, className, targetType);
      targetClassMap.put(enclosingElement, bindingClass);
    }
    return bindingClass;
  }

到這裏,先來理解一下其他類

從上面可以看到FieldViewBinding類代表綁定view的類型和字段名(如上面的tv和btn)

看下上面的BindingClass的addField

void addField(int id, FieldViewBinding binding) {
    getOrCreateViewBindings(id).addFieldBinding(binding);
}

BindingClass的getOrCreateViewBindings生成一個ViewBindings:其根據id代表View所綁定的相關信息,比如有監聽器方法,字段類型FieldViewBinding

private ViewBindings getOrCreateViewBindings(int id) {
    // 先查找是否會該id的ViewBingdings類,否則則創建
    ViewBindings viewId = viewIdMap.get(id);
    if (viewId == null) {
      viewId = new ViewBindings(id);
      viewIdMap.put(id, viewId);
    }
    return viewId;
  }

到這裏,小結一下BindingClass、ViewBinds、FieldViewBinding

  • BindingClass:代表的是生成的MainActivity$$ViewBinder類的信息
  • ViewBinds:代表一個控件(id)的相關信息,有該控件字段信息,監聽器方法
  • FieldViewBinding :代表某控件的類型和字段名

到這裏@Bind註解的解析大致瞭解了一下

解析監聽器註解

現在來看下關於監聽器的解析

 private void findAndParseListener(RoundEnvironment env,
      Class<? extends Annotation> annotationClass, Map<TypeElement, BindingClass> targetClassMap,
      Set<String> erasedTargetNames) {
      // 先通過env.getElementsAnnotatedWith(annotationClass)
      // 獲取所有該註解的元素(即方法),再調用parseListenerAnnotation解析
    for (Element element : env.getElementsAnnotatedWith(annotationClass)) {
      try {
        parseListenerAnnotation(annotationClass, element, targetClassMap, erasedTargetNames);
      } catch (Exception e) {
       // 輸出異常信息
      }
    }
  }

parseListenerAnnotation方法有200多行,所以只看重點:

private void parseListenerAnnotation(Class<? extends Annotation> annotationClass, Element element,
      Map<TypeElement, BindingClass> targetClassMap, Set<String> erasedTargetNames)
      throws Exception {

    // 根據註解獲取各種相關信息,以及各種類型檢查  

    MethodViewBinding binding = new MethodViewBinding(name, Arrays.asList(parameters), required);
    BindingClass bindingClass = getOrCreateTargetClass(targetClassMap, enclosingElement);
    for (int id : ids) {
      if (!bindingClass.addMethod(id, listener, method, binding)) {
          //輸出異常信息...
      }
    }

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

可以看到其根據註解生成一個MethodViewBinding,再加之添加到BindingClass中。相信從名字上大家也可以知道MethodViewBinding代表各空間監聽器的方法。這裏簡單瞭解一下:
new MethodViewBinding(name, Arrays.asList(parameters), required);第一個參數代表可監聽器相應的方法(爲字符串類型),第二個參數爲該相應方法待傳入的參數,第三個參數爲是否執行(默認爲true)

解析資源註解

根據上面View跟監聽器的綁定,同理,關於資源的綁定的思路也是一樣,這裏只分析布爾資源的獲取:

private void parseResourceBool(Element element, Map<TypeElement, BindingClass> targetClassMap,
      Set<String> erasedTargetNames) {
    boolean hasError = false;
    TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();

    // 信息換取,類型檢查...

    BindingClass bindingClass = getOrCreateTargetClass(targetClassMap, enclosingElement);
    FieldResourceBinding binding = new FieldResourceBinding(id, name, "getBoolean");
    bindingClass.addResource(binding);

    erasedTargetNames.add(enclosingElement.toString());
  }

可以看到關於資源信息的類FieldResourceBinding,其封裝了資源id、字段名和相應獲取資源方法"getBoolean"

代碼的生成

看到應該知道Map<TypeElement, BindingClass> targetClassMap = findAndParseTargets(env);Map集合代表什麼了吧。

  • TypeElement:代表使用了ButterKnife註解的元素信息,可以把它理解成一個Activity或者View
  • BindingClass:代表該元素(Activity或者View)中,註解的使用信息,如上面分析的View注入信息,監聽器信息,資源信息等。

那麼有了這裏信息,怎麼生成源代碼呢:

JavaFileObject jfo = filer.createSourceFile(bindingClass.getFqcn(), typeElement);
        Writer writer = jfo.openWriter();
        writer.write(bindingClass.brewJava());

可以看到,通過filer.createSourceFile創建一個文件(即 MainActivity$$ViewBinde相關文件),再通過bindingClass.brewJava()生成代碼片段並寫入

看下brewJava()

String brewJava() {
    StringBuilder builder = new StringBuilder();
    builder.append("// Generated code from Butter Knife. Do not modify!\n");
    builder.append("package ").append(classPackage).append(";\n\n");

    if (!resourceBindings.isEmpty()) {
      builder.append("import android.content.res.Resources;\n");
    }
    if (!viewIdMap.isEmpty() || !collectionBindings.isEmpty()) {
      builder.append("import android.view.View;\n");
    }
    builder.append("import butterknife.ButterKnife.Finder;\n");
    if (parentViewBinder == null) {
      builder.append("import butterknife.ButterKnife.ViewBinder;\n");
    }
    builder.append('\n');

    builder.append("public class ").append(className);
    builder.append("<T extends ").append(targetClass).append(">");

    if (parentViewBinder != null) {
      builder.append(" extends ").append(parentViewBinder).append("<T>");
    } else {
      builder.append(" implements ViewBinder<T>");
    }
    builder.append(" {\n");

    emitBindMethod(builder);
    builder.append('\n');
    emitUnbindMethod(builder);

    builder.append("}\n");
    return builder.toString();
  }

可以看到跟前面的MainActivity$$ViewBinde格式一模一樣,emitBindMethod(builder)方法則生成對應的Bind方法
emitUnbindMethod(builder);方法則生成對應的unBind方法

現在各種註解信息都是BindingClass中,就是隻是生成findViewById和setListener方法了

程序運行時的調用

ButterKnife的使用是在Activity的onCreate()方法中調用ButterKnife.bind(this);

其最終會調用下面方法:

static void bind(Object target, Object source, Finder finder) {
    Class<?> targetClass = target.getClass();
    try {
      if (debug) Log.d(TAG, "Looking up view binder for " + targetClass.getName());
      // 找到對應的ViewBinder,即MainActivity$$ViewBinder,
      // 隨後調用其bind方法來執行findViewById和setListerner
      ViewBinder<Object> viewBinder = findViewBinderForClass(targetClass);
      if (viewBinder != null) {
        viewBinder.bind(finder, target, source);
      }
    } catch (Exception e) {
      throw new RuntimeException("Unable to bind views for " + targetClass.getName(), e);
    }
  }

可以看到程序最終在bind(...)方法調用了findViewById和setListener方法

總結

在理解ButterKnife之前我是抵制使用註解來注入的,但是理解了ButterKnife後,我發現到了註解的魅力所在。

到這裏,ButterKnife源碼分析告一段落,相信你可以看到ButterKnife發揮註解的強大之處,能夠將煩躁的findViewById等代碼在編譯期間生成出來,提高了開發效率,何樂而不爲,但是使用時不能關會用,要知道原理,這也是很重要的。

最後,關於本文,如有不足之處或者錯誤的地方,歡迎指出,謝謝。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章