ButterKnife源碼分析

前言

在N久之前,自從實驗室里面的學長推薦我用butterknife后, 從此的項目再也離不開butterknife了,然而自以為對它很熟時,前不久今日頭條實習生招聘二面卻被面試官洗刷了一頓。然后整個二面完全是被虐的感覺,估計最后會掛,哎!
當時被問到butterknife的實現,懵逼的我想都不想就答上了注解加反射。然而面試官卻一臉疑問的問我:你確定?除了反射還有其他方法么????
我了個去,難道butterknife不是用的反射??難道還有其他方法來實現這玩意兒么?不行,面試完了趕快clone 源碼下來看看。不看不知道,一看嚇一跳,原來還真不是用的注解加反射。在此感謝面試官為我開啟了新世界的大門,原來注解還能這么用!

Butterknife用法

我相信學過android開發應該基本上都用過Butterknife吧,就算沒用過也聽說過吧?畢竟是大名鼎鼎的Jake Wharton出品的東西。要是沒用過的話可以看看這里,里面雖然是講的Annotation,但是例子就是用注解加反射實現的低級的Butterknife。哈哈!用法里面大概也說了下。

Butterknife原理

講到butterknife的原理。這里不得不提一下一般這種注入框架都是運行時注解,即聲明注解的生命周期為RUNTIME,然后在運行的時候通過反射完成注入,這種方式雖然簡單,但是這種方式多多少少會有性能的損耗。那么有沒有一種方法能解決這種性能的損耗呢? 沒錯,答案肯定是有的,那就是Butterknife用的APT(Annotation Processing Tool)編譯時解析技術。

APT大概就是你聲明的注解的生命周期為CLASS,然后繼承AbstractProcessor類。繼承這個類后,在編譯的時候,編譯器會掃描所有帶有你要處理的注解的類,然后再調用AbstractProcessor的process方法,對注解進行處理,那么我們就可以在處理的時候,動態生成綁定事件或者控件的java代碼,然后在運行的時候,直接調用bind方法完成綁定。
其實這種方式的好處是我們不用再一遍一遍地寫findViewById和onClick了,這個框架在編譯的時候幫我們自動生成了這些代碼,然后在運行的時候調用就行了。

源碼解析

上面講了那么多,其實都不如直接解析源碼來得直接,下面我們就一步一步來探究大神怎樣實現Butterknife的吧。

拿到源碼的第一步是從我們調用的地方來突破,那我們就來看看程序里面是怎樣調用它的呢?

 @Override protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.simple_activity);
    ButterKnife.setDebug(true);
    ButterKnife.bind(this);

// Contrived code to use the bound fields.
title.setText("Butter Knife");
subtitle.setText("Field and method binding for Android views.");
footer.setText("by Jake Wharton");
hello.setText("Say Hello");

adapter = new SimpleAdapter(this);
listOfThings.setAdapter(adapter);

}</code></pre>

上面是github上給的例子,我們直接就從 ButterKnife.bind(this)入手吧,點進來看看:

  public static Unbinder bind(@NonNull Activity target) {
    return bind(target, target, Finder.ACTIVITY);
  }

咦?我再點:

  static Unbinder bind(@NonNull Object target, @NonNull Object source, @NonNull Finder finder) {
    Class<?> targetClass = target.getClass();
    try {
      ViewBinder<Object> viewBinder = findViewBinderForClass(targetClass);
      return viewBinder.bind(finder, target, source);
    } catch (Exception e) {
      throw new RuntimeException("Unable to bind views for " + targetClass.getName(), e);
    }
  }

好吧,bind方法主要就是拿到我們綁定的Activity的Class,然后找到這個Class的ViewBinder,最后調用ViewBinder的bind()方法,那么問題來了,ViewBinder是個什么鬼???我們打開
findViewBinderForClass()方法。

 @NonNull
  private static ViewBinder<Object> findViewBinderForClass(Class<?> cls)
      throws IllegalAccessException, InstantiationException {
    ViewBinder<Object> viewBinder = BINDERS.get(cls);
    if (viewBinder != null) {
      return viewBinder;
    }
    String clsName = cls.getName();
    try {
      Class<?> viewBindingClass = Class.forName(clsName + "$$ViewBinder");
      viewBinder = (ViewBinder<Object>) viewBindingClass.newInstance();
    } catch (ClassNotFoundException e) {
      viewBinder = findViewBinderForClass(cls.getSuperclass());
    }
    BINDERS.put(cls, viewBinder);
    return viewBinder;
  }

