ButterKnife第三方庫源碼分析

ButterKnife原理其實也很簡單

我的Github博客詳解


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


這裏有兩個問題:
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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章