ButterKnife源码分析

Posted showCar

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了ButterKnife源码分析相关的知识,希望对你有一定的参考价值。

使用Butter Knife己经很长时间了,一直很喜欢用。因为它能帮我节省很多重复性的绑定控件代码。今天就来讲讲它的源码吧。只是总结我对它源码的一个理解。先看下它的使用效果:

public TestActivity extends Activity
  @Bind(R.id.top_pic)
    ImageView topPic;
    @Bind(R.id.top)
    TextView top;
      @Override
    public void onCreate(Bundle savedInstanceState) 
        super.onCreate(savedInstanceState);
        // TODO: add setContentView(...) invocation
        ButterKnife.bind(this);
    
    

如上它就会自动生成以下的代码。非常方便。

TextView toPic = (TextView)super.findViewById(R.id.top_pic);

Annotation 注解

对这个不了解的同学可以看下java编程思想中的相关内容,这里简单介绍下,Annotation包含几个注解符:@Retention, @Target, @Inherited, @Documented 。

  • @Documented 是否会保存到 Javadoc 文档中。
  • @Retention 保留时间,可选值 SOURCE(源码时),CLASS(编译时),RUNTIME(运行时),默认为CLASS,SOURCE大都为Mark Annotation,这类Annotation大都用来校验,比如Override, SuppressWarnings。
  • @Target 可以用来修饰哪些程序元素,如TYPE, METHOD, CONSTRUCTOR, FIELD, PARAMETER 等,未标注则表示可修饰所有。
  • @Inherited 是否可以被继承,默认为false。

重点关注下Retention中的参数的含义:

  • SOURCE: 源码级别解析,例如@Override, @SupportWarnings。这类注解在编译成功后就不会再起作用,并且不会出现在.class文件中。
  • RetentionPolicy.CLASS: 编译时解析,它解析出的代码会保留在最终的.class中,但是在运行时被忽略,所以无法在运行时获取。编译器在编译时自动查找所有继承自 AbstractProcessor 的类,然后调用他们的process方法去处理
  • RetentionPolicy.RUNTIME: 运行时解析,会保留在最终的.class文件中。这类注解可以用反射API中的getAnnotations()获取到。

讲完这些知识后,就开始我们的源码之旅吧。

源码解析

解析注解

首先我们添加了@Bind(R.id.top_pic)这样的注解,看下@Bind这个注解类:

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

明白了,CLASS,编译时解析。
上面说过编译时自动查找它会调用AbstractProcessor 类中的process方法去处理相关逻辑,这个注解处理什么事情呢?关键找到AbstractProcessor 类。找找找,找到ButterKnifeProcessor。看下它的process方法:

public final class ButterKnifeProcessor extends AbstractProcessor 
@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 
      JavaFileObject jfo = filer .createSourceFile(bindingClass.getFqcn() , typeElement);
      Writer writer = jfo.openWriter() ;
      writer.write(bindingClass.brewJava()) ;
      writer.flush() ;
      writer.close() ;
     catch (IOException e) 
      error(typeElement, "Unable to write view binder for type %s: %s" , typeElement,
          e.getMessage()) ;
    
  

  return true;


上面代码主要分两步。

  • findAndParseTargets解析注解。
  • bindingClass.brewJava()生成ViewBinder类。
    先看下findAndParseTargets具体的实现方法:
private Map<TypeElement, BindingClass> findAndParseTargets(RoundEnvironment env) 
  Map<TypeElement, BindingClass> targetClassMap = new LinkedHashMap<TypeElement, BindingClass>() ;
  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.
  for (Element element : env.getElementsAnnotatedWith( BindColor.class )) 
    try 
      parseResourceColor(element, targetClassMap, erasedTargetNames) ;
     catch (Exception e) 
      logParsingError(element, BindColor.class, e);
    
  

  // Process each @BindDimen element.
  for (Element element : env.getElementsAnnotatedWith( BindDimen.class )) 
    try 
      parseResourceDimen(element, targetClassMap, erasedTargetNames) ;
     catch (Exception e) 
      logParsingError(element, BindDimen.class, e);
    
  

  // Process each @BindDrawable element.
  for (Element element : env.getElementsAnnotatedWith( BindDrawable.class )) 
    try 
      parseResourceDrawable(element, targetClassMap, erasedTargetNames) ;
     catch (Exception e) 
      logParsingError(element, BindDrawable.class, e);
    
  

  // Process each @BindInt element.
  for (Element element : env.getElementsAnnotatedWith( BindInt.class )) 
    try 
      parseResourceInt(element, targetClassMap, erasedTargetNames) ;
     catch (Exception e) 
      logParsingError(element, BindInt.class, e);
    
  

  // Process each @BindString element.
  for (Element element : env.getElementsAnnotatedWith( BindString.class )) 
    try 
      parseResourceString(element, targetClassMap, erasedTargetNames) ;
     catch (Exception e) 
      logParsingError(element, BindString.class, e);
    
  

  // Try to find a parent binder for each.
  for (Map.Entry<TypeElement , BindingClass> entry : targetClassMap.entrySet()) 
    String parentClassFqcn = findParentFqcn(entry.getKey(), erasedTargetNames) ;
    if (parentClassFqcn != null) 
      entry.getValue().setParentViewBinder(parentClassFqcn + SUFFIX) ;
    
  

  return targetClassMap;

