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和艾达pter等中采纳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实行分析,也就进来了parseBindViewJava,其一法子中:

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的信息生成一个Field(Field)ViewBinding,最终添加到下边生成的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的。

写了一点个钟头终于写完了。假使有荒唐欢迎指正(๑>؂<๑)

相关文章