ButterKnife第三方库源码分析

   2016-11-10 0
核心提示:ButterKnife原理其实也很简单ButterKnife是大名鼎鼎JakeWharton热门开源项目的其中一个,让开发者不再重复的进行findViewById的操作。配合android studio的插件,一键自动生成xml文件所有view的实例。ButterKnife为什么会那么神奇,自动帮助开发者省去了繁琐

ButterKnife原理其实也很简单

ButterKnife是大名鼎鼎JakeWharton热门开源项目的其中一个,让开发者不再重复的进行findViewById的操作。

配合android studio的插件,一键自动生成xml文件所有view的实例。

ButterKnife为什么会那么神奇,自动帮助开发者省去了繁琐的操作,他的实现的原理到底是怎么样的呢?

下面我们从代码使用上,一步一步的分析ButterKnife的实现原理

分析ButterKnife版本:com.jakewharton:butterknife:7.0.1

  1. 使用ButterKnife快速初始化xml布局对象.


public class MainActivity extends AppCompatActivity {

  @Bind(R.id.tv01) TextView tv01;
  @Bind(R.id.tv02) TextView tv02;
  @Bind(R.id.tv03) TextView tv03;
  @Bind(R.id.tv04) TextView tv04;
  @Bind(R.id.activity_main) LinearLayout activityMain;

  @OnClick(R.id.tv01)
  public void test(View v) {

  }

  @Override protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    ButterKnife.bind(this);
  }
  
}

一段很常规使用ButterKnife快速初始化xml布局对象的代码。为什么当onCreate方法的ButterKnife.bind(this);

调用完毕,xml所有的布局对象都初始化好了呢?

我们进入到@Bind注解里看看究竟,看看是否找得到线索


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

可以看到@Retention(CLASS),这句表示:保留时间 编译时,也就是工程编译时运行的注解.

常规获取View对象的方式是这样的:

tv01 = (TextView) findViewById(R.id.tv01);


//现在变成这样
@Bind(R.id.tv01) TextView tv01;

所以说,ButterKnife这个库应该拿到了R.id.tv01 这个id值,也拿到了tv01成员变量,在通过findViewById给tv01赋值

但是,ButterKnife在哪里进行这样的操作呢?

从@Bind注解来看,应该是编译时拿到了id值

我们在build目录下找到了ButterKnife生成的新文件:

build\generated\source\apt\debug\com\butterknifedemo\MainActivity$$ViewBinder



// Generated code from Butter Knife. Do not modify!
package com.butterknifedemo;

import android.view.View;
import butterknife.ButterKnife.Finder;
import butterknife.ButterKnife.ViewBinder;

public class MainActivity$$ViewBinder<T extends com.butterknifedemo.MainActivity> implements ViewBinder<T> {
  @Override public void bind(final Finder finder, final T target, Object source) {
    View view;
    view = finder.findRequiredView(source, 2131427413, "field 'tv01' and method 'test'");
    target.tv01 = finder.castView(view, 2131427413, "field 'tv01'");
    view.setOnClickListener(
      new butterknife.internal.DebouncingOnClickListener() {
        @Override public void doClick(
          android.view.View p0
        ) {
          target.test(p0);
        }
      });
    view = finder.findRequiredView(source, 2131427414, "field 'tv02'");
    target.tv02 = finder.castView(view, 2131427414, "field 'tv02'");
    view = finder.findRequiredView(source, 2131427415, "field 'tv03'");
    target.tv03 = finder.castView(view, 2131427415, "field 'tv03'");
    view = finder.findRequiredView(source, 2131427416, "field 'tv04'");
    target.tv04 = finder.castView(view, 2131427416, "field 'tv04'");
    view = finder.findRequiredView(source, 2131427412, "field 'activityMain'");
    target.activityMain = finder.castView(view, 2131427412, "field 'activityMain'");
  }

  @Override public void unbind(T target) {
    target.tv01 = null;
    target.tv02 = null;
    target.tv03 = null;
    target.tv04 = null;
    target.activityMain = null;
  }
}

我们发现ButterKnife在build目录下生成了一个类,这个类竟然帮助我们完成了findVieweById的操作

那这个类是怎么制作出来的呢?

现在,我们直接去看ButterKnife源码:

	

