转载请标明出处:【顾林海的博客】
个人开发的微信小程序,目前功能是书籍推荐,后续会完善一些新功能,希望大家多多支持!
##前言
Butterknife是一个专注于Android系统的View注入框架,可以简化代码,比如findViewById、事件监听、资源绑定等,同时该框架使用了编译时注解,可能大家一听到编译时注解就认为这种方式会影响性能,其实编译时注解并不会影响应用的性能,这是因为编译时注解是在代码编译过程中对注解进行处理,生成代码,这些代码在运行时调用,除了编译时注解,还有一个是运行时注解,它是在运行过程中,通过反射获取相关类、方法、参数等信息,因此运行时注解会有性能问题。
##原理解析
平时使用Butterknife时,需要调用Butterknife的bind方法,Butterknife提供以下几种bind方法:
上面六种方法最终都会调用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 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;
}
findAndParseTargts是用于处理@BindViewXX注解的方法,内部有多个循环代码,这些for循环的作用是对注解进行处理。
private Map findAndParseTargets(RoundEnvironment env) {
Map builderMap = new LinkedHashMap<>();
Set 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> entries =
new ArrayDeque<>(builderMap.entrySet());
Map bindingMap = new LinkedHashMap<>();
while (!entries.isEmpty()) {
Map.Entry 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;
}
方法代码很多,主要是对下面注解的处理:
处理完毕后会通过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定义的方法。