ButterKnife -- 源码分析 -- 在‘编译期’间生成findViewById等代码

Posted Y_ZhiWen

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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等代码在编译期间生成出来,提高了开发效率,何乐而不为,但是使用时不能关会用,要知道原理,这也是很重要的。

最后,关于本文,如有不足之处或者错误的地方,欢迎指出,谢谢。

以上是关于ButterKnife -- 源码分析 -- 在‘编译期’间生成findViewById等代码的主要内容,如果未能解决你的问题,请参考以下文章

Butterknife源码分析

ButterKnife源码分析

ButterKnife源码分析

ButterKnife -- 源码分析 -- 在‘编译期’间生成findViewById等代码

ButterKnife -- 源码分析 -- 在‘编译期’间生成findViewById等代码

自己简易打造的IOC注解框架:SteadyoungIOC