在上一篇文章ButterKnife使用篇里面已经详细介绍过如何使用ButterKnife了,本篇文章将分析它的原理,了解该工具是如何实现绑定控件的. 本文分析基于ButterKnife版本
'com.jakewharton:butterknife:10.2.1'
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(bindLayoutID());
mBind = ButterKnife.bind(this);
initView(savedInstanceState);
}
@Override
protected void onDestroy() {
super.onDestroy();
if (mBind != null && mBind != Unbinder.EMPTY) {
mBind.unbind();
mBind = null;
}
}
@Override
protected int bindLayoutID() {
return R.layout.activity_main;
}
这是我们使用ButterKnife时所写的代码,那么我们就从 ButterKnife.bind(this)入手,去一步步查看源码
/**
* BindView annotated fields and methods in the specified {@link Activity}. The current content
* view is used as the view root.
*
* @param target Target activity for view binding.
*/
@NonNull @UiThread
public static Unbinder bind(@NonNull Activity target) {
View sourceView = target.getWindow().getDecorView();
return bind(target, sourceView);
}
/**
* 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 (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);
}
}
第一个单参数bind(@NonNull Activity target)方法中,获取了当前window的顶级父View,它是一个FrameLayout,然后调用Unbinder bind(@NonNull Object target, @NonNull View source)方法,这个方法中主要是通过Class类获取构造器,并获取一个新的实例.下面我们继续查看findBindingConstructorForClass方法和constructor.newInstance(target, source)方法
@VisibleForTesting
static final Map, Constructor extends Unbinder>> BINDINGS = new LinkedHashMap<>();
@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;
}
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;
}
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;
}
首先从Map中获取该构造对象,如果存在则直接返回对象,如果不存在,则去构造对应的Constructor.使用Map缓存是为了提高重复使用的效率.
如果类名以"android.","java.","androidx."开头,则直接返回null,否则创建一个新的Class,名字由传入的clsName+"_ViewBinding",如果我传入的类名是ButterKnifeFragment,则返回的实例是ButterKnifeFragment_ViewBinding,查找该文件,可以发现在build中可以找到,如图:
下面具体看看这个类
public class ButterKnifeFragment_ViewBinding implements Unbinder {
private ButterKnifeFragment target;
private View view7f070046;
@UiThread
public ButterKnifeFragment_ViewBinding(final ButterKnifeFragment target, View source) {
this.target = target;
View view;
view = Utils.findRequiredView(source, R.id.button, "field 'mButton' and method 'onClick'");
target.mButton = Utils.castView(view, R.id.button, "field 'mButton'", Button.class);
view7f070046 = view;
view.setOnClickListener(new DebouncingOnClickListener() {
@Override
public void doClick(View p0) {
target.onClick(Utils.castParam(p0, "doClick", 0, "onClick", 0, Button.class));
}
});
target.mEtList = Utils.listFilteringNull(
Utils.findRequiredViewAsType(source, R.id.et_1, "field 'mEtList'", EditText.class),
Utils.findRequiredViewAsType(source, R.id.et_2, "field 'mEtList'", EditText.class),
Utils.findRequiredViewAsType(source, R.id.et_3, "field 'mEtList'", EditText.class));
}
@Override
@CallSuper
public void unbind() {
ButterKnifeFragment target = this.target;
if (target == null) throw new IllegalStateException("Bindings already cleared.");
this.target = null;
target.mButton = null;
target.mEtList = null;
view7f070046.setOnClickListener(null);
view7f070046 = null;
}
}
我在ButterKnifeFragment页面添加了4个控件(3EditText和一个Button,button绑定了点击事件,此处只分析button),还记得前面的双参数bind(@NonNull Object target, @NonNull View source)方法么,就是调用了这个构造方法.它里面有3个函数,一个是findRequiredView,一个是castView,还有一个点击事件,下面我们一个个查看源码
public static View findRequiredView(View source, @IdRes int id, String who) {
View view = source.findViewById(id);
if (view != null) {
return view;
}
String name = getResourceEntryName(source, id);
throw new IllegalStateException("Required view '"
+ name
+ "' with ID "
+ id
+ " for "
+ who
+ " was not found. If this view is optional add '@Nullable' (fields) or '@Optional'"
+ " (methods) annotation.");
}
public static T castView(View view, @IdRes int id, String who, Class cls) {
try {
return cls.cast(view);
} catch (ClassCastException e) {
String name = getResourceEntryName(view, id);
throw new IllegalStateException("View '"
+ name
+ "' with ID "
+ id
+ " for "
+ who
+ " was of the wrong type. See cause for more info.", e);
}
}
在findRequiredView里面,我们看到了熟悉的findViewById方法...最终,还是回归到了系统的API. castView方法也是调用系统的cast方法..
那么,它是如何实现点击事件防抖动的呢? 继续往下看
/**
* A {@linkplain View.OnClickListener click listener} that debounces multiple clicks posted in the
* same frame. A click on one button disables all buttons for that frame.
*/
public abstract class DebouncingOnClickListener implements View.OnClickListener {
static boolean enabled = true;
private static final Runnable ENABLE_AGAIN = () -> 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);
}
点击一次后,让该控件enabled = false;再调用v.post(ENABLE_AGAIN);恢复控件的状态.
到此为止,我们了解到ButterKnife底层依旧是使用的findViewById方法,但是我们依旧有个疑问,ButterKnifeFragment_ViewBinding 并非我们自己写的,它是如何生成的呢?要知道这个就必须了解注解器相关知识了.
回顾我们使用ButterKnife的条件
dependencies {
implementation 'com.jakewharton:butterknife:10.2.1'
annotationProcessor 'com.jakewharton:butterknife-compiler:10.2.1'
//kapt 'com.jakewharton:butterknife-compiler:10.2.1' //Kotlin
}
@BindView(R.id.button1) Button button1;
@BindView(R.id.button2) Button button2;
ButterKnifeProcessor完成了注解处理的核心逻辑,下面我们来看看它是如何实现的.
@AutoService(Processor.class)
@IncrementalAnnotationProcessor(IncrementalAnnotationProcessorType.DYNAMIC)
@SuppressWarnings("NullAway") // TODO fix all these...
public final class ButterKnifeProcessor extends AbstractProcessor {
//忽略部分代码,只查看核心代码
.......
@Override public synchronized void init(ProcessingEnvironment env) {
super.init(env);
String sdk = env.getOptions().get(OPTION_SDK_INT);
if (sdk != null) {
try {
this.sdk = Integer.parseInt(sdk);
} catch (NumberFormatException e) {
env.getMessager()
.printMessage(Kind.WARNING, "Unable to parse supplied minSdk option '"
+ sdk
+ "'. Falling back to API 1 support.");
}
}
debuggable = !"false".equals(env.getOptions().get(OPTION_DEBUGGABLE));
typeUtils = env.getTypeUtils();
filer = env.getFiler();
try {
trees = Trees.instance(processingEnv);
} catch (IllegalArgumentException ignored) {
try {
// Get original ProcessingEnvironment from Gradle-wrapped one or KAPT-wrapped one.
for (Field field : processingEnv.getClass().getDeclaredFields()) {
if (field.getName().equals("delegate") || field.getName().equals("processingEnv")) {
field.setAccessible(true);
ProcessingEnvironment javacEnv = (ProcessingEnvironment) field.get(processingEnv);
trees = Trees.instance(javacEnv);
break;
}
}
} catch (Throwable ignored2) {
}
}
}
@Override public Set getSupportedOptions() {
ImmutableSet.Builder builder = ImmutableSet.builder();
builder.add(OPTION_SDK_INT, OPTION_DEBUGGABLE);
if (trees != null) {
builder.add(IncrementalAnnotationProcessorType.ISOLATING.getProcessorOption());
}
return builder.build();
}
@Override public Set getSupportedAnnotationTypes() {
Set types = new LinkedHashSet<>();
for (Class extends Annotation> annotation : getSupportedAnnotations()) {
types.add(annotation.getCanonicalName());
}
return types;
}
private Set> getSupportedAnnotations() {
Set> annotations = new LinkedHashSet<>();
annotations.add(BindAnim.class);
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(BindFont.class);
annotations.add(BindInt.class);
annotations.add(BindString.class);
annotations.add(BindView.class);
annotations.add(BindViews.class);
annotations.addAll(LISTENERS);
return annotations;
}
@Override public boolean process(Set extends TypeElement> 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;
}
....... //忽略部分代码,只查看核心代码
}
注意,ButterKnifeProcessor类上使用了@AutoService(Processor.class)注解并继承了AbstractProcessor ,来实现注解处理器的注册,注册到 javac 后,在项目编译时就能执行注解处理器了。
我的ButterKnife相关文章
ButterKnife(一): 使用篇
ButterKnife(二): 原理解析篇
ButterKnife(三): ButterKnifeProcessor解析