阅读 152

ButterKnife源码拆轮子学习

前提

Version 10.1.0

地址 github.com/JakeWharton…

ButterKnife有两种实现方式

  1. 定义注解,在运行时利用反射实现。
  2. 定义注解,在编译时利用APT生成固定格式的源文件。

源码下载编译遇到的一个问题

直接git clone的Project的名称是butterknife,和里面的一个Module重名,导致gradle sync失败,需要clone时修改下名称。

使用

Application Module和Library Module在使用上有点差别。

  1. Application Module 1.1 build.gradle 添加依赖
android {
  ...
  // Butterknife requires Java 8.
  compileOptions {
    sourceCompatibility JavaVersion.VERSION_1_8
    targetCompatibility JavaVersion.VERSION_1_8
  }
}

dependencies {
  implementation 'com.jakewharton:butterknife:10.1.0'
  annotationProcessor 'com.jakewharton:butterknife-compiler:10.1.0'
}
复制代码

1.2 使用

不能给private或static添加,否则报错

class ExampleActivity extends Activity {
  @BindView(R.id.user) EditText username;
  @BindView(R.id.pass) EditText password;

  @BindString(R.string.login_error) String loginErrorMessage;

  @OnClick(R.id.submit) void submit() {
    // TODO call server...
  }

  @Override public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.simple_activity);
    ButterKnife.bind(this);
    // TODO Use fields...
  }
}
复制代码
  1. Library Module 2.1 添加依赖

project.gradle

buildscript {
  repositories {
    mavenCentral()
    google()
   }
  dependencies {
    classpath 'com.jakewharton:butterknife-gradle-plugin:10.1.0'
  }
}
复制代码

build.gradle

android {
  ...
  // Butterknife requires Java 8.
  compileOptions {
    sourceCompatibility JavaVersion.VERSION_1_8
    targetCompatibility JavaVersion.VERSION_1_8
  }
}

dependencies {
  implementation 'com.jakewharton:butterknife:10.1.0'
  annotationProcessor 'com.jakewharton:butterknife-compiler:10.1.0'
}
复制代码

添加plugin

apply plugin: 'com.jakewharton.butterknife'
复制代码

2.2 使用 需要用R2替换掉R,因为注解的值只支持常量,而Library Module中的R变量不再是常量,ButterKnife生成的R2变量都是常量。

class ExampleActivity extends Activity {
  @BindView(R2.id.user) EditText username;
  @BindView(R2.id.pass) EditText password;
  ...
}
复制代码

源码分析 先看下源码包结构

可见 支持反射实现和APT实现

  1. 首先分析APT的实现方式

(1) 从入口ButterKnife.bind()开始

/**
   * BindView annotated fields and methods in the specified {@code target} using the {@code source}
   * {@link View} as the view root.
   *
   * @param target Target class for view binding.
   * @param source View root on which IDs will be looked up.
   */
  @NonNull @UiThread
  public static Unbinder bind(@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 {
        ......
    }
  }
复制代码

主要是通过绑定的类,获取继承Unbinder的类的构造方法,利用反射创建对象。

@Nullable @CheckResult @UiThread
  private static Constructor<? extends Unbinder> findBindingConstructorForClass(Class<?> cls) {
    Constructor<? extends Unbinder> bindingCtor = BINDINGS.get(cls);
    // 如果已经获取保存过,则直接返回
    if (bindingCtor != null || BINDINGS.containsKey(cls)) {
      if (debug) Log.d(TAG, "HIT: Cached in binding map.");
      return bindingCtor;
    }
    // 如果是系统类,则返回null
    String clsName = cls.getName();
    if (clsName.startsWith("android.") || clsName.startsWith("java.")
        || clsName.startsWith("androidx.")) {
      if (debug) Log.d(TAG, "MISS: Reached framework class. Abandoning search.");
      return null;
    }
    // 获取类名+"_ViewBinding"的类的构造函数
    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 () {
        ......
    }
    BINDINGS.put(cls, bindingCtor);
    return bindingCtor;
  }
复制代码

返回类名+"_ViewBinding"的类的构造函数。该类是如何产生的呢?

先别急,这个就是APT在编译时生成的。我们先看下这个类的代码。在Project中build下工程。

public static View findRequiredView(View source, @IdRes int id, String who) {
    View view = source.findViewById(id);
    if (view != null) {
      return view;
    }
    ......
  }
复制代码

可见,View的获取,还是通过Android提供的findViewById,和类型转换。

view.setOnClickListener(new DebouncingOnClickListener() {
      @Override
      public void doClick(View p0) {
        target.show(p0);
      }
    });
复制代码

点击事件,封装了一层DebouncingOnClickListener用来防快速点击。 这种防快速点击的方式我们也可以直接应用到我们的工程中。

Context context = source.getContext();
Resources res = context.getResources();
target.appname = res.getString(R.string.app_name);
复制代码

获取资源的方式,也是通过Resource。

(2) 下面分析下,如何在编译时生成类

关于APT技术和如何实现就不细说了,网上都有,很简单。

所以先从ButterKnifeProcessor proces方法开始讲起。

@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;
  }
复制代码

具体生成代码的逻辑就不细说了,主要就是通过APT技术,遍历文件,查找自定义注解,封装成固定格式的javapoet的类,再生成文件。

  • 第一行,遍历所有自定义Bind注解,存到Map中
  • 第二行,生成javapoet中的JavaFile对象
  • 第三行,利用JavaFile对象生成具体文件

(3) 说下ButterKnife如何在Module中生成R2文件,将资源定义成常量,编译在注解中使用,这个我们开发中也可以借鉴。 juejin.im/post/5ce3aa…

关注下面的标签,发现更多相似文章
评论