分分钟带你读懂 ButterKnife 的源码

发布时间:2020-08-30 11:06:13 作者:Android丶VG
来源:网络 阅读:288

为什么要写这一系列的博客呢?

因为在 Android 开发的过程中, 泛型,反射,注解这些知识进场会用到,几乎所有的框架至少都会用到上面的一两种知识,如 Gson 就用到泛型,反射,注解,Retrofit 也用到泛型,反射,注解 。学好这些知识对我们进阶非常重要,尤其是阅读开源框架源码或者自己开发开源框架。

前言

ButterKnife 这个开源库火了有一段时间了,刚开始它的实现原理是使用反射实现的,性能较差。再后面的 版本中逐渐使用注解+放射实现,性能提高了不少。

ButterKnife是基于编译时的框架,它能够帮助我们减去每次写 FindViewById 的麻烦,截止到 2017.5.1 ,在 github 上面的 start 已经超过 15000.

本篇博客要分析的 ButterKnife 的源码主要包括以下三个部分,版本号是8.5.1

其中 butterknife-annotations 库主要用来存放自定义注解;butterknife-compiler 主要是用来扫描哪些地方使用到我们的自定义注解,并进行相应的处理,生成模板代码等;butterknife 主要是用来注入我们的代码的。

我们先来先一下要怎样使用 butterknife:
ButterKnife 的基本使用
在 moudle 的 build.gradle 增加依赖

dependencies {
  compile 'com.jakewharton:butterknife:8.5.1'
  annotationProcessor 'com.jakewharton:butterknife-compiler:8.5.1'
}
public class SimpleActivity extends Activity {
  private static final ButterKnife.Action<View> ALPHA_FADE = new ButterKnife.Action<View>() {
    @Override public void apply(@NonNull View view, int index) {
      AlphaAnimation alphaAnimation = new AlphaAnimation(0, 1);
      alphaAnimation.setFillBefore(true);
      alphaAnimation.setDuration(500);
      alphaAnimation.setStartOffset(index * 100);
      view.startAnimation(alphaAnimation);
    }
  };

  @BindView(R2.id.title) TextView title;
  @BindView(R2.id.subtitle) TextView subtitle;
  @BindView(R2.id.hello) Button hello;
  @BindView(R2.id.list_of_things) ListView listOfThings;
  @BindView(R2.id.footer) TextView footer;

  @BindViews({ R2.id.title, R2.id.subtitle, R2.id.hello }) List<View> headerViews;

  private SimpleAdapter adapter;

  @OnClick(R2.id.hello) void sayHello() {
    Toast.makeText(this, "Hello, views!", LENGTH_SHORT).show();
    ButterKnife.apply(headerViews, ALPHA_FADE);
  }

  @OnLongClick(R2.id.hello) boolean sayGetOffMe() {
    Toast.makeText(this, "Let go of me!", LENGTH_SHORT).show();
    return true;
  }

  @OnItemClick(R2.id.list_of_things) void onItemClick(int position) {
    Toast.makeText(this, "You clicked: " + adapter.getItem(position), LENGTH_SHORT).show();
  }

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

    // Contrived code to use the bound fields.
    title.setText("Butter Knife");
    subtitle.setText("Field and method binding for Android views.");
    footer.setText("by Jake Wharton");
    hello.setText("Say Hello");

    adapter = new SimpleAdapter(this);
    listOfThings.setAdapter(adapter);
  }
}

调用 gradle build 命令,我们在相应的目录下将可以看到生成类似这样的代码。

public class SimpleActivity_ViewBinding<T extends SimpleActivity> implements Unbinder {
  protected T target;

  private View view2130968578;

  private View view2130968579;

  @UiThread
  public SimpleActivity_ViewBinding(final T target, View source) {
    this.target = target;

    View view;
    target.title = Utils.findRequiredViewAsType(source, R.id.title, "field 'title'", TextView.class);
    target.subtitle = Utils.findRequiredViewAsType(source, R.id.subtitle, "field 'subtitle'", TextView.class);
    view = Utils.findRequiredView(source, R.id.hello, "field 'hello', method 'sayHello', and method 'sayGetOffMe'");
    target.hello = Utils.castView(view, R.id.hello, "field 'hello'", Button.class);
    view2130968578 = view;
    view.setOnClickListener(new DebouncingOnClickListener() {
      @Override
      public void doClick(View p0) {
        target.sayHello();
      }
    });
    view.setOnLongClickListener(new View.OnLongClickListener() {
      @Override
      public boolean onLongClick(View p0) {
        return target.sayGetOffMe();
      }
    });
    view = Utils.findRequiredView(source, R.id.list_of_things, "field 'listOfThings' and method 'onItemClick'");
    target.listOfThings = Utils.castView(view, R.id.list_of_things, "field 'listOfThings'", ListView.class);
    view2130968579 = view;
    ((AdapterView<?>) view).setOnItemClickListener(new AdapterView.OnItemClickListener() {
      @Override
      public void onItemClick(AdapterView<?> p0, View p1, int p2, long p3) {
        target.onItemClick(p2);
      }
    });
    target.footer = Utils.findRequiredViewAsType(source, R.id.footer, "field 'footer'", TextView.class);
    target.headerViews = Utils.listOf(
        Utils.findRequiredView(source, R.id.title, "field 'headerViews'"), 
        Utils.findRequiredView(source, R.id.subtitle, "field 'headerViews'"), 
        Utils.findRequiredView(source, R.id.hello, "field 'headerViews'"));
  }

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