//先进入里面看看
ButterKnife.bind(this);


 //显然Activity对象作为target往下传递了
 //Finder.ACTIVITY 是什么呢
 public static void bind(Activity target) {
   bind(target, target, Finder.ACTIVITY);
 }


//Finder.ACTIVITY 原来是 ButterKnife 内部枚举
//return ((Activity) source).findViewById(id); 注意看句代码
public final class ButterKnife {
  private ButterKnife() {
    throw new Assertion
Error("No instances.");
  }

  /** DO NOT USE: Exposed for generated code. */
  @SuppressWarnings("UnusedDeclaration") // Used by generated code.
  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();
      }
    };



 //findViewBinderForClass这个方法通过Activity对象去查找返回了一个ViewBinder类,
 //然后viewBinder.bind(finder, target, source);
 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);
   }
 }

这里有两个问题:

  1. findViewBinderForClass通过Activity字节码如何找到的viewBinder?
  2. viewBinder是什么?

首先,看viewBinder是什么:


/** DO NOT USE: Exposed for generated code. */
public interface ViewBinder<T> {
void bind(Finder finder, T target, Object source);
void unbind(T target);
}

原来是个接口,注释说自动生成代码用的,看看刚刚在build找到的类:


public class MainActivity$$ViewBinder
<T extends com.butterknifedemo.MainActivity> implements ViewBinder<T> {

  @Override public void bind(final Finder finder, final T target, Object source) {

原来viewBinder.bind(finder, target, source);这行代码调用了MainActivity$$ViewBinder类里的bind方法,

帮助我们完成findViewById工作

现在我们知道了,平时我们调用ButterKnife.bind(this);

最终都会调用对应生成的$$ViewBinder类里的bind方法帮助我们完成繁琐的操作

问题2:怎么通过Activity字节码找到viewBinder对象的?

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)) {
    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);里面取,空的话在通过Class.forName(clsName + ButterKnifeProcessor.SUFFIX);

创建出一个新对象出来,前提是这个$$ViewBinder已经生成好了

什么时候生成这个类,怎么生成的?通过注解@Bind我们应该猜到,是工程编译时就生成好了的

在ButterKnife源码中我们发现了这一个类:


public final class ButterKnifeProcessor extends AbstractProcessor {
  public static final String SUFFIX = "$$ViewBinder";
  public static final String ANDROID_PREFIX = "android.";
  public static final String JAVA_PREFIX = "java.";
  static final String VIEW_TYPE = "android.view.View";
  private static final String COLOR_STATE_LIST_TYPE = "android.content.res.ColorStateList";
  private static final String DRAWABLE_TYPE = "android.graphics.drawable.Drawable";
  private static final String NULLABLE_ANNOTATION_NAME = "Nullable";
  private static final String ITERABLE_TYPE = "java.lang.Iterable<?>";
  private static final String LIST_TYPE = List.class.getCanonicalName();
  private static final List<Class<? extends Annotation>> LISTENERS = Arrays.asList(//
      OnCheckedChanged.class, //
      OnClick.class, //
      OnEditorAction.class, //
      OnFocusChange.class, //
      OnItemClick.class, //
      OnItemLongClick.class, //
      OnItemSelected.class, //
      OnLongClick.class, //
      OnPageChange.class, //
      OnTextChanged.class, //
      OnTouch.class //
  );


