欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Android之Butterknife原理解析

程序员文章站 2024-02-29 23:27:04
...

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

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

##前言

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

##原理解析

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

Android之Butterknife原理解析

上面六种方法最终都会调用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<? extends Unbinder> 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<? extends Unbinder> findBindingConstructorForClass(Class<?> cls) {
  Constructor<? extends Unbinder> 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<? extends Unbinder>) 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<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        // TODO Auto-generated method stub return false; } }
    }
}

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

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

    for (Map.Entry<TypeElement, BindingSet> 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<TypeElement, BindingSet> findAndParseTargets(RoundEnvironment env) {
    Map<TypeElement, BindingSet.Builder> builderMap = new LinkedHashMap<>();
    Set<TypeElement> 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<? extends Annotation> 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<Map.Entry<TypeElement, BindingSet.Builder>> entries =
            new ArrayDeque<>(builderMap.entrySet());
    Map<TypeElement, BindingSet> bindingMap = new LinkedHashMap<>();
    while (!entries.isEmpty()) {
        Map.Entry<TypeElement, BindingSet.Builder> 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原理解析

处理完毕后会通过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定义的方法。