ButterKnife的github地址
https://github.com/JakeWharton/butterknife
1.ButterKnife的使用
第一步 在moudle的gradle配置butterknife
// 1 引入Butter knife到module
implementation 'com.jakewharton:butterknife:10.2.3'
annotationProcessor 'com.jakewharton:butterknife-compiler:10.2.3'
public class MainActivity extends AppCompatActivity {
// 3.ButterKnife属性初始化 注意View必须有id
@BindView(R.id.tv1)
TextView mTextView;
@BindView(R.id.button1)
Button mButton;
Unbinder unbinder = null;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//2 初始化ButterKnife
unbinder = ButterKnife.bind(this);
//4 ButterKnife属性使用
mTextView.setText("Activity1-butter-tv1");
mButton.setText("Activity1-butter-button1");
}
//5 ButterKnife Event使用
@OnClick(R.id.button1)
void buttonClick() {
Intent intent = new Intent(MainActivity.this,Activity2.class);
startActivity(intent);
}
@Override
protected void onDestroy() {
super.onDestroy();
// 6.调用unbind取消ButterKnife的注入
unbinder.unbind();
}
}
另外一个activity
public class Activity2 extends AppCompatActivity {
@BindView(R.id.tv1)
TextView mTextView;
@BindView(R.id.button1)
Button mButton;
Unbinder unbinder = null;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_2);
unbinder = ButterKnife.bind(this);
mTextView.setText("Activity2-butter-tv2");
mButton.setText("Activity2-butter-button2");
}
@Override
protected void onDestroy() {
super.onDestroy();
unbinder.unbind();
}
}
2.ButterKnife hardcode版
ButterKnife 的主要原理是利用javaPoet自动生成xx_ViewBingding.java代码,比如上面的两个activity会生成下面的两个文件
// Generated code from Butter Knife. Do not modify!
package com.example.selfbutterknife;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import androidx.annotation.CallSuper;
import androidx.annotation.UiThread;
import butterknife.Unbinder;
import butterknife.internal.DebouncingOnClickListener;
import butterknife.internal.Utils;
import java.lang.IllegalStateException;
import java.lang.Override;
public class MainActivity_ViewBinding implements Unbinder {
private MainActivity target;
private View view7f080058;
@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;
target.mTextView = Utils.findRequiredViewAsType(source, R.id.tv1, "field 'mTextView'", TextView.class);
view = Utils.findRequiredView(source, R.id.button1, "field 'mButton' and method 'buttonClick'");
target.mButton = Utils.castView(view, R.id.button1, "field 'mButton'", Button.class);
view7f080058 = view;
view.setOnClickListener(new DebouncingOnClickListener() {
@Override
public void doClick(View p0) {
target.buttonClick();
}
});
}
@Override
@CallSuper
public void unbind() {
MainActivity target = this.target;
if (target == null) throw new IllegalStateException("Bindings already cleared.");
this.target = null;
target.mTextView = null;
target.mButton = null;
view7f080058.setOnClickListener(null);
view7f080058 = null;
}
}
// Generated code from Butter Knife. Do not modify!
package com.example.selfbutterknife;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import androidx.annotation.CallSuper;
import androidx.annotation.UiThread;
import butterknife.Unbinder;
import butterknife.internal.Utils;
import java.lang.IllegalStateException;
import java.lang.Override;
public class Activity2_ViewBinding implements Unbinder {
private Activity2 target;
@UiThread
public Activity2_ViewBinding(Activity2 target) {
this(target, target.getWindow().getDecorView());
}
@UiThread
public Activity2_ViewBinding(Activity2 target, View source) {
this.target = target;
target.mTextView = Utils.findRequiredViewAsType(source, R.id.tv1, "field 'mTextView'", TextView.class);
target.mButton = Utils.findRequiredViewAsType(source, R.id.button1, "field 'mButton'", Button.class);
}
@Override
@CallSuper
public void unbind() {
Activity2 target = this.target;
if (target == null) throw new IllegalStateException("Bindings already cleared.");
this.target = null;
target.mTextView = null;
target.mButton = null;
}
}
其实如果我们不添加ButterKnife的依赖 直接复制以下三个类到activity同级目录
public class MainActivity_ViewBinding {
private MainActivity target;
private View view7f080058;
@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;
target.mTextView = Utils.findRequiredViewAsType(source, R.id.tv1, "field 'mTextView'", TextView.class);
view = Utils.findRequiredView(source, R.id.button1, "field 'mButton' and method 'buttonClick'");
target.mButton = Utils.castView(view, R.id.button1, "field 'mButton'", Button.class);
view7f080058 = view;
view.setOnClickListener(new DebouncingOnClickListener() {
@Override
public void doClick(View p0) {
target.buttonClick();
}
});
}
}
public abstract class DebouncingOnClickListener implements View.OnClickListener {
private static final Runnable ENABLE_AGAIN = () -> enabled = true;
private static final Handler MAIN = new Handler(Looper.getMainLooper());
static boolean enabled = true;
@Override
public final void onClick(View v) {
if (enabled) {
enabled = false;
// Post to the main looper directly rather than going through the view.
// Ensure that ENABLE_AGAIN will be executed, avoid static field {@link #enabled}
// staying in false state.
MAIN.post(ENABLE_AGAIN);
doClick(v);
}
}
public abstract void doClick(View v);
}
public final class Utils {
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> T findRequiredViewAsType(View source, @IdRes int id, String who,
Class<T> cls) {
View view = findRequiredView(source, id, who);
return castView(view, id, who, cls);
}
public static <T> T castView(View view, @IdRes int id, String who, Class<T> 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);
}
}
private static String getResourceEntryName(View view, @IdRes int id) {
if (view.isInEditMode()) {
return "<unavailable while editing>";
}
return view.getContext().getResources().getResourceEntryName(id);
}
private Utils() {
throw new AssertionError("No instances.");
}
}
然后在MainActivity稍作修改
public class MainActivity extends AppCompatActivity {
TextView mTextView;
Button mButton;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
MainActivity_ViewBinding binding = new MainActivity_ViewBinding(this);//重点
mTextView.setText("Activity1-butter-tv1");
mButton.setText("Activity1-butter-button1");
}
void buttonClick() {
Intent intent = new Intent(MainActivity.this, Activity2.class);
startActivity(intent);
}
@Override
protected void onDestroy() {
super.onDestroy();
}
}
就达到了butterknife的效果 只不过我们是写死的 那么下面就学习如何生成这些代码 让我们有一个新的id 也能自动添加到生成的文件里
3.ButterKnife动态生成版
架构如下
3.1 注解模块
build.gradle
plugins {
id 'java-library'
}
java {
sourceCompatibility = JavaVersion.VERSION_1_7
targetCompatibility = JavaVersion.VERSION_1_7
}
// 注释可以使用中文
tasks.withType(JavaCompile){
options.encoding='UTF-8'
}
注解类
package com.example.self_butterknife_annotations;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Created by hjcai on 2021/7/19.
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.CLASS)
public @interface BindView {
int value();
}
3.2 代码生成器模块
build.gradle
plugins {
id 'java-library'
}
java {
sourceCompatibility = JavaVersion.VERSION_1_7
targetCompatibility = JavaVersion.VERSION_1_7
}
// 引入Java Poet 用于生成代码
dependencies {
// 用于自动为 JAVA Processor 生成 META-INF 信息
implementation 'com.google.auto.service:auto-service:1.0-rc6'
// 指定当前module的processor
annotationProcessor 'com.google.auto.service:auto-service:1.0-rc6'
//快速生成.java文件的库
implementation 'com.squareup:javapoet:1.11.1'
// implementation 'com.google.auto:auto-common:0.10'
implementation project(path: ':self-butterknife-annotations')
}
// 注释可以使用中文
tasks.withType(JavaCompile){
options.encoding='UTF-8'
}
具体进行代码生成的类(核心)
需要参考第一节中生成的文件来编写代码
package com.example.self_butterknife_compiler;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import com.example.self_butterknife_annotations.BindView;
import com.google.auto.service.AutoService;
import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.JavaFile;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.TypeSpec;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Filer;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.Processor;
import javax.annotation.processing.RoundEnvironment;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;
import javax.lang.model.util.Elements;
@AutoService(Processor.class)
public class ButterKnifeProcessor extends AbstractProcessor {
private Filer mFiler;
private Elements mElementUtils;
// 初始化
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
mFiler = processingEnv.getFiler();
mElementUtils = processingEnv.getElementUtils();
}
/**
* 用来指定支持的 SourceVersion
*/
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
// 有注解就都会进process方法 这里是生成代码的核心
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
// System.out.println("===process===");
// System.out.println("annotations "+annotations);
// System.out.println("roundEnv "+roundEnv);
// 获取所有拥有注释BindView的变量
Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(BindView.class);
// 该map 存储的key是Activity的完整路径 value是其中被BindView标记的所有变量的list
// 例如 {com.example.client.MainActivity=[mTextView,mtv1]}
Map<Element, List<Element>> elementsMap = new LinkedHashMap<>();
for (Element element : elements) {
// 获取拥有注释BindView的变量位于哪个类 例如 com.example.client.MainActivity
Element enclosingElement = element.getEnclosingElement();
// 查看map中没有存储过该类
List<Element> viewBindElements = elementsMap.get(enclosingElement);
// 如果map中没有存储过该类 创建一个新的list 将Activity作为key list作为value 存储到map
if (viewBindElements == null) {
viewBindElements = new ArrayList<>();
elementsMap.put(enclosingElement, viewBindElements);
}
// 将拥有注释BindView的变量存储到list
viewBindElements.add(element);
}
// 遍历添加相关注解的Class 准备生成代码
for (Map.Entry<Element, List<Element>> entry : elementsMap.entrySet()) {
// 类的完整报名+类名
Element enclosingElement = entry.getKey();
// 类中存储的拥有注释BindView的变量集合
List<Element> viewBindElements = entry.getValue();
// 从完整包名+类名获取简短类名 例如 从 com.example.client.MainActivity 得到 MainActivity
String activityClassNameStr = enclosingElement.getSimpleName().toString();
ClassName activityClassName = ClassName.bestGuess(activityClassNameStr);
// 用包名+类名创建接口的className
// 目前是hardcode的包名
ClassName unbinderClassName = ClassName.get("com.example.self_butterknife", "Unbinder");
// 创建的类名为 类名+_ViewBinding
TypeSpec.Builder classBuilder = TypeSpec.classBuilder(activityClassNameStr + "_ViewBinding")
// 该类是public final的
.addModifiers(Modifier.FINAL, Modifier.PUBLIC)
// 实现了一个接口
.addSuperinterface(unbinderClassName)
// 创建一个activityClassName的实例 私有对象 变量名为target
.addField(activityClassName, "target", Modifier.PRIVATE);
/*****unbind 方法实现 start *****/
// 利用包名+类名 构建 android.support.annotation.CallSuper的ClassName
ClassName callSuperClassName = ClassName.get("androidx.annotation", "CallSuper");
// 创建unbind方法
MethodSpec.Builder unbindMethodBuilder = MethodSpec.methodBuilder("unbind")
// 给unbind方法加上Override注解
.addAnnotation(Override.class)
// 给unbind方法加上CallSuper注解
.addAnnotation(callSuperClassName)
// unbind方法是public final的
.addModifiers(Modifier.PUBLIC, Modifier.FINAL);
// 方法内部添加 一行内容
unbindMethodBuilder.addStatement("$T target = this.target", activityClassName);
unbindMethodBuilder.addStatement("if (target == null) throw new IllegalStateException(\"Bindings already cleared. target is null! \");");
/*****unbind方法实现 end *****/
// 创建构造函数
MethodSpec.Builder constructorMethodBuilder = MethodSpec.constructorBuilder()
// 给构造方法添加一个参数类型为activityClassName 值为target的参数
.addParameter(activityClassName, "target")
// 构造方法是public的
.addModifiers(Modifier.PUBLIC)
// 构造方法添加一行代码 this.target = target;
.addStatement("this.target = target");
// 遍历添加相关注解的变量
for (Element viewBindElement : viewBindElements) {
// 获得变量名称
String filedName = viewBindElement.getSimpleName().toString();
// 获取Utils的ClassName
ClassName utilsClassName = ClassName.get("com.example.self_butterknife", "Utils");
// 获取注解的值 即view的id
int resId = viewBindElement.getAnnotation(BindView.class).value();
// TODO 根据id的int值反推其资源id
// 在构造方法生成 类似 target.textView1 = Utils.findViewById(target, R.id.tv1); 的代码
constructorMethodBuilder.addStatement("target.$L = $T.findViewById(target, $L)", filedName, utilsClassName, resId);
// 在unbind方法生成类似 target.textView1 = null;的语句
unbindMethodBuilder.addStatement("target.$L = null", filedName);
}
// 加入两个方法
classBuilder.addMethod(unbindMethodBuilder.build());
classBuilder.addMethod(constructorMethodBuilder.build());
// 生成类,看下效果
try {
// 根据完整包名+类名得到包名
String packageName = mElementUtils.getPackageOf(enclosingElement).getQualifiedName().toString();
// 文件创建的位置在packageName下
JavaFile.builder(packageName, classBuilder.build())
// Java文件 头部添加注释添加
.addFileComment("由ButterKnifeProcessor自动生成 请勿修改")
.build()
// 写文件
.writeTo(mFiler);
} catch (IOException e) {
e.printStackTrace();
System.out.println("发生异常...");
}
}
return false;
}
// 哪些注解支持自动生成代码
@Override
public Set<String> getSupportedAnnotationTypes() {
Set<String> types = new LinkedHashSet<>();
for (Class<? extends Annotation> annotation : getSupportedAnnotations()) {
types.add(annotation.getCanonicalName());
}
return types;
}
// 支持自动生成文件的Annotation(需要解析的自定义注解 例如 BindView OnClick)
private Set<Class<? extends Annotation>> getSupportedAnnotations() {
Set<Class<? extends Annotation>> annotations = new LinkedHashSet<>();
// 需要解析的自定义注解 BindView OnClick
annotations.add(BindView.class);
return annotations;
}
}
3.3 工具类模块
build.gradle
plugins {
id 'com.android.library'
}
android {
compileSdkVersion 30
buildToolsVersion "30.0.3"
defaultConfig {
minSdkVersion 24
targetSdkVersion 30
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles "consumer-rules.pro"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
dependencies {
implementation 'androidx.appcompat:appcompat:1.3.0'
implementation 'com.google.android.material:material:1.4.0'
testImplementation 'junit:junit:4.+'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
}
findView工具类
package com.example.self_butterknife;
import android.app.Activity;
import android.view.View;
/**
* Created by hjcai on 2021/7/19.
*/
public class Utils {
public static <T extends View> T findViewById(Activity activity, int viewId) {
return activity.findViewById(viewId);
}
}
反注册接口
package com.example.self_butterknife;
import androidx.annotation.UiThread;
/**
* Created by hjcai on 2021/7/19.
*/
public interface Unbinder {
@UiThread
void unbind();
Unbinder EMPTY = () -> {
};
}
ButterKnife Binding类实例化的类
package com.example.self_butterknife;
import android.app.Activity;
import java.lang.reflect.Constructor;
/**
* Created by hjcai on 2021/7/19.
*/
public class ButterKnife {
public static Unbinder bind(Activity activity) {
if (activity == null) {
throw new IllegalArgumentException(" activity should not be null");
}
try {
Class<? extends Unbinder> bindClassName = (Class<? extends Unbinder>)
Class.forName(activity.getClass().getName() + "_ViewBinding");
// 构造函数
// 通过反射调用 类似MainActivity_ViewBinding bind = new MainActivity_ViewBinding(this); 的构造方法
Constructor<? extends Unbinder> bindConstructor = bindClassName.getDeclaredConstructor(activity.getClass());
// 返回 Unbinder
return bindConstructor.newInstance(activity);
} catch (Exception e) {
e.printStackTrace();
}
return Unbinder.EMPTY;
}
}
3.4 客户端模块
build.gradle
plugins {
id 'com.android.application'
}
android {
compileSdkVersion 30
buildToolsVersion "30.0.3"
defaultConfig {
applicationId "com.example.self_butterknife_client"
minSdkVersion 24
targetSdkVersion 30
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
dependencies {
implementation 'androidx.appcompat:appcompat:1.3.0'
implementation 'com.google.android.material:material:1.4.0'
implementation project(path: ':self-butterknife')
implementation project(path: ':self-butterknife-annotations')
annotationProcessor project(path: ':self-butterknife-compiler')
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
}
MainActivity
package com.example.client;
import android.os.Bundle;
import android.widget.Button;
import android.widget.TextView;
import androidx.appcompat.app.AppCompatActivity;
import com.example.self_butterknife.ButterKnife;
import com.example.self_butterknife.Unbinder;
import com.example.self_butterknife_annotations.BindView;
import com.example.self_butterknife_client.R;
public class MainActivity extends AppCompatActivity {
@BindView(R.id.tv1)
TextView mTextView;
@BindView(R.id.button1)
Button mButton;
Unbinder unbinder;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
unbinder = ButterKnife.bind(this);
mTextView.setText("Activity1-butter-tv1");
mButton.setText("Activity1-butter-button1");
}
// void buttonClick() {
// Intent intent = new Intent(MainActivity.this, Activity2.class);
// startActivity(intent);
// }
@Override
protected void onDestroy() {
super.onDestroy();
unbinder.unbind();
}
}
3.5 最终生成的类
// 由ButterKnifeProcessor自动生成 请勿修改
package com.example.client;
import androidx.annotation.CallSuper;
import com.example.self_butterknife.Unbinder;
import com.example.self_butterknife.Utils;
import java.lang.Override;
public final class MainActivity_ViewBinding implements Unbinder {
private MainActivity target;
public MainActivity_ViewBinding(MainActivity target) {
this.target = target;
target.mTextView = Utils.findViewById(target, 2131231125);
target.mButton = Utils.findViewById(target, 2131230808);
}
@Override
@CallSuper
public final void unbind() {
MainActivity target = this.target;
if (target == null) throw new IllegalStateException("Bindings already cleared. target is null! ");;
target.mTextView = null;
target.mButton = null;
}
}
后记:
本节难点不多 耗时最长的是一直在查AbstractProcessor的process不执行的原因。因为Android Studio以及Gradle版本的变化 很多gradle的写法都发生了改变 需要注意的是
1.ButterKnifeProcessor头部的注解@AutoService(Processor.class)
2.compiler模块自身com.google.auto.service:auto-service:1.0-rc6要写2次,个人理解一次是引用其中的类 另一次是指定当前module的processor
// 用于自动为 JAVA Processor 生成 META-INF 信息
implementation 'com.google.auto.service:auto-service:1.0-rc6'
// 指定当前module的processor
annotationProcessor 'com.google.auto.service:auto-service:1.0-rc6'
3.client对于compiler模块也是有依赖的
annotationProcessor project(path: ':self-butterknife-compiler')
另外当前的code还有很多提升的地方
比如ButterKnifeProcessor中有很多包名是hardcode的
比如生成的代码中id的值是一个很长的int值而不是一个R.id.xxx类似的规范id
比如目前只支持findviewbyid,还不能支持点击事件
不过这个javapoet生成代码的技术确实比较有趣 应该还能做其他很多事情