這里我去掉了一些Log信息,保留了關鍵代碼,上面的BINDERS是一個保存了Class為key,Class$$ViewBinder為Value的一個LinkedHashMap,主要是做一下緩存,提高下次再來bind的性能。
在第10行的時候,clsName 是我們傳入要綁定的Activity類名,這里相當于拿到了Activity$$ViewBinder這個東西,這個類又是什么玩意兒?其實從類名可以看出來,相當于Activity的一個內部類,這時候我們就要問了,我們在用的時候沒有聲明這個類啊???從哪里來的? 不要方,其實它就是我們在之前講原理的時候說到的AbstractProcessor在編譯的時候生成的一個類,我們后面再來看它,現在我們繼續往下面分析。在第11行就用反射反射了一個viewBinder 實例出來。
剛剛說了,這個方法里面用linkhashMap做了下緩存,所以在15行的時候,就把剛剛反射的viewBinder作為value,Class作為key加入這個LinkedHashMap,下次再bind這個類的時候,就直接在第4行的時候取出來用,提升性能。

現在返回剛剛的bind方法,我們拿到了這個Activity的viewBinder,然后調用它的bind方法。咦?這就完了???我們再點進viewBinder的bind方法看看。

public interface ViewBinder<T> {
  Unbinder bind(Finder finder, T target, Object source);
}

什么,接口???什么鬼?剛剛不是new了一個viewBinder出來么?然后這里就調用了這個viewBinder的bind方法, 不行,我要看一下bind到底是什么鬼!上面說了,Butterknife用了APT技術,那么這里的viewBinder應該就是編譯的時候生成的,那么我們就反編譯下apk。看看到底生成了什么代碼:
下面我們就先用一個簡單的綁定TextView的例子,然后反編譯出來看看:

public class MainActivity extends AppCompatActivity {

@Bind(R.id.text_view)
TextView textView;

@OnClick(R.id.text_view)
 void onClick(View view) {
    textView.setText("我被click了");
}

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    ButterKnife.bind(this);
    textView.setText("我還沒有被click");
}

}</code></pre>

源代碼就這行幾行,然后反編譯看看:


源代碼就多了一個類,MainActivity$$ViewBinder,打開看看:

public class MainActivity$$ViewBinder<T extends MainActivity>
  implements ButterKnife.ViewBinder<T>
{
  public void bind(ButterKnife.Finder paramFinder, final T paramT, Object paramObject)
  {
    View localView = (View)paramFinder.findRequiredView(paramObject, 2131492944, "field 'textView' and method 'onClick'");
    paramT.textView = ((TextView)paramFinder.castView(localView, 2131492944, "field 'textView'"));
    localView.setOnClickListener(new DebouncingOnClickListener()
    {
      public void doClick(View paramAnonymousView)
      {
        paramT.onClick(paramAnonymousView);
      }
    });
  }

public void unbind(T paramT) { paramT.textView = null; } }</code></pre>

還記得剛剛說的,反射了一個Class$$ViewBinder么?看這里的類名。現在應該懂了吧?它剛好也是實現了ButterKnife.ViewBinder<T>接口,我們說了,在bind方法中,最后調用了ViewBinder的bind方法,先說下幾個參數paramFinder其實就是一個Finder,因為我們可以在Activity中使用butterknife,也可以在Fragment和Adapter等中使用butterknife,那么在不同的地方使用butterknife,這個Finder也就不同。在Activity中,其實源碼 就是這樣子的:

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

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

}</code></pre>

有沒有很熟悉???其實還是用的findViewById,那么在Dialog和Fragment中,根據不同的地方,實現的方式不同。

這里的paramT和paramObject都是我們要綁定的Activity類,通過代碼可以跟蹤到。

返回上面的ViewBinder代碼,首先調用了Finder的findRequiredView方法,其實這個方法最后經過處理就是調用了findView方法,拿到相應的view,然后再賦值給paramT.textView,剛說了paramT就是那個要綁定的Activity,現在懂了吧?這里通過 paramT.textView 這樣的調用方式,說明了Activity中不能把TextView設置為private,不然會報錯,其實這里可以用反射來拿到textView的,這里大概也是為了性能著想吧。最后setOnClickListener,DebouncingOnClickListener這個Listener其實也是實現了View.OnClickListener 方法,然后在OnClick里面調用了doClick方法。流程大概跟蹤了一遍。現在還留下最后一塊了:

Butterknife到底是怎樣在編譯的時候生成代碼的?

我們來看一下它的ButterKnifeProcessor類:

Init方法:

  @Override public synchronized void init(ProcessingEnvironment env) {
    super.init(env);

elementUtils = env.getElementUtils();
typeUtils = env.getTypeUtils();
filer = env.getFiler();

}</code></pre>

