Android之Butterknife原理解析

转载请标明出处:【顾林海的博客】

个人开发的微信小程序,目前功能是书籍推荐,后续会完善一些新功能,希望大家多多支持!
Android之Butterknife原理解析_第1张图片

##前言

Butterknife是一个专注于Android系统的View注入框架,可以简化代码,比如findViewById、事件监听、资源绑定等,同时该框架使用了编译时注解,可能大家一听到编译时注解就认为这种方式会影响性能,其实编译时注解并不会影响应用的性能,这是因为编译时注解是在代码编译过程中对注解进行处理,生成代码,这些代码在运行时调用,除了编译时注解,还有一个是运行时注解,它是在运行过程中,通过反射获取相关类、方法、参数等信息,因此运行时注解会有性能问题。

##原理解析

平时使用Butterknife时,需要调用Butterknife的bind方法,Butterknife提供以下几种bind方法:

Android之Butterknife原理解析_第2张图片

上面六种方法最终都会调用createBinding方法,这里以bind(Activity)代码为例:

@NonNull @UiThread
public static Unbinder bind(@NonNull Activity target) {
  View sourceView = target.getWindow().getDecorView();
  return createBinding(target, sourceView);
}

方法中通过传入的Activity获取DecorView,DecorView是整个View树的顶层View,内部包含标题栏和ContentView,而ContentView内部就是我们定义的视图View,拿到DecorView后调用用createBinding方法并把目标Activity和DecorView传入过去。

private static Unbinder createBinding(@NonNull Object target, @NonNull View source) {
  Class targetClass = target.getClass();
  if (debug) Log.d(TAG, "Looking up binding for " + targetClass.getName());
  Constructor constructor = findBindingConstructorForClass(targetClass);

  if (constructor == null) {
    return Unbinder.EMPTY;
  }

  //noinspection TryWithIdenticalCatches Resolves to API 19+ only type.
  try {
    return constructor.newInstance(target, source);
  } catch (IllegalAccessException e) {
    throw new RuntimeException("Unable to invoke " + constructor, e);
  } catch (InstantiationException e) {
    throw new RuntimeException("Unable to invoke " + constructor, e);
  } catch (InvocationTargetException e) {
    Throwable cause = e.getCause();
    if (cause instanceof RuntimeException) {
      throw (RuntimeException) cause;
    }
    if (cause instanceof Error) {
      throw (Error) cause;
    }
    throw new RuntimeException("Unable to create binding instance.", cause);
  }
}

方法中获取目标Class,并通过findBindingConstructorForClass方法获取构造函数,findBindingConstructorForClass方法代码如下:

@Nullable @CheckResult @UiThread
private static Constructor findBindingConstructorForClass(Class cls) {
  Constructor bindingCtor = BINDINGS.get(cls);
  if (bindingCtor != null) {
    if (debug) Log.d(TAG, "HIT: Cached in binding map.");
    return bindingCtor;
  }
  //以下创建文件
  String clsName = cls.getName();
  if (clsName.startsWith("android.") || clsName.startsWith("java.")) {
    if (debug) Log.d(TAG, "MISS: Reached framework class. Abandoning search.");
    return null;
  }
  try {
    Class bindingClass = cls.getClassLoader().loadClass(clsName + "_ViewBinding");
    //noinspection unchecked
    bindingCtor = (Constructor) bindingClass.getConstructor(cls, View.class);
    if (debug) Log.d(TAG, "HIT: Loaded binding class and constructor.");
  } catch (ClassNotFoundException e) {
    if (debug) Log.d(TAG, "Not found. Trying superclass " + cls.getSuperclass().getName());
    bindingCtor = findBindingConstructorForClass(cls.getSuperclass());
  } catch (NoSuchMethodException e) {
    throw new RuntimeException("Unable to find binding constructor for " + clsName, e);
  }
  BINDINGS.put(cls, bindingCtor);
  return bindingCtor;
}

BINDINGS是一个Map,用于缓存,方便下次使用时之间从集合中取出,如果集合中没有,会加载当前的class的名字加上_ViewBinding,比如MainActivity_ViewBinding,加载并获取该class,在获取它的两个参数的构造函数,最后将加载的class进行缓存存入BINDINGS集合中。之后通过createBinding方法中的newInstance进行实例化。

从上面Butterknife执行的绑定方法就可以知道先去加载classname+_ViewBinding的类,并进行实例化,但这个类我们并没有编写,是自动生成的,也就是编译时生成,编译时注解时需要AbstractProcessor这个类来实现,需要重写它的process方法,比如下面:

public class TestProcessor extends AbstractProcessor {
    @Override
    public boolean process(Set annotations, RoundEnvironment roundEnv) {
        // TODO Auto-generated method stub return false; } }
    }
}

