ButterKnife第三方库源码分析

maohude 8年前
   <p>ButterKnife原理其实也很简单</p>    <p>ButterKnife是大名鼎鼎JakeWharton热门开源项目的其中一个,让开发者不再重复的进行findViewById的操作。</p>    <p>配合android studio的插件,一键自动生成xml文件所有view的实例。</p>    <p>ButterKnife为什么会那么神奇,自动帮助开发者省去了繁琐的操作,他的实现的原理到底是怎么样的呢?</p>    <p>下面我们从代码使用上,一步一步的分析ButterKnife的实现原理</p>    <p>分析ButterKnife版本:com.jakewharton:butterknife:7.0.1</p>    <ol>     <li>使用ButterKnife快速初始化xml布局对象.</li>    </ol>    <pre>  <code class="language-java">    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);    }      }  </code></pre>    <p>一段很常规使用ButterKnife快速初始化xml布局对象的代码。为什么当onCreate方法的ButterKnife.bind(this);</p>    <p>调用完毕,xml所有的布局对象都初始化好了呢?</p>    <p>我们进入到@Bind注解里看看究竟,看看是否找得到线索</p>    <pre>  <code class="language-java">  @Retention(CLASS) @Target(FIELD)  public @interface Bind {    /** View ID to which the field will be bound. */    int[] value();  }  </code></pre>    <p>可以看到@Retention(CLASS),这句表示:保留时间 编译时,也就是工程编译时运行的注解.</p>    <p>常规获取View对象的方式是这样的:</p>    <pre>  <code class="language-java">tv01 = (TextView) findViewById(R.id.tv01);      //现在变成这样  @Bind(R.id.tv01) TextView tv01;  </code></pre>    <p>所以说,ButterKnife这个库应该拿到了R.id.tv01 这个id值,也拿到了tv01成员变量,在通过findViewById给tv01赋值</p>    <p>但是,ButterKnife在哪里进行这样的操作呢?</p>    <p>从@Bind注解来看,应该是编译时拿到了id值</p>    <p>我们在build目录下找到了ButterKnife生成的新文件:</p>    <p>build\generated\source\apt\debug\com\butterknifedemo\MainActivity$$ViewBinder</p>    <pre>  <code class="language-java">    // 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;    }  }  </code></pre>    <p>我们发现ButterKnife在build目录下生成了一个类,这个类竟然帮助我们完成了findVieweById的操作</p>    <p>那这个类是怎么制作出来的呢?</p>    <p>现在,我们直接去看ButterKnife源码:</p>    <pre>  <code class="language-java">     //先进入里面看看  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 AssertionError("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);     }   }  </code></pre>    <p>这里有两个问题:</p>    <ol>     <li>findViewBinderForClass通过Activity字节码如何找到的viewBinder?</li>     <li>viewBinder是什么?</li>    </ol>    <p>首先,看viewBinder是什么:</p>    <pre>  <code class="language-java">  /** DO NOT USE: Exposed for generated code. */  public interface ViewBinder<T> {  void bind(Finder finder, T target, Object source);  void unbind(T target);  }  </code></pre>    <p>原来是个接口,注释说自动生成代码用的,看看刚刚在build找到的类:</p>    <pre>  <code class="language-java">  public class MainActivity$$ViewBinder  <T extends com.butterknifedemo.MainActivity> implements ViewBinder<T> {      @Override public void bind(final Finder finder, final T target, Object source) {  </code></pre>    <p>原来viewBinder.bind(finder, target, source);这行代码调用了MainActivity$$ViewBinder类里的bind方法,</p>    <p>帮助我们完成findViewById工作</p>    <p>现在我们知道了,平时我们调用ButterKnife.bind(this);</p>    <p>最终都会调用对应生成的$$ViewBinder类里的bind方法帮助我们完成繁琐的操作</p>    <p>问题2:怎么通过Activity字节码找到viewBinder对象的?</p>    <pre>  <code class="language-java">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;  }  </code></pre>    <p>原来先从BINDERS.get(cls);里面取,空的话在通过Class.forName(clsName + ButterKnifeProcessor.SUFFIX);</p>    <p>创建出一个新对象出来,前提是这个$$ViewBinder已经生成好了</p>    <p>什么时候生成这个类,怎么生成的?通过注解@Bind我们应该猜到,是工程编译时就生成好了的</p>    <p>在ButterKnife源码中我们发现了这一个类:</p>    <pre>  <code class="language-java">  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;   }  </code></pre>    <p>extends AbstractProcessor,继承这一个类,表示它可以在工程编译是运行里面的process方法,</p>    <p>ButterKnife就是通过编译时,apt会自动查找集成AbstractProcessor的类,调用process方法</p>    <p>在process方法中找到存在ButterKnife的注解信息,获取在注解对应下的数据,例如id值</p>    <p>上面的代码我们还可以看到,Bind,OnClick等等注解已经存储好了,就等着遍历配对处理获取数据</p>    <p>看process方法:</p>    <pre>  <code class="language-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 {      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;  }  </code></pre>    <p>从JavaFileObject,Writer这个类就可以知道,ButterKnife把一些东西写到文件中去了,应该猜到</p>    <p>那些自动生成的java文件就从这里出来的</p>    <p>代码生成java文件,代码加载java文件去运行,有点意思</p>    <p>先看看findAndParseTargets方法做了什么:</p>    <pre>  <code class="language-java">    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);    }  }    ....  </code></pre>    <p>可以看到,正在查找Bind之类的注解,猜都猜到通过定位注解获取注解下面的值了吧</p>    <p>现在,我们回过头看看把什么东西写到文件中去了,看着行代码: writer.write(bindingClass.brewJava());</p>    <pre>  <code class="language-java">    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();  }  </code></pre>    <p>可以一目了然的看到使用了StringBuilder手动拼接字符串的方式,生成了java文件,挺不容易的。</p>    <p>问题又来了,这些java文件静态不变化的部分可以写死,那些动态灵活的部分呢?例如View的id,对象名称</p>    <p>继续深入看源码:</p>    <pre>  <code class="language-java">  //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 + "'";    }  </code></pre>    <p>从上面的代码可以知道,动态的部分通过BindingClass这个类来获取的,那这是类怎么来的,看之前的代码:</p>    <pre>  <code class="language-java">@Override public boolean process(Set<? extends TypeElement> elements, RoundEnvironment env) {    Map<TypeElement, BindingClass> targetClassMap = findAndParseTargets(env);  </code></pre>    <p>原来这个类是从findAndParseTargets里来的,刚刚我们知道,里面做了定位Bind,OnClick注解的操作,</p>    <p>定位的同时也把注解的值,例如id值,变量名称存在到BindingClass对象中了,很符合面向对象的思想</p>    <pre>  <code class="language-java">  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;   }  </code></pre>    <p>最后,还有一点的是:ButterKnife通过注解获取id值并没有使用到反射,获取到变量也是通过Activity.view的形式</p>    <p>不同与一些反射获取注解的框架,使用反射会增加IO操作,增加了时间操作,多了会变得卡顿</p>    <p>反射的方式成员变量可使用private,而ButterKnife不可以,必须public或者protected</p>    <p>因为ButterKnife没有使用反射,需要Activity.view这样去获取一些对象赋值</p>    <p>具体可以去看源码,总的来说:</p>    <ol>     <li>ButterKnife将View的id值放到@Bind注解中</li>     <li>ButterKnife通过extends AbstractProcessor编译时自动调用process方法来定位和存在注解与注解上的id值</li>     <li>找到所有带注解与值的对象,存储在集合中,一个for循环一顿狂写,把java文件写到build目录下</li>     <li>当调用ButterKnife.bind(this)的时候,最终会调用生成的$$viewBinder类里的bind方法</li>     <li>$$viewBinder里的bind方法,找已动态生成好了finfViewById的过程,通过Activity.view的形式初始化所有view</li>    </ol>    <p> </p>    <p>来自:https://ruzhan123.github.io/2016/11/09/2016-11-09-22-ButterKnife第三方库源码分析/</p>    <p> </p>