学习完注解,再来探索下ButterKnife的实现原理

1. 简介

分析完Java中的反射(有关Java反射的使用看这一篇就够了)和注解(Java注解全面总结),如果就这样结束了,总感觉缺少了些什么,不拿一个开源库来分析分析,然后动手实践一遍,怎么能体会到反射和注解的博大精深呢。于是想到了Android中的一个开源库ButterKnife,用法简单,为我们减少了大量垃圾代码(例如findViewById)。本篇文章首先从ButterKnife的基本用法入手,然后着手分析ButterKnife的实现原理,最后自己动手撸一把,实现最简单的替换findViewById功能,能看到开源库并不代表就会了,只有自己动手实现一遍,才能够真正掌握,希望阅读完本篇文章,能够给大家一定的技术提升。

2. 基本用法

本篇文章重点在于ButterKnife的源码分析和动手实践,只是简单介绍下最基本用法,对于更多的用法细节,不在讨论范围内。

     2.1 ButterKnife项目地址

https://github.com/JakeWharton/butterknife

     2.2 基本配置

百度相关文章,看到很多博客在写AS配置ButterKnife的时候,都说首先需要在Project 的 build.gradle中添加相关代码,然后在APP的build.gradle中添加 apply plugin ……, 最后在dependencies中添加相关依赖等等,其实并非如此,虽然这样配置是不影响最终的结果的,但还是有些多余,或者说对于Module是application和Module是library,两者的配置是有些不一样的,可以参考官方文档。

        1) Module为application的配置

如果你仅仅是在Module为application的模块下使用ButterKnife,那么配置非常简单,只需要在Module下的build.gradle文件中添加如下代码即可:

dependencies {
    .........
    compile 'com.jakewharton:butterknife:8.8.1'
    annotationProcessor 'com.jakewharton:butterknife-compiler:8.8.1'
}

         2) Module为library的配置

如果你的Module为library,配置稍微复杂一点,其实也非常简单,首先在Project下的build.gradle添加如下代码:

buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        ............
        classpath 'com.jakewharton:butterknife-gradle-plugin:8.8.1'
    }
}

在library的build.gradle文件中添加如下代码:

apply plugin: 'com.android.library'
apply plugin: 'com.jakewharton.butterknife'    // 新增
dependencies {
    ..................
    compile 'com.jakewharton:butterknife:8.8.1'
    annotationProcessor 'com.jakewharton:butterknife-compiler:8.8.1'
}

     2.3 基本使用

ButterKnife的使用十分简单,只是在Module是application和Module是library稍有不同,下面为Module是application的最简单使用方式:

public class ButterKnifeTestActivity extends AppCompatActivity {

    @BindView(R.id.tv_show_info)
    TextView tvInfo;

    public static void startActivity(Context context){
        Intent intent = new Intent(context, ButterKnifeTestActivity.class);
        context.startActivity(intent);
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_butter_knife_test);

        //绑定初始化ButterKnife
        ButterKnife.bind(this);

        initView();

    }

    private void initView(){
        tvInfo.setText("测试信息");
    }
}

布局文件非常简单,此处就省略了,当进入该activity,textview显示的信息为 测试信息。如果Module是library的情况,需要将@BindView(R.id.tv_show_info) 中的R 修改为 R2即可,官方文档也有相关说明。

3. ButterKnife的实现原理

了解了基本的使用方法,有没有很想知道它的实现原理呢?

文章主要就是从以上基本使用发放入手,一步步探索实现该功能的基本原理,以上用法实在是很简单。

一个使用到ButterKnife.bind(this);另一个就是用到注解@BindView(R.id.tv_show_info) TextView tvInfo;

  • @BindView(R.id.tv_show_info) TextView tvInfo

首先我们来看看BindView注解:

@Retention(CLASS) @Target(FIELD)
public @interface BindView {
  /** View ID to which the field will be bound. */
  @IdRes int value();
}

@Documented
@Retention(CLASS)
@Target({METHOD, PARAMETER, FIELD, LOCAL_VARIABLE})
public @interface IdRes {
}

注解十分简单,BindView为编译时注解,作用域在字段,对其成员变量再次使用注解,对于BindView我们暂且分析到这里。看到是编译型注解,我们是否可以大胆想象一下,ButterKnife通过编写型注解然后结合JavaPoet生成某些class类,对View进行绑定呢,下面我们来看看ButterKnife.bind(this)具体做了些什么。

  • ButterKnife.bind(this)
@NonNull @UiThread
public static Unbinder bind(@NonNull Activity target) {
   View sourceView = target.getWindow().getDecorView();  // 获取当前activity的顶层view
   return createBinding(target, sourceView);
}

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

代码首先获取传入activity的顶层view视图对象,然后调用createBinding函数,createBinding中又调用了findBindingConstructorForClass函数,返回一个构造器,看过我前面的有关反射的文字,就知道通过构造器可以实例化对应的对象,事实却是如此,后面调用constructor.newInstance(target, source)函数,构造了具体的实例对象。到这里我们不得不看看findBindingConstructorForClass具体做了些什么事了。

