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

地点是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");
    }
}

源代码就那行几行,然后反编译看看:

源代码就多了一个类,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;
  }
}

还记得刚刚说的,反射了一个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;
    }
  }

有没有很熟悉???其实仍然用的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();
  }

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

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

本条点子的功效至关重假设扫描、评估和拍卖大家先后中的注明,然后生成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;
  }

此处代码炒鸡多,我就不全敬重出来了,只贴出来一部分,那个办法最后还用了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);
  }

接下来那里从一进入那一个主意到

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

那里面其实很简单,就是获取一些那个表明所修饰的变量的有些音信,比如类名呀,包名呀,然后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();

这边运用了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源文件。

即使对javapoet感兴趣的话,可以看看那篇小说,介绍javapoet的。

写了几许个时辰终于写完了。如若有不当欢迎指正(๑>؂<๑)