ButterKnife(黄油刀)是控件注入框架,可以帮助安卓开发者省去初始化控件的重复性工作,简单快捷的初始化布局文件中的控件,极大的提升开发效率。
ButterKnife框架有很多优化:
- 强大的View绑定和Click事件处理功能,简化代码,提升开发效率
- 方便处理Adapter中的ViewHolder绑定问题
- 运行时不会影响App效率,使用配置方便
- 代码清晰,可读性强
与IOC架构主要区别有:
- 共同特点:都实现了解耦的目的
- 核心技术:IOC框架运行时通过反射技术(reflect),ButterKnife框架通过注解处理器技术(APT)
- 开发使用:两者几乎一样
- 代码难易:IOC编码更具有挑战性
- 程序稳定:两者都未发现致命的缺陷
- 两者缺陷:reflect会消耗一定性能,APT会增加APK的大小
- 开发追求:更偏向编译期的APT技术
ButterKnife基本使用
- 在build.gradle文件中添加依赖:
android {
...
// Butterknife requires Java 8.
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
dependencies {
implementation 'com.jakewharton:butterknife:10.2.0'
annotationProcessor 'com.jakewharton:butterknife-compiler:10.2.0'
}
如果是使用kotlin语言,将annotationProcessor替换为kapt
- 如果想在Android Library中使用ButterKnife,在项目根build.gradle中添加buildScript:
buildscript {
repositories {
mavenCentral()
google()
}
dependencies {
classpath 'com.jakewharton:butterknife-gradle-plugin:10.2.0'
}
}
然后在项目module中的build.gradle中apply
apply plugin: 'com.android.library'
apply plugin: 'com.jakewharton.butterknife'
最后在ButterKnife注解中使用R2替换R
class ExampleActivity extends Activity {
@BindView(R2.id.user) EditText username;
@BindView(R2.id.pass) EditText password;
...
}
- 在字段上通过
@BindView
注解,代替findViewById
。在Activity中setContentView方法后调用ButterKnife.bind(this)
class ExampleActivity extends Activity {
@BindView(R.id.title) TextView title;
@BindView(R.id.subtitle) TextView subtitle;
@BindView(R.id.footer) TextView footer;
@Override public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.simple_activity);
ButterKnife.bind(this);
// TODO Use fields...
}
}
ButterKnife源码原理分析
ButterKnife框架不是使用反射技术实现的,而是通过APT技术生成委托类代码,通过委托类查找试图中的View。以上例子生成的代码如下:
public class ExampleActivity_ViewBinding implements Unbinder {
private ExampleActivity target;
private View view7f0700af;
@UiThread
public ExampleActivity_ViewBinding(ExampleActivity target) {
this(target, target.getWindow().getDecorView());
}
@UiThread
public ExampleActivity_ViewBinding(final ExampleActivity target, View source) {
this.target = target;
View view;
view = Utils.findRequiredView(source, R.id.title, "field 'title' and method 'jumpTestActivity'");
target.title = Utils.castView(view, R.id.title, "field 'title'", TextView.class);
view7f0700af = view;
view.setOnClickListener(new DebouncingOnClickListener() {
@Override
public void doClick(View p0) {
target.jumpTestActivity();
}
});
target.subtitle = Utils.findRequiredViewAsType(source, R.id.subtitle, "field 'subtitle'", TextView.class);
target.footer = Utils.findRequiredViewAsType(source, R.id.footer, "field 'footer'", TextView.class);
}
}
然后调用ButterKnife.bind(this)加载上面生成的代码。接下来看下bind方法源码实现:
public static Unbinder bind(@NonNull Activity target) {
View sourceView = target.getWindow().getDecorView();
return bind(target, sourceView);
}
bind方法需要在setContentView方法之后调用,否则sourceView为null。
public static Unbinder bind(@NonNull Object target, @NonNull View source) {
// 获取目标的Class类,如上面示例中:ExampleActivity
Class<?> targetClass = target.getClass();
if (debug) Log.d(TAG, "Looking up binding for " + targetClass.getName());
// 找到目标类的构造,如:ExampleActivity_ViewBinding类的构造
Constructor<? extends Unbinder> constructor = findBindingConstructorForClass(targetClass);
if (constructor == null) { // 如果没有找到,直接返回空实现的委托类
return Unbinder.EMPTY;
}
//noinspection TryWithIdenticalCatches Resolves to API 19+ only type.
try {
// 通过反射创建目标类的实例,也是通过APT生成的ExampleActivity_ViewBinding类实例
return constructor.newInstance(target, source);
} catch (Exception e) {
// ...
}
}
首先通过绑定目标类的查找由APT生成的类的构造,然后创建目标类的实例对象。例如:创建生成的ExampleActivity_ViewBinding类
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;
}
// 如果目标类不是自己定义的,是JDK或者SDK中的类直接返回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;
}
try {
// 加载目标类的Class,如:ExampleActivity_ViewBinding类的Class
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;
}
通过反射技术加载目标类的构造。先是从缓存中获取,如果找到直接返回,否则反射获取并放入到缓存中。
ButterKnife编译时注解
运行时注解由于性能问题被一些人所诟病,主要是通过反射技术实现,而编译时注解核心原理是通过APT(Annotation Processing Tool)实现的。
使用编译时注解的第三方框架有很多,如:ButterKnife、Dragger、Retrofit、ARouter等。而ButterKnife这个库是针对View,资源ID,部分事件等进行注解的开源库,它能够去除掉一些不怎么雅观的样板式代码,使得我们的代码更加简洁,易于维护,同时使用APT技术也使得它的效率得到保证。
这篇文章组件化已经详细介绍了APT技术,这里不在重复了。
ButterKnife框架Coding
新建一个java libary库,在build.gradle文件中添加依赖:
dependencies {
implementation project(path: ':annotation')
compileOnly 'com.google.auto.service:auto-service:1.0-rc6'
implementation 'com.google.auto:auto-common:0.10'
api 'com.squareup:javapoet:1.11.1'
annotationProcessor'com.google.auto.service:auto-service:1.0-rc6'
}
创建ButterKnifeProcessor类继承AbstractProcessor,并指定JDK编译版本、支持的注解类型等
// 用来生成 META-INF/services/javax.annotation.processing.Processor文件
@AutoService(Processor.class)
// 指定JDK编译版本
@SupportedSourceVersion(SourceVersion.RELEASE_8)
// 允许/支持的注解类型,让注解处理器处理
@SupportedAnnotationTypes({"com.todo.butterknife.annotation.BindView", "com.todo.butterknife.annotation.OnClick"})
public class ButterKnifeProcessor extends AbstractProcessor { }
接下来初始化一些工具类:
// 操作Element工具类(类、函数、属性都是操作Element工具类)
private Elements elementUtils;
// Messager用来报告错误,警告和其他提示信息
private Messager messager;
// Types类信息工具类,包含操作TypeMirror的工具方法
private Types typeUtils;
// 文件生成器,Filer用来创建新的类文件,class文件及辅助文件
private Filer filer;
@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
super.init(processingEnvironment);
elementUtils = processingEnv.getElementUtils();
messager = processingEnv.getMessager();
typeUtils = processingEnv.getTypeUtils();
filer = processingEnv.getFiler();
messager.printMessage(Diagnostic.Kind.NOTE, "<<<<<ButterKnifeProcessor##INIT>>>>>");
}
接下来处理支持的注解
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
if (set.isEmpty()) return false;
Set<? extends Element> elementsAnnotatedWithBindView = roundEnvironment.getElementsAnnotatedWith(BindView.class);
Set<? extends Element> elementsAnnotatedWithOnClick = roundEnvironment.getElementsAnnotatedWith(OnClick.class);
if ((elementsAnnotatedWithBindView != null && !elementsAnnotatedWithBindView.isEmpty())
|| (elementsAnnotatedWithOnClick != null && !elementsAnnotatedWithOnClick.isEmpty())) {
// 缓存满足条件被注解的元素
fillElementMap(elementsAnnotatedWithBindView, elementsAnnotatedWithOnClick);
try {
// 创建文件
createFile();
} catch (IOException e) {
e.printStackTrace();
}
return true;
}
return false;
}
缓存被@BindView和@OnClick注解的元素
private Map<TypeElement, ElementSet> cacheMap = new HashMap<>();
private void fillElementMap(
Set<? extends Element> elementsAnnotatedWithBindView,
Set<? extends Element> elementsAnnotatedWithOnClick
) {
if (elementsAnnotatedWithBindView != null && !elementsAnnotatedWithBindView.isEmpty()) {
for (Element element : elementsAnnotatedWithBindView) {
messager.printMessage(Diagnostic.Kind.NOTE, "被@BindView注解的元素::" + element.getSimpleName());
// 判断元素种类是否是字段
if (element.getKind() == ElementKind.FIELD) {
VariableElement fieldElement = (VariableElement) element;
// 注解在属性上,属性节点的父节点是类节点
TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();
// 如果缓存Map中包含指定的类节点,直接加入到缓存中
if (cacheMap.containsKey(enclosingElement)) {
cacheMap.get(enclosingElement).variableElements.add(fieldElement);
} else {
ElementSet set = new ElementSet();
set.variableElements.add(fieldElement);
cacheMap.put(enclosingElement, set);
}
}
}
}
if (elementsAnnotatedWithOnClick != null && !elementsAnnotatedWithOnClick.isEmpty()) {
for (Element element : elementsAnnotatedWithOnClick) {
messager.printMessage(Diagnostic.Kind.NOTE, "被@OnClick注解的元素::" + element.getSimpleName());
if (element.getKind() == ElementKind.METHOD) {
ExecutableElement methodElement = (ExecutableElement) element;
TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();
if (cacheMap.containsKey(enclosingElement)) {
cacheMap.get(enclosingElement).executableElements.add(methodElement);
} else {
ElementSet set = new ElementSet();
set.executableElements.add(methodElement);
cacheMap.put(enclosingElement, set);
}
}
}
}
}
ElementSet的结构为:
public class ElementSet {
// 被@BindView注解的属性集合
public List<VariableElement> variableElements = new ArrayList<>();
// 被@OnClick注解的方法集合
public List<ExecutableElement> executableElements = new ArrayList<>();
}
最后一步就是创建文件
private void createFile() throws IOException {
if (cacheMap.isEmpty()) return;
TypeElement unBinderElement = elementUtils.getTypeElement("com.todo.butterknife.api.UnBinder");
TypeElement debouncingOnClickListenerElement = elementUtils.getTypeElement("com.todo.butterknife.api.DebouncingOnClickListener");
TypeElement viewElement = elementUtils.getTypeElement("android.view.View");
for (Map.Entry<TypeElement, ElementSet> entry : cacheMap.entrySet()) {
TypeElement typeElement = entry.getKey();
ElementSet elementSet = entry.getValue();
ParameterSpec targetParameterSpec = ParameterSpec.builder(
ClassName.get(typeElement), "target", Modifier.FINAL
).build();
// public void bind(ExampleActivity target)
MethodSpec.Builder bindMethodBuilder = MethodSpec.methodBuilder("bind")
.addAnnotation(Override.class)
.addModifiers(Modifier.PUBLIC)
.addParameter(targetParameterSpec);
if (!elementSet.variableElements.isEmpty()) {
for (VariableElement element : elementSet.variableElements) {
// target.title = target.findViewById(R.id.title);
String fieldName = element.getSimpleName().toString();
int resId = element.getAnnotation(BindView.class).value();
String format = "$N." + fieldName + " = $N.findViewById($L)";
bindMethodBuilder.addStatement(format, "target", "target", resId);
}
}
if (!elementSet.executableElements.isEmpty()) {
for (ExecutableElement element : elementSet.executableElements) {
// target.findViewById(R.id.title).setOnClickListener(new DebouncingOnClickListener() {
// @Override
// protected void doClick(View v) {
// target.jumpTestActivity();
// }
// });
String methodName = element.getSimpleName().toString();
int resId = element.getAnnotation(OnClick.class).value();
bindMethodBuilder.beginControlFlow("$N.findViewById($L).setOnClickListener(new $T()",
"target", resId, ClassName.get(debouncingOnClickListenerElement))
.beginControlFlow("protected void doClick($T v)", ClassName.get(viewElement))
.addStatement("$N." + methodName + "()", "target")
.endControlFlow()
.endControlFlow(")");
}
}
MethodSpec bindMethod = bindMethodBuilder.build();
ClassName className = ClassName.get(typeElement);
String packageName = className.packageName();
// ExampleActivity_ViewBinder
String finalClassName = className.simpleName() + "_ViewBinder";
messager.printMessage(Diagnostic.Kind.NOTE, "最终生成的类文件:" + packageName + "." + finalClassName);
ParameterizedTypeName parameterizedTypeName = ParameterizedTypeName.get(ClassName.get(unBinderElement), className);
// public class ExampleActivity_ViewBinder implements UnBinder<ExampleActivity>
TypeSpec finaClass = TypeSpec.classBuilder(finalClassName)
.addModifiers(Modifier.PUBLIC, Modifier.FINAL)
.addSuperinterface(parameterizedTypeName)
.addMethod(bindMethod).build();
JavaFile.builder(
packageName, finaClass
).build().writeTo(filer);
}
}
如果我的文章对您有帮助,不妨点个赞鼓励一下(^_^)
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。