static final Map<Class<?>, Constructor<? extends Unbinder>> BINDINGS = new LinkedHashMap<>();
@Nullable @CheckResult @UiThread
private static Constructor<? extends Unbinder> findBindingConstructorForClass(Class<?> cls) {
    Constructor<? extends Unbinder> bindingCtor = BINDINGS.get(cls); // 首先从一个全局的Map中获取,也是为了性能优化
    if (bindingCtor != null) {
      if (debug) Log.d(TAG, "HIT: Cached in binding map.");
      return bindingCtor;
    }
    String clsName = cls.getName();     // 获取传入activity的完整类名(包括包名)
    if (clsName.startsWith("android.") || clsName.startsWith("java.")) {
       // 排除系统自带的类
      if (debug) Log.d(TAG, "MISS: Reached framework class. Abandoning search.");
      return null;
    }
    try {
       // 此处是最关键的地方了,通过类加载器加载另外一个类,类名为传入的 类_ViewBinding
      Class<?> bindingClass = cls.getClassLoader().loadClass(clsName + "_ViewBinding");
      //noinspection unchecked
      // 通过反射获取该类中具有两个参数的构造器,一个参数为传入的activity,另一个为View
      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);
    }
    // 最后将找到的构造器和传入的class类进行绑定,方便下次进来无需进行下面的一系列反射操作
    BINDINGS.put(cls, bindingCtor);
    return bindingCtor;
}

代码中已经做了很清晰的注释,最终通过反射,获取传入类的类名加上一个“_ViewBinding”这样一个类的class对象,然后在获取该类中的有参构造,最后返回。

到这里逻辑依然不复杂,ButterKnife.bind(this)最终的意义就是调用类名+“_ViewBinding”的有个有参构造,生成一个实例对象。我们来看看这个类名+“_ViewBinding”的类到底是何方神圣。我这里仍然是以我上面的Demo为例来查看。

通过搜索或者在build/generated/source/apt/debug下可以看到ButterKnifeTestActivity_ViewBinding这个类(注意一定要先编译整个工程后,才会有)。

public class ButterKnifeTestActivity_ViewBinding implements Unbinder {
  private ButterKnifeTestActivity target;

  @UiThread
  public ButterKnifeTestActivity_ViewBinding(ButterKnifeTestActivity target) {
    this(target, target.getWindow().getDecorView());
  }

  @UiThread
  public ButterKnifeTestActivity_ViewBinding(ButterKnifeTestActivity target, View source) {
    this.target = target;

    target.tvInfo = Utils.findRequiredViewAsType(source, R.id.tv_show_info, "field 'tvInfo'", TextView.class);
  }

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

    target.tvInfo = null;
  }
}

我们看到ButterKnifeTestActivity_ViewBinding(ButterKnifeTestActivity target, View source)这个构造函数里面的内容,其实上面通过反射调用的构造函数也就是改函数,看里面的代码是否很诧异,尽然有这么一个类为我们定义的activity进行遍历赋值。findRequiredViewAsType这个函数就没必要讲了,返回一个TextView赋值给target的tvInfo。

看到这里大家是否想起了点什么?  我们在自己的activity中对某一个变量使用BindView注解,是不能声明为private的,由于此处直接调用了变量名,这就是原因所在。

分析到此处,那相信大家都了解到,使用BindView注解,在编译期会给我们生产“类名+_ViewBinding”类,那么该类是如何生成的呢?

看过我前面一篇有关注解的文章相信已经了解了,这里再次说明下整个流程。

  • 使用到编译型注解,通常情况下回结合注解处理器一起使用;
  • 如果需要通过代码额外生成Java类,通常会只用开源库JavaPoet;

那么我们来看看ButterKnife是否用到了这两点技术:在ButterKnife源代码中在project下的build.gradle中定义

javapoet: 'com.squareup:javapoet:1.10.0',

在butterknife-compiler module下的build.gradle文件中添加了依赖:api deps.javapoet, 同时我们也可以butterknife-compiler module下找到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, useAndroidX);
      try {
        javaFile.writeTo(filer);
      } catch (IOException e) {
        error(typeElement, "Unable to write binding for type %s: %s", typeElement, e.getMessage());
      }
    }

    return false;
}

该函数作用就是生成对应的Java代码,此处不在详细分析。

关于ButterKnife的基本原理就分析到这里,当然这只是ButterKnife的冰山一角,但是实现原理基本相同。

4. 自定义注解实现ButterKnife简单功能

ButterKnife采用的是编译时注解,为什么采用编译时注解呢?这里给我自己一点看法,大家都知道Java反射是一个相对比较耗性能的操作,正常情况下,如果可以减少使用反射那当然是不去使用,因此ButterKnife最终采用编译时注解,这样大大减少反射的使用,仅仅只是在构造ViewBinding对象时使用,当然另一方面同时又增加了很多class类文件,只是这对性能的影响及apk包大小的影响非常小。

在这里,为了简单化,我采用的是运行时注解来实现ButterKnife的简单功能。代码如下:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface MyBindView {
    int value();
}

public class MyButterKnife {

    public static void bind(Activity activity){
        Class c = activity.getClass();

        Field[] fields = c.getFields();

        for (Field field:fields) {
            MyBindView myBindView = field.getAnnotation(MyBindView.class);
            if(myBindView != null){
                try {
                    field.set(activity, activity.findViewById(myBindView.value()));
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

首先定一个运行是注解,作用域为字段(变量)。然后在定一个MyButterKnife类,实现bind函数,bind函数中主要通过反射获取当前对象中哪些变量使用了MyBindView注解,由于是通过getFields来获取变量,因此变量名必须声明为public类型,然后获取到对应的注解值,通过set函数设置到具体的变量上。下面看看使用的代码:

public class MyButterKnifeTestActivity extends AppCompatActivity {

    @MyBindView(R.id.tv_show_info)
    public TextView tvShowInfo;

    public static void startActivity(Context context){
        Intent intent = new Intent(context, MyButterKnifeTestActivity.class);
        context.startActivity(intent);
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_my_butter_knife_test);
        MyButterKnife.bind(this);

        initView();
    }

    private void initView(){
        tvShowInfo.setText("重新设置显示信息");
    }
}

可以正常运行,显示没问题。是不是非常简单,当然我这只是简单的实现,有些地方考虑也不太周全,仅仅只是用来学习。至此,文章分析到这里就结束了,希望对大家有所帮助。

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值