    target.title = null;
    target.subtitle = null;
    target.hello = null;
    target.listOfThings = null;
    target.footer = null;
    target.headerViews = null;

    view2130968578.setOnClickListener(null);
    view2130968578.setOnLongClickListener(null);
    view2130968578 = null;
    ((AdapterView<?>) view2130968579).setOnItemClickListener(null);
    view2130968579 = null;

    this.target = null;
  }
}

ButterKnife 的执行流程
总的来说,大概可以分为以下几步:

第一步:在编译的时候扫描注解,并做相应的处理,生成 java 代码。这一步,可以拆分为几个小步骤:

butterknife-annotations 讲解
分分钟带你读懂 ButterKnife 的源码
我们知道 ButterKnife 自定义很多的注解,有 BindArray,BindBitmap,BindColor,BindView 等,这里我们以 BindView 为例子讲解就 OK 了,其他的也是基本类似的,这里就不再讲解了。

//编译时注解
@Retention(CLASS)
//成员变量, (includes enum constants) 
@Target(FIELD)
public @interface BindView {
  /** View ID to which the field will be bound. */
  @IdRes int value();
}

Processor 解析器说明
我们先来看一些基本方法:在 init 方法里面得到一些辅助工具类,这样有一个好处,确保工具类是单例的,因为 init 方法只会在初始化的时候调用。

public synchronized void init(ProcessingEnvironment env) {
    super.init(env);

    ---

    //辅助工具类
    elementUtils = env.getElementUtils();
    typeUtils = env.getTypeUtils();
    filer = env.getFiler();

   ---
}

接着重写 getSupportedAnnotationTypes 方法,返回我们支持的注解类型。

@Override
public Set<String> getSupportedAnnotationTypes() {
    Set<String> types = new LinkedHashSet<>();
    for (Class<? extends Annotation> annotation : getSupportedAnnotations()) {
        types.add(annotation.getCanonicalName());
    }
    //返回支持注解的类型
    return types;
}

private Set<Class<? extends Annotation>> getSupportedAnnotations() {
    Set<Class<? extends Annotation>> annotations = new LinkedHashSet<>();

    annotations.add(BindArray.class);
    annotations.add(BindBitmap.class);
    annotations.add(BindBool.class);
    annotations.add(BindColor.class);
    annotations.add(BindDimen.class);
    annotations.add(BindDrawable.class);
    annotations.add(BindFloat.class);
    annotations.add(BindInt.class);
    annotations.add(BindString.class);
    annotations.add(BindView.class);
    annotations.add(BindViews.class);
    annotations.addAll(LISTENERS);

    return annotations;
}

接下来来看我们的重点, process 方法。所做的工作大概就是拿到我们所有的注解信息,存进 map 集合,遍历 map 集合,做相应的 处理,生成 java 代码。