ProcessingEnviroment參數提供很多有用的工具類Elements, Types和Filer。Types是用來處理TypeMirror的工具類,Filer用來創建生成輔助文件。至于ElementUtils嘛,其實ButterKnifeProcessor在運行的時候,會掃描所有的Java源文件,然后每一個Java源文件的每一個部分都是一個Element,比如一個包、類或者方法。

 @Override public Set<String> getSupportedAnnotationTypes() {
    Set<String> types = new LinkedHashSet<>();

types.add(BindArray.class.getCanonicalName());
types.add(BindBitmap.class.getCanonicalName());
types.add(BindBool.class.getCanonicalName());
types.add(BindColor.class.getCanonicalName());
types.add(BindDimen.class.getCanonicalName());
types.add(BindDrawable.class.getCanonicalName());
types.add(BindInt.class.getCanonicalName());
types.add(BindString.class.getCanonicalName());
types.add(BindView.class.getCanonicalName());
types.add(BindViews.class.getCanonicalName());

for (Class<? extends Annotation> listener : LISTENERS) {
  types.add(listener.getCanonicalName());
}

return types;

}</code></pre>

getSupportedAnnotationTypes()方法主要是指定ButterknifeProcessor是注冊給哪些注解的。我們可以看到,在源代碼里面,作者一個一個地把Class文件加到那個LinkedHashSet里面,然后再把LISTENERS也全部加進去。

其實整個類最重要的是process方法:

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

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

  try {
    bindingClass.brewJava().writeTo(filer);
  } catch (IOException e) {
    error(typeElement, "Unable to write view binder for type %s: %s", typeElement,
        e.getMessage());
  }
}
return true;

}</code></pre>

這個方法的作用主要是掃描、評估和處理我們程序中的注解,然后生成Java文件。也就是前面說的ViewBinder。首先一進這個函數就調用了findAndParseTargets方法,我們就去看看findAndParseTargets方法到底做了什么:

  private Map<TypeElement, BindingClass> findAndParseTargets(RoundEnvironment env) {
    Map<TypeElement, BindingClass> targetClassMap = new LinkedHashMap<>();
    Set<TypeElement> erasedTargetNames = new LinkedHashSet<>();

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

Observable.from(topLevelClasses)
    .flatMap(new Func1<BindingClass, Observable<?>>() {
      @Override public Observable<?> call(BindingClass topLevelClass) {
        if (topLevelClass.hasViewBindings()) {
          // It has an unbinder class and it will also be the highest unbinder class for all
          // descendants.
          topLevelClass.setHighestUnbinderClassName(topLevelClass.getUnbinderClassName());
        } else {
          // No unbinder class, so null it out so we know we can just return the NOP unbinder.
          topLevelClass.setUnbinderClassName(null);
        }

        // Recursively set up parent unbinding relationships on all its descendants.
        return ButterKnifeProcessor.this.setParentUnbindingRelationships(
            topLevelClass.getDescendants());
      }
    })
    .toCompletable()
    .await();

return targetClassMap;

}</code></pre>

這里代碼炒雞多,我就不全部貼出來了,只貼出來一部分,這個方法最后還用了rxjava的樣子。這個方法的主要的流程如下:

  • 掃描所有具有注解的類,然后根據這些類的信息生成BindingClass,最后生成以TypeElement為鍵,BindingClass為值的鍵值對。
  • 循環遍歷這個鍵值對,根據TypeElement和BindingClass里面的信息生成對應的java類。例如AnnotationActivity生成的類即為Cliass$$ViewBinder類。

因為我們之前用的例子是綁定的一個View,所以我們就只貼了解析View的代碼。好吧,這里遍歷了所有帶有@BindView的Element,然后對每一個Element進行解析,也就進入了parseBindView這個方法中:

private void parseBindView(Element element, Map<TypeElement, BindingClass> targetClassMap,
      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();
}
if (!isSubtypeOfType(elementType, VIEW_TYPE) && !isInterface(elementType)) {
  error(element, "@%s fields must extend from View or be an interface. (%s.%s)",
      BindView.class.getSimpleName(), enclosingElement.getQualifiedName(),
      element.getSimpleName());
  hasError = true;
}

if (hasError) {
  return;
}

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

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)",
          BindView.class.getSimpleName(), id, existingBinding.getName(),
          enclosingElement.getQualifiedName(), element.getSimpleName());
      return;
    }
  }
} else {
  bindingClass = getOrCreateTargetClass(targetClassMap, enclosingElement);
}

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

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);

}</code></pre>

然后這里從一進入這個方法到

  int id = element.getAnnotation(BindView.class).value();

都是在拿到注解信息,然后驗證注解的target的類型是否繼承自view,然后上面這一行代碼獲得我們要綁定的View的id,再從targetClassMap里面取出BindingClass(這個BindingClass是管理了所有關于這個注解的一些信息還有實例本身的信息,其實最后是通過BindingClass來生成java代碼的),如果targetClassMap里面不存在的話,就在

      bindingClass = getOrCreateTargetClass(targetClassMap, enclosingElement);