在process方法中可以扫描和处理注解的代码,并会生成相关的Java文件,查看Butterknife中继承了AbstractProcessor的类ButterKnifeProcessor中的process方法:

@Override public boolean process(Set elements, RoundEnvironment env) {
    Map bindingMap = findAndParseTargets(env);

    for (Map.Entry entry : bindingMap.entrySet()) {
        TypeElement typeElement = entry.getKey();
        BindingSet binding = entry.getValue();

        JavaFile javaFile = binding.brewJava(sdk, debuggable);
        try {
            javaFile.writeTo(filer);
        } catch (IOException e) {
            error(typeElement, "Unable to write binding for type %s: %s", typeElement, e.getMessage());
        }
    }

    return false;
}

findAndParseTargts是用于处理@BindViewXX注解的方法,内部有多个循环代码,这些for循环的作用是对注解进行处理。

private Map findAndParseTargets(RoundEnvironment env) {
    Map builderMap = new LinkedHashMap<>();
    Set erasedTargetNames = new LinkedHashSet<>();

    scanForRClasses(env);

    // Process each @BindAnim element.
    for (Element element : env.getElementsAnnotatedWith(BindAnim.class)) {
        if (!SuperficialValidation.validateElement(element)) continue;
        try {
            parseResourceAnimation(element, builderMap, erasedTargetNames);
        } catch (Exception e) {
            logParsingError(element, BindAnim.class, e);
        }
    }

    // Process each @BindArray element.
    for (Element element : env.getElementsAnnotatedWith(BindArray.class)) {
        if (!SuperficialValidation.validateElement(element)) continue;
        try {
            parseResourceArray(element, builderMap, erasedTargetNames);
        } catch (Exception e) {
            logParsingError(element, BindArray.class, e);
        }
    }

    // Process each @BindBitmap element.
    for (Element element : env.getElementsAnnotatedWith(BindBitmap.class)) {
        if (!SuperficialValidation.validateElement(element)) continue;
        try {
            parseResourceBitmap(element, builderMap, erasedTargetNames);
        } catch (Exception e) {
            logParsingError(element, BindBitmap.class, e);
        }
    }

    // Process each @BindBool element.
    for (Element element : env.getElementsAnnotatedWith(BindBool.class)) {
        if (!SuperficialValidation.validateElement(element)) continue;
        try {
            parseResourceBool(element, builderMap, erasedTargetNames);
        } catch (Exception e) {
            logParsingError(element, BindBool.class, e);
        }
    }

    // Process each @BindColor element.
    for (Element element : env.getElementsAnnotatedWith(BindColor.class)) {
        if (!SuperficialValidation.validateElement(element)) continue;
        try {
            parseResourceColor(element, builderMap, erasedTargetNames);
        } catch (Exception e) {
            logParsingError(element, BindColor.class, e);
        }
    }

    // Process each @BindDimen element.
    for (Element element : env.getElementsAnnotatedWith(BindDimen.class)) {
        if (!SuperficialValidation.validateElement(element)) continue;
        try {
            parseResourceDimen(element, builderMap, erasedTargetNames);
        } catch (Exception e) {
            logParsingError(element, BindDimen.class, e);
        }
    }

    // Process each @BindDrawable element.
    for (Element element : env.getElementsAnnotatedWith(BindDrawable.class)) {
        if (!SuperficialValidation.validateElement(element)) continue;
        try {
            parseResourceDrawable(element, builderMap, erasedTargetNames);
        } catch (Exception e) {
            logParsingError(element, BindDrawable.class, e);
        }
    }

    // Process each @BindFloat element.
    for (Element element : env.getElementsAnnotatedWith(BindFloat.class)) {
        if (!SuperficialValidation.validateElement(element)) continue;
        try {
            parseResourceFloat(element, builderMap, erasedTargetNames);
        } catch (Exception e) {
            logParsingError(element, BindFloat.class, e);
        }
    }

    // Process each @BindFont element.
    for (Element element : env.getElementsAnnotatedWith(BindFont.class)) {
        if (!SuperficialValidation.validateElement(element)) continue;
        try {
            parseResourceFont(element, builderMap, erasedTargetNames);
        } catch (Exception e) {
            logParsingError(element, BindFont.class, e);
        }
    }

    // Process each @BindInt element.
    for (Element element : env.getElementsAnnotatedWith(BindInt.class)) {
        if (!SuperficialValidation.validateElement(element)) continue;
        try {
            parseResourceInt(element, builderMap, erasedTargetNames);
        } catch (Exception e) {
            logParsingError(element, BindInt.class, e);
        }
    }

    // Process each @BindString element.
    for (Element element : env.getElementsAnnotatedWith(BindString.class)) {
        if (!SuperficialValidation.validateElement(element)) continue;
        try {
            parseResourceString(element, builderMap, erasedTargetNames);
        } catch (Exception e) {
            logParsingError(element, BindString.class, e);
        }
    }

    // Process each @BindView element.
    for (Element element : env.getElementsAnnotatedWith(BindView.class)) {
        // we don't SuperficialValidation.validateElement(element)
        // so that an unresolved View type can be generated by later processing rounds
        try {
            parseBindView(element, builderMap, erasedTargetNames);
        } catch (Exception e) {
            logParsingError(element, BindView.class, e);
        }
    }

    // Process each @BindViews element.
    for (Element element : env.getElementsAnnotatedWith(BindViews.class)) {
        // we don't SuperficialValidation.validateElement(element)
        // so that an unresolved View type can be generated by later processing rounds
        try {
            parseBindViews(element, builderMap, erasedTargetNames);
        } catch (Exception e) {
            logParsingError(element, BindViews.class, e);
        }
    }

    // Process each annotation that corresponds to a listener.
    for (Class listener : LISTENERS) {
        findAndParseListener(env, listener, builderMap, erasedTargetNames);
    }

    // Associate superclass binders with their subclass binders. This is a queue-based tree walk
    // which starts at the roots (superclasses) and walks to the leafs (subclasses).
    Deque> entries =
            new ArrayDeque<>(builderMap.entrySet());
    Map bindingMap = new LinkedHashMap<>();
    while (!entries.isEmpty()) {
        Map.Entry entry = entries.removeFirst();

        TypeElement type = entry.getKey();
        BindingSet.Builder builder = entry.getValue();

        TypeElement parentType = findParentType(type, erasedTargetNames);
        if (parentType == null) {
            bindingMap.put(type, builder.build());
        } else {
            BindingSet parentBinding = bindingMap.get(parentType);
            if (parentBinding != null) {
                builder.setParent(parentBinding);
                bindingMap.put(type, builder.build());
            } else {
                // Has a superclass binding but we haven't built it yet. Re-enqueue for later.
                entries.addLast(entry);
            }
        }
    }

    return bindingMap;
}