@Override
public boolean process(Set<? extends TypeElement> elements, RoundEnvironment env) {
    //  拿到所有的注解信息,TypeElement 作为 key,BindingSet 作为 value
    Map<TypeElement, BindingSet> bindingMap = findAndParseTargets(env);
    // 遍历 map 里面的所有信息,并生成 java 代码
    for (Map.Entry<TypeElement, BindingSet> entry : bindingMap.entrySet()) {
        TypeElement typeElement = entry.getKey();
        BindingSet binding = entry.getValue();

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

    return false;
}

这里我们进入 findAndParseTargets 方法,看里面到底是怎样将注解信息存进 map 集合的?

findAndParseTargets 方法里面 针对每一个自定义注解(BindArray,BindBitmap,BindColor,BindView) 等都做了处理,这里我们重点关注 @BindView 的处理即可。其他注解的处理思想也是一样的。

我们先来看一下 findAndParseTargets 方法的前半部分,遍历 env.getElementsAnnotatedWith(BindView.class) 集合,并调用 parseBindView 方法去转化。

private Map<TypeElement, BindingSet> findAndParseTargets(RoundEnvironment env) {
    Map<TypeElement, BindingSet.Builder> builderMap = new LinkedHashMap<>();
    Set<TypeElement> erasedTargetNames = new LinkedHashSet<>();

    scanForRClasses(env);

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

    ---

    // 后半部分,待会再讲

}

可以看到牵绊部分的主要逻辑在 parseBindView 方法里面,主要做了以下几步操作:

private TypeSpec createType(int sdk) {
TypeSpec.Builder result = TypeSpec.classBuilder(bindingClassName.simpleName())
.addModifiers(PUBLIC);
if (isFinal) {
result.addModifiers(FINAL);
}

if (parentBinding != null) {
    result.superclass(parentBinding.bindingClassName);
} else {
    result.addSuperinterface(UNBINDER);
}

if (hasTargetField()) {
    result.addField(targetTypeName, "target", PRIVATE);
}
// 如果是 View 或者是 View 的子类的话,添加构造方法
if (isView) {
    result.addMethod(createBindingConstructorForView());
} else if (isActivity) { // 如果是 Activity 或者是 Activity 的子类的话,添加构造方法
    result.addMethod(createBindingConstructorForActivity());
} else if (isDialog) {  // 如果是 Dialog 或者是 Dialog 的子类的话,添加构造方法
    result.addMethod(createBindingConstructorForDialog());
}
//  如果构造方法不需要 View 参数,添加 需要 View 参数的构造方法
if (!constructorNeedsView()) {
    // Add a delegating constructor with a target type + view signature for reflective use.
    result.addMethod(createBindingViewDelegateConstructor());
}
result.addMethod(createBindingConstructor(sdk));

if (hasViewBindings() || parentBinding == null) {
    //生成unBind方法
    result.addMethod(createBindingUnbindMethod(result));
}

return result.build();

}

接着我们一起来看一下 createBindingConstructor(sdk) 方法,大概做的事情就是

- 判断是否有设置监听,如果有监听,将 View 设置为 final
- 遍历 viewBindings ,调用 addViewBinding 生成 findViewById 形式的代码。

private MethodSpec createBindingConstructor(int sdk) {
MethodSpec.Builder constructor = MethodSpec.constructorBuilder()
.addAnnotation(UI_THREAD)
.addModifiers(PUBLIC);
// 如果有方法绑定,比如 @onClick,那么增加一个 targetTypeName 类型 的方法参数 target,并且是 final 类型的
if (hasMethodBindings()) {
constructor.addParameter(targetTypeName, "target", FINAL);
} else { // 如果没有 ,不是 final 类型的
constructor.addParameter(targetTypeName, "target");
}
//如果有注解的 View,那么添加 VIEW 类型 source 参数
if (constructorNeedsView()) {
constructor.addParameter(VIEW, "source");
} else {
// 添加 Context 类型的 context 参数
constructor.addParameter(CONTEXT, "context");
}

if (hasUnqualifiedResourceBindings()) {
    // Aapt can change IDs out from underneath us, just suppress since all will work at
    // runtime.
    constructor.addAnnotation(AnnotationSpec.builder(SuppressWarnings.class)
            .addMember("value", "$S", "ResourceType")
            .build());
}
// 如果 @OnTouch 绑定 View,添加 @SuppressLint("ClickableViewAccessibility")
if (hasOnTouchMethodBindings()) {
    constructor.addAnnotation(AnnotationSpec.builder(SUPPRESS_LINT)
            .addMember("value", "$S", "ClickableViewAccessibility")
            .build());
}
// 如果 parentBinding 不为空,调用父类 的构造方法
if (parentBinding != null) {
    if (parentBinding.constructorNeedsView()) {
        constructor.addStatement("super(target, source)");
    } else if (constructorNeedsView()) {
        constructor.addStatement("super(target, source.getContext())");
    } else {
        constructor.addStatement("super(target, context)");
    }
    constructor.addCode("\n");
}
//  添加成员变量
if (hasTargetField()) {
    constructor.addStatement("this.target = target");
    constructor.addCode("\n");
}

if (hasViewBindings()) {
    if (hasViewLocal()) {
        // Local variable in which all views will be temporarily stored.
        constructor.addStatement("$T view", VIEW);
    }
    //   遍历  viewBindings,生成  source.findViewById($L) 代码
    for (ViewBinding binding : viewBindings) {
        addViewBinding(constructor, binding);
    }
    for (FieldCollectionViewBinding binding : collectionBindings) {
        constructor.addStatement("$L", binding.render());
    }

    if (!resourceBindings.isEmpty()) {
        constructor.addCode("\n");
    }
}

if (!resourceBindings.isEmpty()) {
    if (constructorNeedsView()) {
        constructor.addStatement("$T context = source.getContext()", CONTEXT);
    }
    if (hasResourceBindingsNeedingResource(sdk)) {
        constructor.addStatement("$T res = context.getResources()", RESOURCES);
    }
    for (ResourceBinding binding : resourceBindings) {
        constructor.addStatement("$L", binding.render(sdk));
    }
}

return constructor.build();

}

下面我们一起来看一下 addViewBinding 方法是怎样生成代码的。

private void addViewBinding(MethodSpec.Builder result, ViewBinding binding) {
if (binding.isSingleFieldBinding()) {
// Optimize the common case where there's a single binding directly to a field.
FieldViewBinding fieldBinding = binding.getFieldBinding();
// 注意这里直接使用了 target. 的形式,所以属性肯定是不能 private 的
CodeBlock.Builder builder = CodeBlock.builder()
.add("target.$L = ", fieldBinding.getName());

    boolean requiresCast = requiresCast(fieldBinding.getType());
    if (!requiresCast && !fieldBinding.isRequired()) {
        builder.add("source.findViewById($L)", binding.getId().code);
    } else {
        builder.add("$T.find", UTILS);
        builder.add(fieldBinding.isRequired() ? "RequiredView" : "OptionalView");
        if (requiresCast) {
            builder.add("AsType");
        }
        builder.add("(source, $L", binding.getId().code);
        if (fieldBinding.isRequired() || requiresCast) {
            builder.add(", $S", asHumanDescription(singletonList(fieldBinding)));
        }
        if (requiresCast) {
            builder.add(", $T.class", fieldBinding.getRawType());
        }
        builder.add(")");
    }
    result.addStatement("$L", builder.build());
    return;
}

**ButterKnife 是怎样实现代码注入的**
使用过 ButterKnife 得人基本都知道,我们是通过 bind 方法来实现注入的,即自动帮我们 findViewById ,解放我们的双手,提高工作效率。下面我们一起来看一下 bind 方法是怎样实现注入的。

@NonNull@UiThread
br/>@UiThread
View sourceView = target.getWindow().getDecorView();
return createBinding(target, sourceView);
}

可以看到 bind 方法很简单,逻辑基本都交给 createBinding 方法去完成。我们一起进入 createBinding 方法来看一下到底做了什么

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());
// 从 Class 中查找 constructor
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);
}

}