這里生成一個,我們進去看一下getOrCreateTargetClass

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);
      boolean isFinal = enclosingElement.getModifiers().contains(Modifier.FINAL);
      String className = getClassName(enclosingElement, classPackage) + BINDING_CLASS_SUFFIX;
      String classFqcn = getFqcn(enclosingElement) + BINDING_CLASS_SUFFIX;

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

}</code></pre>

這里面其實很簡單,就是獲取一些這個注解所修飾的變量的一些信息,比如類名呀,包名呀,然后className這里就賦值成Class$$ViewHolder了,因為:

  private static final String BINDING_CLASS_SUFFIX = "$$ViewBinder";

然后把這個解析后的bindingClass加入到targetClassMap里面。

返回剛剛的parseBindView中,根據view的信息生成一個FieldViewBinding,最后添加到上邊生成的BindingClass實例中。這里基本完成了解析工作。最后回到findAndParseTargets中:

Observable.from(topLevelClasses)
        .flatMap(new Func1<BindingClass, Observable<?>>() {
          @Override public Observable<?> call(BindingClass topLevelClass) {
            if (topLevelClass.hasViewBindings()) {
              topLevelClass.setHighestUnbinderClassName(topLevelClass.getUnbinderClassName());
            } else {

          topLevelClass.setUnbinderClassName(null);
        }
        return ButterKnifeProcessor.this.setParentUnbindingRelationships(
            topLevelClass.getDescendants());
      }
    })
    .toCompletable()
    .await();</code></pre> 

這里用到了rxjava,其實這里主要的工作是建立上面的綁定的所有的實例的解綁的關系,因為我們綁定了,最后在代碼中還是會解綁的。這里預先處理好了這些關系。因為這里要遞歸地完成解綁,所以用了flatmap,flatmap把每一個創建出來的 Observable 發送的事件,都集中到同一個 Observable 中,然后這個 Observable 負責將這些事件統一交給 Subscriber 。
然而這部分涉及到很多rxjava的東西,有興趣的童鞋去看看大神的寫給android開發者的RxJava 詳解這篇文章,然后再來看這里就很輕松了。

回到我們的process中, 現在解析完了annotation,該生成java文件了,我再把代碼貼一下:

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

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

      try {
        bindingClass.brewJava().writeTo(filer);
      } catch (IOException e) {
        error(typeElement, "Unable to write view binder for type %s: %s", typeElement,
            e.getMessage());
      }
    }

    return true;
  }

遍歷剛剛得到的targetClassMap ,然后再一個一個地通過

bindingClass.brewJava().writeTo(filer);

來生成java文件。然而生成的java文件也是根據上面的信息來用字符串拼接起來的,然而這個工作在brewJava()中完成了:

  JavaFile brewJava() {
    TypeSpec.Builder result = TypeSpec.classBuilder(className)
        .addModifiers(PUBLIC)
        .addTypeVariable(TypeVariableName.get("T", ClassName.bestGuess(targetClass)));
    if (isFinal) {
      result.addModifiers(Modifier.FINAL);
    }

    if (hasParentBinding()) {
      result.superclass(ParameterizedTypeName.get(ClassName.bestGuess(parentBinding.classFqcn),
          TypeVariableName.get("T")));
    } else {
      result.addSuperinterface(ParameterizedTypeName.get(VIEW_BINDER, TypeVariableName.get("T")));
    }

    result.addMethod(createBindMethod());

    if (hasUnbinder() && hasViewBindings()) {
      // Create unbinding class.
      result.addType(createUnbinderClass());

      if (!isFinal) {
        // Now we need to provide child classes to access and override unbinder implementations.
        createUnbinderCreateUnbinderMethod(result);
      }
    }

    return JavaFile.builder(classPackage, result.build())
        .addFileComment("Generated code from Butter Knife. Do not modify!")
        .build();
  }

這里用到了java中的javapoet技術,不了解的童鞋可以傳送到github上面,也是square的杰作,這個不在這篇文章的講解范圍內,有興趣的童鞋可以去看看,很不錯的開源項目。

最后通過writeTo(Filer filer)生成java源文件。

寫了好幾個小時終于寫完了,最后希望剛面完HR面的幾個公司不要再掛我了,不然就找不到實習工作了啊啊啊!!!


 

文/尸情化異(簡書作者)
原文鏈接:http://www.jianshu.com/p/0f3f4f7ca505
著作權歸作者所有,轉載請聯系作者獲得授權,并標注“簡書作者”。

 本文由用戶 ManMayhew 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!