 @Override public Set<String> getSupportedAnnotationTypes() {
   Set<String> types = new LinkedHashSet<String>();
	
   types.add(Bind.class.getCanonicalName());
	
   for (Class<? extends Annotation> listener : LISTENERS) {
     types.add(listener.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());
	
   return types;
 }

extends AbstractProcessor,继承这一个类,表示它可以在工程编译是运行里面的process方法,

ButterKnife就是通过编译时,apt会自动查找集成AbstractProcessor的类,调用process方法

在process方法中找到存在ButterKnife的注解信息,获取在注解对应下的数据,例如id值

上面的代码我们还可以看到,Bind,OnClick等等注解已经存储好了,就等着遍历配对处理获取数据

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

从JavaFileObject,Writer这个类就可以知道,ButterKnife把一些东西写到文件中去了,应该猜到

那些自动生成的java文件就从这里出来的

代码生成java文件,代码加载java文件去运行,有点意思

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

....

可以看到,正在查找Bind之类的注解,猜都猜到通过定位注解获取注解下面的值了吧

现在,我们回过头看看把什么东西写到文件中去了,看着行代码: writer.write(bindingClass.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();
}

可以一目了然的看到使用了StringBuilder手动拼接字符串的方式,生成了java文件,挺不容易的。

问题又来了,这些java文件静态不变化的部分可以写死,那些动态灵活的部分呢?例如View的id,对象名称

继续深入看源码:


//bind方法代码拼接
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);
  }







  private void emitViewBindings(StringBuilder builder, ViewBindings bindings) {
    builder.append("    view = ");

    List<ViewBinding> requiredViewBindings = bindings.getRequiredBindings();
    if (requiredViewBindings.isEmpty()) {
      builder.append("finder.findOptionalView(source, ")
          .append(bindings.getId())//这个就是View的id了
          .append(", null);\n");
    } else {
      if (bindings.getId() == View.NO_ID) {
        builder.append("target;\n");
      } else {
        builder.append("finder.findRequiredView(source, ")
            .append(bindings.getId())
            .append(", \"");
        emitHumanDescription(builder, requiredViewBindings);
        builder.append("\");\n");
      }
    }

	//字段看这里,进去
    emitFieldBindings(builder, bindings);
    emitMethodBindings(builder, bindings);
  }



static void emitHumanDescription(StringBuilder builder,
	  Collection<? extends ViewBinding> bindings) {
	Iterator<? extends ViewBinding> iterator = bindings.iterator();
	switch (bindings.size()) {
	  case 1:
	    builder.append(iterator.next().getDescription());//View变量名称
	    break;
	  case 2:
	    builder.append(iterator.next().getDescription())



@Override public String getDescription() {
    return "field '" + name + "'";
  }

从上面的代码可以知道,动态的部分通过BindingClass这个类来获取的,那这是类怎么来的,看之前的代码:

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

原来这个类是从findAndParseTargets里来的,刚刚我们知道,里面做了定位Bind,OnClick注解的操作,

定位的同时也把注解的值,例如id值,变量名称存在到BindingClass对象中了,很符合面向对象的思想


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


	...


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

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

最后,还有一点的是:ButterKnife通过注解获取id值并没有使用到反射,获取到变量也是通过Activity.view的形式

不同与一些反射获取注解的框架,使用反射会增加IO操作,增加了时间操作,多了会变得卡顿

反射的方式成员变量可使用private,而ButterKnife不可以,必须public或者protected

因为ButterKnife没有使用反射,需要Activity.view这样去获取一些对象赋值

具体可以去看源码,总的来说:

  1. ButterKnife将View的id值放到@Bind注解中
  2. ButterKnife通过extends AbstractProcessor编译时自动调用process方法来定位和存在注解与注解上的id值
  3. 找到所有带注解与值的对象,存储在集合中,一个for循环一顿狂写,把java文件写到build目录下
  4. 当调用ButterKnife.bind(this)的时候,最终会调用生成的$$viewBinder类里的bind方法
  5. $$viewBinder里的bind方法,找已动态生成好了finfViewById的过程,通过Activity.view的形式初始化所有view

分析就到这里了

11/9/2016 11:33:29 PM

 
反对 0举报 0 评论 0
 

免责声明:本文仅代表作者个人观点,与乐学笔记(本网)无关。其原创性以及文中陈述文字和内容未经本站证实,对本文以及其中全部或者部分内容、文字的真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。
    本网站有部分内容均转载自其它媒体,转载目的在于传递更多信息,并不代表本网赞同其观点和对其真实性负责,若因作品内容、知识产权、版权和其他问题,请及时提供相关证明等材料并与我们留言联系,本网站将在规定时间内给予删除等相关处理.

  • Butterknife的使用
    Butterknife的使用
    介绍如何使用Jake大神的butterknife。。。About ButterknifeButterKnife是一个Android View注入的库,使用这个库我们可以不用写很多无聊的findViewById()和setOnClickListener()等代码。 项目的主页在这里: http://jakewharton.github.io/butterknife/ 如何
    10-13 ButterKnife
点击排行