解析Binder注释,解析完信息存在targetClassMap中。上面代码有很多解析方式,如解析drawable,解析color等。这里只以parseBind为例进行分析。

private void parseBind(Element element, Map<TypeElement, BindingClass> targetClassMap,
    Set<String> erasedTargetNames) 
  // Verify common generated code restrictions.
  if (isInaccessibleViaGeneratedCode( Bind.class, "fields", element)
      || isBindingInWrongPackage(Bind .class, element)) 
    return;
  

  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)) 
    error(element, "@%s must be a List or array. (%s.%s)" , Bind. class.getSimpleName(),
        ((TypeElement) element.getEnclosingElement()).getQualifiedName() ,
        element.getSimpleName()) ;
   else 
    parseBindOne(element, targetClassMap, erasedTargetNames) ;
  

最终都会调用parseBindOne::

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

  // 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)" ,
        Bind .class.getSimpleName() , enclosingElement.getQualifiedName(), element.getSimpleName()) ;
    hasError = true;
  

  // Assemble information on the field.
  int [] ids = element.getAnnotation( Bind.class ).value();
  if (ids. length != 1 ) 
    error(element, "@%s for a view must only specify one ID. Found: %s. (%s.%s)" ,
        Bind .class.getSimpleName() , Arrays.toString (ids), enclosingElement.getQualifiedName() ,
        element.getSimpleName()) ;
    hasError = true;
  

  if (hasError) 
    return;
  

  int id = ids[0] ;
  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 binding = new FieldViewBinding(name, type, required) ;
  bindingClass.addField(id , binding); //保存这个bind,后面在生成类文件时候要用到!

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

所有应用ButterKnife的类都有一个单独的BindingClass与其挂钩,它里面包含了很多信息:Field绑定、Drawable绑定等等很多信息。以类名+”$$ViewBinder”包名标识。FieldViewBinding存放于BindingClass内,用于绑定变量View(与之对应还有绑定Drawable、Bitmap、List、Method等)。记录每个Field的绑定信息,如变量名、类名。每个FieldViewBinding都与一个id对应。注解生成就分析完了,接下来看看是如何生成findViewById之类的代码的。
解析完了,看第二步,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() ;

其实就是生成ViewBinder类,看下emitBindMethod方法:

private void emitBindMethod(StringBuilder builder) 
  builder.append("  @Override " )
      .append("public void bind(final Finder finder, final T target, Object source)  \\n") ;

  // Emit a call to the superclass binder, if any.
  if (parentViewBinder != null) 
    builder.append("    super.bind(finder, target, source); \\n\\n") ;
  

  if (!viewIdMap.isEmpty() || ! collectionBindings.isEmpty()) 
    // Local variable in which all views will be temporarily stored.
    builder.append( "    View view; \\n") ;

    // Loop over each view bindings and emit it.
    for (ViewBindings bindings : viewIdMap.values()) 
      emitViewBindings(builder, bindings);
    

    // Loop over each collection binding and emit it.
    for (Map.Entry<FieldCollectionViewBinding , int[]> entry : collectionBindings .entrySet()) 
      emitCollectionBinding(builder, entry.getKey(), entry.getValue()) ;
    
  

  if (!resourceBindings.isEmpty()) 
    builder.append("    Resources res = finder.getContext(source).getResources(); \\n") ;

    for (FieldResourceBinding binding : resourceBindings) 
      builder.append("    target." )
          .append(binding.getName())
          .append(" = res." )
          .append(binding.getMethod())
          .append('(' )
          .append(binding.getId())
          .append("); \\n") ;
    
  

  builder.append("   \\n") ;