方法代码很多,主要是对下面注解的处理:

Android之Butterknife原理解析_第3张图片

处理完毕后会通过JavaFile javaFile = binding.brewJava(sdk, debuggable);生成Java文件。

接着看生成的文件比如我们的MainActivity_ViewBinding文件,路径在build/generated/source/apt/debug/packgename/下:

public class MainActivity extends AppCompatActivity {

    @BindView(R.id.btn_test)
    Button mButton;


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

    @OnClick(R.id.btn_test)
    public void clickButton() {
        Toast.makeText(this, "test", Toast.LENGTH_SHORT).show();
    }
}


public class MainActivity_ViewBinding implements Unbinder {
  private MainActivity target;

  private View view2131427415;

  @UiThread
  public MainActivity_ViewBinding(MainActivity target) {
    this(target, target.getWindow().getDecorView());
  }

  @UiThread
  public MainActivity_ViewBinding(final MainActivity target, View source) {
    this.target = target;

    View view;
    view = Utils.findRequiredView(source, R.id.btn_test, "field 'mButton' and method 'clickButton'");
    target.mButton = Utils.castView(view, R.id.btn_test, "field 'mButton'", Button.class);
    view2131427415 = view;
    view.setOnClickListener(new DebouncingOnClickListener() {
      @Override
      public void doClick(View p0) {
        target.clickButton();
      }
    });
  }

  @Override
  @CallSuper
  public void unbind() {
    MainActivity target = this.target;
    if (target == null) throw new IllegalStateException("Bindings already cleared.");
    this.target = null;

    target.mButton = null;

    view2131427415.setOnClickListener(null);
    view2131427415 = null;
  }
}


public abstract class DebouncingOnClickListener implements View.OnClickListener {
  static boolean enabled = true;

  private static final Runnable ENABLE_AGAIN = new Runnable() {
    @Override public void run() {
      enabled = true;
    }
  };

  @Override public final void onClick(View v) {
    if (enabled) {
      enabled = false;
      v.post(ENABLE_AGAIN);
      doClick(v);
    }
  }

  public abstract void doClick(View v);
}

在上面通过ButterKnife的bind方法会通过两个参数的构造函数进行实例化,在构造函数中,可以看出最终还是会调用findViewById方法,并对view进行点击事件的监听,DebouncingOnClickListener是View.OnclickListener的子类,用于防止一定时间内对View的多次点击,在onClick方法中执行抽象方法doClick,在MainActivity_ViewBinding的构造函数中,可以看到为Button的点击时会调用MainActivity的clickButton方法,也就是在MainActivity中通过注解@OnClick定义的方法。

你可能感兴趣的:(Android,Android开发笔记)