其实 createBinding 来说,主要做了这几件事情

- 传入 class ,通过 findBindingConstructorForClass 方法来实例化 constructor
- 利用反射来初始化 constructor 对象
- 初始化 constructor 失败会抛出异常

下面我们一起来看一下 findBindingConstructorForClass 方法是怎样实现的。

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;
}
// 如果是 android ,java 原生的文件,不处理
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);
}
// 存进 LinkedHashMap 缓存
BINDINGS.put(cls, bindingCtor);
return bindingCtor;
}


它的实现思想是这样的:
- 读取缓存,若缓存命中,直接返回,这样有利于提高效率。从代码中可以看到,缓存是通过存进 map 集合实现的。
- 是否是我们目标文件,是的话,进行处理,不是的话,直接返回,并打印相应的日志
- 利用类加载器加载我们自己生成的 class 文件,并获取其构造方法,获取到,直接返回。获取不到,会抛出异常,在异常的处理中,我们再从当前 class 文件的父类去查找。并把结果存进 map 集合中,做缓存处理。

我们对 ButterKnife 的分析到此为止。

**题外话**
这篇博客主要是分析了 ButterKnife 的主要原理实现,对 ButterKnife 里面的一些实现细节并未详细分析。不过对我们读懂代码已经足够了。下一个系列,主要讲解 CoordinatorLayout 的实现原理及怎样自定义 CoordinatorLayout 的 behavior 实现仿新浪微博发现页面的效果,敬请期待。
**关于我**
更多信息可以点击[关于我](https://www.jianshu.com/p/78f6f0b5bce4)
, 非常希望和大家一起交流 , 共同进步
推荐阅读:
  1. BAT大牛 带你深度剖析Android 10大开源框架
  2. 常用的Android 开发工具有什么

免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。

android 源码 butterknife

上一篇:这个功能使用说明是每次app更新或者第一次安装都需要显示的

下一篇:js实现自动播放匀速轮播图

相关阅读

您好,登录后才能下订单哦!

密码登录
登录注册
其他方式登录
点击 登录注册 即表示同意《亿速云用户服务条款》