利用上面解析保存的ViewBindings 来拼凑类。每次编译时ButterKnife都会针对你写的代码生成一个ViewBinder类,它里面有这样的代码:

public class MainActivity$$ViewBinder<T extends demo.lbb.test.MainActivity> implements ViewBinder<T> 
  @Override public void bind(final Finder finder, final T target, Object source) 
    View view;
    view = finder.findRequiredView(source, 2131492969, "field 'TextView' ");
    target.textview= finder.castView(view, 2131492969, "field 'imageview'");

解析生成ViewHolder完成了,接下来看如何动态生成我们要的那些代码。

进行绑定

肯定是从bind方法开始分析,来看下它的具体实现:

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

首先通过findViewBinderForClass来获得ViewHolder对象,然后调用ViewHolder的Binder来生成我们需要生成的代码。这里ViewHolder是什么鬼,我们先留个疑问在这。来看下findViewBinderForClass的实现:

private static ViewBinder<Object> findViewBinderForClass(Class<?> cls)
    throws IllegalAccessException, InstantiationException 
  ViewBinder<Object> viewBinder = BINDERS.get(cls) ;//查缓存。节省时间
  if (viewBinder != null) 
    if (debug) Log. d(TAG, "HIT: Cached in view binder map." );
    return viewBinder ;
  
  String clsName = cls.getName();
  if (clsName.startsWith( ANDROID_PREFIX) || clsName.startsWith(JAVA_PREFIX))  //android. 和java.
    if (debug) Log. d(TAG, "MISS: Reached framework class. Abandoning search." );
    return NOP_VIEW_BINDER;
  
  try 
    Class<?> viewBindingClass = Class.forName (clsName + ButterKnifeProcessor.SUFFIX) ; //SUFFIX = "$$ViewBinder" ;
    //noinspection unchecked
    viewBinder = (ViewBinder<Object>) viewBindingClass.newInstance() ;
    if ( debug) Log.d( TAG, "HIT: Loaded view binder class." );
   catch (ClassNotFoundException e) 
    if (debug) Log. d(TAG, "Not found. Trying superclass " + cls.getSuperclass().getName()) ;
    viewBinder = findViewBinderForClass(cls.getSuperclass()) ;
  
  BINDERS .put(cls, viewBinder) ;//进行缓存
  return viewBinder ;

通过BINDERS.get(cls)先判断缓存里是否存在,如果存在己经缓存的viewBinder对象就直接读取出来。不存在就Class.forName反射调用获取ViewBinder。findViewBinderForClass(cls.getSuperclass())用于在子类找不到继续找父类。
继续看 viewBinder.bind实现的功能。先看下它的参数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() ;
    
  ;

什么,一个枚举,什么return ((View) source).findViewById(id) ;不就是我们要生成的代码吗。到此我们可以猜测ButterKinfe根据传进来的context类型不同枚举生成不同的代码。是不是这样,往下看。上面说过我们生成了一个ViewHolder,ViewHolder.binder会调用以下代码:

view = finder.findRequiredView(source, 2131296337, "field 'toolbar'");
target.toolbar = finder.castView(view, 2131296337, "field 'toolbar'");

看下findRequiredView的具体实现:

public <T> T findRequiredView(Object source , int id, String who) 
  T view = findOptionalView(source , id, who);
  if (view == null) 
    String name = getContext(source).getResources().getResourceEntryName(id);
    throw new IllegalStateException( "Required view '"
        + name
        + "' with ID "
        + id
        + " for "
        + who
        + " was not found. If this view is optional add '@Nullable' annotation." );
  
  return view;

通过findOptionView获得一个View,继续看看findOptionView的实现:

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

看到findView,是不是就明白了,通过Finder.findView()来获得要生成的代码,比如如果是Activity就调用((Activity) source).findViewById(id) ,到此绑定成功。
这篇博客只是简单分析下生成控件绑定代码的源码,其实ButterKnife能做的不仅如此,它还可以生成触发事件代码等,是一个非常好用的工具。

以上是关于ButterKnife源码分析的主要内容,如果未能解决你的问题,请参考以下文章

ButterKnife编译时生成代码原理:butterknife-compiler源码分析

ButterKnife编译时生成代码原理:butterknife-compiler源码分析

Butterknife源码分析

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

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

分分钟带你读懂 ButterKnife 的源码