ButterKnife的光辉历史
ButterKnife是JakeWharton大神开源的一个库,主要作用是把Android View和回调绑定到属性和方法上面,在ViewBinding出现之前是一个很好的库,在性能和易用性方面是一个毋庸置疑的库,应用范围比较广泛。虽然现在已经废弃,但是其中的技术和理念对我们的软件研发人员还是有借鉴意义。很多方面都是相通的,大道至简同样也是这个道理。
最简单实现View绑定的方法
findViewById是查找View最初的方法,可以使用运行时注解在Activity的onCreate方法使用反射来实现Android View和属性的绑定。运行时注解的定义如下:
java
package com.example.simplebutterknife;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME) //注解保留范围,SOURCE代表代码级别(Annotation Processor可以使用),CLASS代表编译器级别 , RUNTIME代码VM级别(反射可以使用)
@Target(ElementType.FIELD) //注解使用范围
public @interface ReflectBindView {
int value();
}
注意三点:
- 因为使用反射实现,注解定义的Retention保留范围应该是RUNTIME,代表VM运行时可以得到相应的注解。
- 设置Target代表注解的使用范围,FIELD代表Java类的属性。
- 可以有一个value()方法,方便在注解中增加元数据。
反射相关的代码如下:
ini
public static void bind(Activity activity) {
Class clazz = activity.getClass();
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
if (field.isAnnotationPresent(ReflectBindView.class)) {
ReflectBindView annotation = field.getAnnotation(ReflectBindView.class);
int resId = annotation.value();
try {
Method method = clazz.getMethod("findViewById", int.class);
Object invoke = method.invoke(activity, resId);
field.setAccessible(true);
field.set(activity, invoke);
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
}
ReflectBinnView注解在相关的Activity中的定义,代码如下:
python
@ReflectBindView(R.id.tv_hello)
TextView mHelloTv;
@ReflectBindView(R.id.tv_name)
TextView mNameTv;
大体思路是获取Activity的所有属性,然后逐个判断属性是否是ReflectBindView注解,如果是此注解就通过value()方法获取设定的值,然后通过反射findViewById获取View值,最后通过field设置刚刚获取的view值。
因为运行时注解须要在Activity初始化中进行绑定操作,调用了大量反射相关代码,在界面复杂的状况下,使用这种方法就会严重影响Activity初始化效率。
ButterKnife使用了更高效的方式------Annotation Processor来完成这一工作,Pluggable Annotation Processing API是JDK6新增的注解处理API。那么什么是Annotation Processor呢?
ButterKnife如何实现View绑定
Annotation Processor使用 javapoet生成Java文件。 Android整个编译过程就是 source(源代码) -> processor(处理器) -> generate (文件生成)-> javacompiler -> .class 文件 -> .dex(只针对安卓)。Annotation Processor执行时机在Android项目编译的第一步,这样生成的Java代码才最终会编译到Android apk软件中。 ButterKnife使用的是源码级注解,代码如下:
java
package com.example.lib_annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.FIELD)
public @interface BindView {
int value();
}
Annotation Processor处理流程如下图所示: 实现的AbstractProcessor代码如下:
java
package com.example.lib;
import java.io.IOException;
import java.util.Collections;
import java.util.Set;
import com.example.lib_annotation.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 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.annotation.processing.SupportedSourceVersion;
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.tools.Diagnostic;
//@SupportedAnnotationTypes("com.example.lib_annotation.BindView")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
@AutoService(Processor.class)
public class BindingProcessor extends AbstractProcessor {
private Filer filer;
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
filer = processingEnv.getFiler();
}
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
System.out.println("BindingProcessor start");
for (Element element : roundEnv.getRootElements()) {
String packageStr = element.getEnclosingElement().toString();
String classStr = element.getSimpleName().toString();
ClassName className = ClassName.get(packageStr, classStr + "$Binding");
MethodSpec.Builder constructorBuilder = MethodSpec.constructorBuilder()
.addModifiers(Modifier.PUBLIC)
.addParameter(ClassName.get(packageStr, classStr), "activity");
boolean hasBinding = false;
for (Element e: element.getEnclosedElements()) {
BindView bindView = e.getAnnotation(BindView.class);
if (bindView != null) {
hasBinding = true;
constructorBuilder.addStatement
("activity.$L = activity.findViewById($L)", e.getSimpleName()
, bindView.value());
}
}
TypeSpec buildClass = TypeSpec.classBuilder(className)
.addModifiers(Modifier.PUBLIC)
.addMethod(constructorBuilder.build()
)
.build();
if (hasBinding) {
try {
JavaFile.builder(packageStr, buildClass)
.build()
.writeTo(filer);
} catch (IOException e) {
e.printStackTrace();
}
}
}
processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, "BindingProcessor start process annotation");
return false;
}
@Override
public Set<String> getSupportedAnnotationTypes() {
System.out.println("BindingProcessor getSupportedAnnotationTypes");
return Collections.singleton(BindView.class.getCanonicalName());
}
}
需要注意几点:
- AbstractProcessor中需要重写的getSupportedAnnotationTypes方法定义的是需要此注解处理器处理的注解的名称。
- 生成的Java代码的类一般和我们手动创建的类的名称做区分,比如说添加$Binding作为类名称后缀。
- Javapoet的具体如何使用可以到它的网站进行了解熟悉。
在子项目中配置annotationProcessor,如:
java
annotationProcessor project(':lib') //AbstractProcessor所在项目
implementation project(':lib-annotation') //BindView注解所在项目
在Activity中的属性设置注解的值,代码如下:
python
@BindView(R.id.tv_hello)
TextView mHelloTv;
@BindView(R.id.tv_name)
TextView mNameTv;
重新编译项目,或者运行Android项目能够执行生成Java代码的逻辑。重新编译项目如下图所示:
生成的Java代码如下图所示:
Activity如何使用上面想成的Java代码呢,自然而然就是反射,代码如下:
java
package com.example.simplebutterknife;
import android.app.Activity;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
public class ProcessBindViewUtil {
public static void bind(Activity activity) {
try {
Class bindClass = Class.forName(activity.getClass().getCanonicalName() + "$Binding");
Class activityClass = activity.getClass();
Constructor constructor = bindClass.getConstructor(activityClass);
constructor.newInstance(activity);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
}
}
}
在Activity中的onCreate方法中,在调用setContentView() 之后调用ProcessBindViewUtil.bind(this)方法就可以完成了。
参考资料:github.com/caiweihao/S...
致谢
希望文章对大家有所帮助,如果文章有所纰漏请不吝指教,大家共同进步。欢迎关注"技术蔡"的公众号,此公众号也是本作者的技术相关的公众号。之后会发布一系列Android相关的文章,可能会涉及到视频编解码,手机录屏,JNI,ASM字节码处理,hook等技术的应用,希望对大家有所帮助。