三分钟的介绍
相信很多人都在开发中都使用过ButterKnife吧!没有用过的也都听过。ButterKnife是一个专注于Android系统的View注入框架,以前总是要写很多findViewById来找到View对象,有了ButterKnife可以很轻松的省去这些步骤。
说说人家的优点
简化代码,提升开发效率
强大的View绑定和Click事件处理功能不会影响app运行效率
ButterKnife采用编译时注解的方式生成代码,运行是不会影响App效率
用法
GitHub地址(Star 25.1k):https://github.com/JakeWharton/butterknife
class ExampleActivity extends Activity {
@BindView(R.id.user) EditText username;
@BindView(R.id.pass) EditText password;
@BindString(R.string.login_error) String loginErrorMessage;
@OnClick(R.id.submit) void submit() {
// TODO call server...
}
@Override public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.simple_activity);
ButterKnife.bind(this);
// TODO Use fields...
}
}
原理
仅仅一个注解加一行代码可以实现 findViewById
,它们都做了哪些事情呢?
1、给元素加注解标记
2、收集注解的元素生成Java类(编译器执行)
3、动态注入
源码走一波
第一步:加注解没啥好说的,过
第二步:收集注解生成Java类
在编译时,通过处理注解元素,生成新的 Java 代码类,该Java代码 里面包含了我们的 findViewById(R.id.xxx)、view.setonclickListener(new lis... )的这些动作;
ButterKnifeProcessor.java(GitHub中的源码)
public boolean process(Set<? extends TypeElement> set, RoundEnvironment env) {
print("process:");
print("env"+env.getRootElements());
Map<TypeElement, List<FieldBinding>> map = new HashMap<>();
for (Element element : env.getElementsAnnotatedWith(BindView.class)) {
//get the Activity
TypeElement activityElement = (TypeElement) element.getEnclosingElement();
print(" activityElement:"+ activityElement.toString());
List<FieldBinding> list = map.get(activityElement);
if (list == null) {
list = new ArrayList<>();
map.put(activityElement, list);
}
//get id
int id = element.getAnnotation(BindView.class).value();
//get fieldName
String fieldName = element.getSimpleName().toString();
//get mirror
TypeMirror typeMirror = element.asType();
print(" typeMirror:"+ typeMirror);
FieldBinding fieldBinding = new FieldBinding(fieldName, typeMirror, id);
list.add(fieldBinding);
}
for (Map.Entry<TypeElement, List<FieldBinding>> item :
map.entrySet()) {
TypeElement activityElement = item.getKey();
//get packageName
String packageName = elementUtils.getPackageOf(activityElement).getQualifiedName().toString();
//get activityName
String activityName = activityElement.getSimpleName().toString();
//transfrom type Activity with system can discern
ClassName activityClassName = ClassName.bestGuess(activityName);
ClassName viewBuild = ClassName.get(ViewBinder.class.getPackage().getName(), ViewBinder.class.getSimpleName()); //
TypeSpec.Builder result = TypeSpec.classBuilder(activityClassName + "$$ViewBinder")
.addModifiers(Modifier.PUBLIC)
.addTypeVariable(TypeVariableName.get("T", activityClassName))
.addSuperinterface(ParameterizedTypeName.get(viewBuild,activityClassName));
MethodSpec.Builder method = methodBuilder("bind") //methodName
.addModifiers(Modifier.PUBLIC) // modifier
.returns(TypeName.VOID)
.addAnnotation(Override.class)
.addParameter(activityClassName, "target", Modifier.FINAL);
//
List<FieldBinding> list = item.getValue();
for (FieldBinding fieldBinding : list) {
//
String pacageName = fieldBinding.getType().toString();
ClassName viewClass = ClassName.bestGuess(pacageName);
method.addStatement("target.$L=($T)target.findViewById($L)", fieldBinding.getName(), viewClass, fieldBinding.getResId());
}
//
result.addMethod(method.build());
try {
JavaFile.builder(packageName, result.build()).build().writeTo(filer);
} catch (IOException e) {
e.printStackTrace();
}
}
return true;
}
第三步:就是动态注入, ButterKnife.bind(this);
源码中最后会通过反射加载一个***_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;
}
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;
}
瞧一瞧看一看MainActivity_ViewBinding .java
public class MainActivity_ViewBinding implements Unbinder {
private MainActivity target;
private View view7f070022;
@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.button, "field 'button' and method 'click'");
target.button = Utils.castView(view, R.id.button, "field 'button'", Button.class);
view7f070022 = view;
view.setOnClickListener(new DebouncingOnClickListener() {
@Override
public void doClick(View p0) {
target.click();
}
});
}
@Override
@CallSuper
public void unbind() {
MainActivity target = this.target;
if (target == null) throw new IllegalStateException("Bindings already cleared.");
this.target = null;
target.button = null;
view7f070022.setOnClickListener(null);
view7f070022 = null;
}
}
看到这里已经完全明白了,为什么只需要短短的两行代码了。。。
开启手撸模式(三步走 模式)
第一步:我们需要创建注解,在项目中New Module -- Java Library(Library name: injectAnnotations)
// 具体可以去了解一下注解的使用
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.SOURCE)
public @interface InjectView {
// 我们这里也使用了android提供的一些注解
@IdRes
int value();
}
官方提供了很多特别好用的类或注解,这里说的support annotation就是特别好的工具,多使用其中的注解,需要在gradle中加入
implementation 'com.android.support:support-annotations:25.2.0'
第二步:注解生成器,收集所有的注解,生成Java文件
在项目中New Module -- Java Library(Library name: injectCompiler)
思考一下,我们的注解Module需要提供给app使用,注解生成器Module也提供给app
那么我们需要在app的gradle中加入
implementation project(':injectAnnotations')
// annotationProcessor表示这是编译时的注解处理器
annotationProcessor project(':injectCompiler')
注解生成器Module也需要知道我都需要处理哪些注解,所以需要在gradle中引入inject-annotations
dependencies {
implementation fileTree(include: ['*.jar'], dir: 'libs')
implementation project(':injectAnnotations')
implementation "com.google.auto.service:auto-service:1.0-rc4"//自动配置的
annotationProcessor "com.google.auto.service:auto-service:1.0-rc4" //这个在gradle5.0以上需要的
implementation 'com.squareup:javapoet:1.11.1'//方便编写代码的
}
// 解决build 错误:编码GBK的不可映射字符
tasks.withType(JavaCompile) {
options.encoding = "UTF-8"
}
注解生成器配置OK了,接下来我们创建 ButterKnifeProcessor.class
@AutoService(Processor.class)
public class ButterKnifeProcessor extends AbstractProcessor {
private Filer filer;
private Elements mElementUtils;
// 使用之前需要初始化三个动作
// 1、支持的java版本
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
// 2、当前APT能用来处理哪些注解
@Override
public Set<String> getSupportedAnnotationTypes() {
Set<String> supportTypes = new LinkedHashSet<>();
supportTypes.add(InjectView.class.getCanonicalName());
supportTypes.add(InjectClick.class.getCanonicalName());
return supportTypes;
}
// 3、需要一个用来生产文件的对象
@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
super.init(processingEnvironment);
filer = processingEnvironment.getFiler();
mElementUtils = processingEnvironment.getElementUtils();
}
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
// 这里会把所有跟注解有关的field全部拿到,我们需要手动进行分类
Set<? extends Element> viewElements = roundEnvironment.getElementsAnnotatedWith(InjectView.class);
Set<? extends Element> clickElements = roundEnvironment.getElementsAnnotatedWith(InjectClick.class);
// 将所有注解集合分离出以activity为单元的注解集合
Map<Element, List<Element>> viewElementsMap = set2Map(viewElements);
// 将所有注解集合分离出以activity为单元,再以控件ID为单元的注解
Map<Element, List<Element>> clickElementsMap = set2Map(clickElements);
//------------生成代码,使用Java代码生成框架-JavaPoet解析-----------
for (Map.Entry<Element, List<Element>> entry : viewElementsMap.entrySet()) {
Element activityElement = entry.getKey();
List<Element> viewFieldElementList = entry.getValue();
//得到类名的字符串
String activityName = activityElement.getSimpleName().toString();
ClassName activityClassName = ClassName.bestGuess(activityName);
// 拼装这一行代码:public final class xxx_ViewBinding implements IBinder
ClassName targetTypeName = ClassName.get("com.demo.james","IBinder");
TypeSpec.Builder classBuilder = TypeSpec.classBuilder(activityName+"_ViewBinding")
//类名前添加public final
.addModifiers(Modifier.FINAL, Modifier.PUBLIC)
//添加类的实现接口,并指定泛型的具体类型
.addSuperinterface(ParameterizedTypeName.get(targetTypeName, activityClassName))
//添加一个成员变量target
.addField(activityClassName, "target", Modifier.PRIVATE);
// 实现IBinder的方法
// 拼装这一行代码:public final void bind(ButterknifeActivity target)
MethodSpec.Builder bindMethod = MethodSpec.methodBuilder("bind")//和你创建的bind中的方法名保持一致
.addAnnotation(Override.class)
.addParameter(activityClassName, "activity")
.addStatement("this.target = activity")
.addModifiers(Modifier.FINAL, Modifier.PUBLIC);
// 存储已findView的控件,为添加点击事件的时候判断是否需要重新findViewById
Map<Integer, String> findViewMap = new LinkedHashMap<>();
// 遍历注解的字段生成findViewById
for (Element fieldElement : viewFieldElementList) {
String fieldName = fieldElement.getSimpleName().toString();
//在构造方法中添加初始化代码
// 在bind方法中添加
// target.btn = target.findViewById(2131230762);
// 获取注解里面的值,也就是id
InjectView annotation = fieldElement.getAnnotation(InjectView.class);
int resId = annotation.value();
findViewMap.put(resId, fieldName);
bindMethod.addStatement("target.$L = target.findViewById($L)",fieldName,resId);
}
List<Element> clickFieldElementList = clickElementsMap.get(activityElement);
if (clickFieldElementList != null){
for (Element fieldElement : clickFieldElementList) {
System.out.println("clickFieldElementList : "+ fieldElement.getSimpleName().toString());
// 添加onCLickListener
// target.btn.setOnClickListener(new View.OnClickListener() {
// @Override
// public void onClick(View view) {
// target.test();
// }
// });
ClassName viewClass = ClassName.get("android.view","View");
TypeSpec onCLick = TypeSpec.anonymousClassBuilder("")
.superclass(ClassName.bestGuess("android.view.View.OnClickListener"))
.addMethod(MethodSpec.methodBuilder("onClick")
.addAnnotation(Override.class)
.addModifiers(Modifier.PUBLIC)
.addParameter(viewClass, "view")
.returns(void.class)
.addStatement("target.$L()", fieldElement.getSimpleName().toString())
.build())
.build();
InjectClick annotation = fieldElement.getAnnotation(InjectClick.class);
int resId = annotation.value();
if (findViewMap.get(resId) == null){
bindMethod.addStatement("target.findViewById($L).setOnClickListener($L)",resId,onCLick);
}else{
bindMethod.addStatement("target.$L.setOnClickListener($L)",findViewMap.get(resId),onCLick);
}
}
}
classBuilder.addMethod(bindMethod.build());
//开始生成
try {
//得到包名
String packageName = mElementUtils.getPackageOf(activityElement)
.getQualifiedName().toString();
JavaFile.builder(packageName,classBuilder.build())
//添加类的注释
.addFileComment("butterknife 自动生成")
.build().writeTo(filer);
} catch (Exception e) {
e.printStackTrace();
}
}
return false;
}
private Map<Element, List<Element>> set2Map(Set<? extends Element> viewElements) {
Map<Element, List<Element>> viewElementsMap = new LinkedHashMap<>();
for (Element fieldElement : viewElements) {
//element.getSimpleName()得到的是这个field注解名, Button btn; 输出 btn
System.out.println("field name : "+fieldElement.getSimpleName());
Element activityElement = fieldElement.getEnclosingElement();
//得到的是这个field所在类的类名
System.out.println("activityElement name : "+activityElement.getSimpleName());
//以类对象为key值存储一个类中所有的field到集合中
List<Element> elementList = viewElementsMap.get(activityElement);
if (elementList == null){
elementList = new ArrayList<>();
viewElementsMap.put(activityElement, elementList);
}
elementList.add(fieldElement);
}
return viewElementsMap;
}
}
第三步:动态注入,需要提供一个给用户使用的东东
public class JettButterKnife{
public static void bind(Activity activity){
String name = activity.getClass().getName() + "_ViewBinding" ;
try {
Class<?> clazz = Class.forName(name);
IBinder iBinder = (IBinder) clazz.newInstance();
iBinder.bind(activity);
}catch (Exception e){
e.printStackTrace();
}
}
}
public class ButterknifeActivity extends AppCompatActivity {
@InjectView(R.id.button6)
Button btn;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_butterknife);
JettButterKnife.bind(this);
btn.setText("我要赋值咯!。。。。");
}
@InjectClick(R.id.button8)
public void test(){
Toast.makeText(this, "点的就是我", Toast.LENGTH_LONG).show();
}
}
基本上都已经注释了,自己手撸一个BufferKnife,实现了findViewById与onClick的注解功能。
总结
一晃已经凌晨两点了,熬不牢!!!
确实一个插件需要考虑的事情非常多,不动手去做是想不到的。之前只是实现了findViewById,但是要正在加onClick的时候,还需要考虑更多。代码中还有很多验证的地方没有去做,比如:一个ID多个注解、ID的有效性等等,代码还存在很多优化的地方,今天就到这里了,有问题可以留言一起探讨探讨。。。