说到Bufferknife,相信基本都用过。在Activity中使用:
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...
}
}
这样的话 我们就初始化绑定了View,那么其内部是什么原理呢? 我相信很多小伙伴都能说出来:注解+反射,但是具体怎么实现的,懵逼了吧?= - = ? 看完本篇博客我相信你也可以自己写一个。
上面的四个知识点是我们手写Bufferknife比需要掌握和理解的,下面我们一个一个讲解
apt (annotationProcessor)
ATP是一种处理注解的工具,它对源代码文件进行检测找出其中的Annotation,根据注解自动生成代码。 这里的源代码文件就是我们自己的包含@BindView注解的XXXActivity或者XXX Fragment。
annotationProcessor是APT工具的一种,是Google开发的内置框架,不需要特殊引入,比如我们是不是在引入Bufferknife的时候需要加入:
annotationProcessor 'com.jakewharton:butterknife-compiler:10.2.0'
这就表明,要自动检测我们的代码从而在编译的时候生成代码类。生成什么代码类呢?
public class MainActivity$ViewBinder implements ViewBinder
也就是生成类似上面的代码类,这些自动生成的类的功能就是:findViewById 和 setOnClickListener。 好,那么生成这些类有什么用处呢? 试想一下,如果我们把这个类和我们的自己的XXXActivity关联起来,
也就是说让这些生成的类代替XXXActivity执行findViewById的操作,那么是不是就相当于实现了Bufferknife呢? 我想说是的!
好,现在总结一点:就是说,我们如果有我们自己的注解处理器,然后在我们的工程中引入:
annotationProcessor XXXX
这样的话,就可以自动扫描我们代码中使用该注解的Java文件,最后自动在生成Java文件。
我们创建了注解BindView, 然后在XXXActivity中使用该注解,通过APT在编译器的扫描,那最后可以产出Java文件,而这个Java文件是我们自己定义创建的。
好,这样的话APT我们知道了,上图中的注解是怎么定义的呢?
注解
说到注解我们肯定很熟悉Override吧? 看下源代码
package java.lang;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}
这是一个标准的注解定义,首先Retention定义了注解的“生命周期”,也就是起作用的时间段:只在源码阶段保留,在编译器进行编译时它将被丢弃忽视。
其次Target定义了该注解要用在什么用途?换句话说就是:这个注解要用在字段?类?方法?接口?枚举?
说到Retention,分三种类型:
RUNTIME是运行时注解,在程序运行的时候才起作用,然后在程序运行的时候,再通过反射,所具体的操作。
比如:
/**
* 绑定View
* @param object
* @param view
*/
private static void bindView(Object object, View view) {
Field[] declaredFields = object.getClass().getDeclaredFields();
for(Field field : declaredFields) {
boolean annotationPresent = field.isAnnotationPresent(BindView.class);
if(!annotationPresent) {
return;
}
BindView fieldAnnotation = field.getAnnotation(BindView.class);
field.setAccessible(true);
try {
field.set(object, view.findViewById(fieldAnnotation.resIdValue()));
//view设置点击事件
bindClick(object, view);
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
这样的话,我们是不是可以想一下,RUNTME 是在运行的时候在起作用,是不是多多少少会消耗一点性能?! 答案是肯定的。对于寸土寸金的移动端,是不能容忍的!
那么能不能在编译的时候就把代码生成好,然后在运行的时候直接用呢? 答案还是肯定的!RetentionPolicy.CLASS华丽出场!
RetentionPolicy.CLASS作用于编译时期,那么也就是说作用域字节码上。然后配合注解处理器,生成我们想要的Java文件。
小提示:据说Bufferknife早期用的是运行时注解,但是我们都知道啦,用的是编译时注解。 = - =
好,说完了注解的存活时间,接下来聊聊Target;
这个意思顾名思义就是:“目标”,这个注解可以用在哪里? 哪里?
在Override注解定义的是:METHOD,也就是Override只能用于方法上,其它种类的使用,各位小伙伴可以自己试一试。
提问:我们要手写Bufferknife,那么我们的注解BindView和onClick应该怎么定义呢?
提示:都是编译时;一个是用在Field上,一个是用在Method上。
好,既然注解存活时间和目标我们已经选好了,那么怎么处理这些注解呢? 这是我们的注解处理器闪亮登场。
注解处理器
注解处理器是Javac的一个工具,会在编译期间扫描和处理注解。通过自定义注解处理器,可以自定义处理标注了类,方法,字段等注解。然后可以通过JavaPoet生成自定义的Java类。空口无凭,举个栗子?
/**
* author: liumengqiang
* Date : 2019/10/27
* Description :
*/
public class TestProcessor extends AbstractProcessor {
@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
super.init(processingEnvironment);
/**
* ProcessingEnvironment提供很多有用的工具类Elements, Types 和 Filer,用来访问工具框架的环境
*/
}
@Override
public boolean process(Set extends TypeElement> set, RoundEnvironment roundEnvironment) {
/**
* TODO 处理注解
*/
return false;
}
@Override
public Set getSupportedAnnotationTypes() {
Set annotataions = new LinkedHashSet();
annotataions.add(MyAnnotation.class.getCanonicalName());
return annotataions;
}
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
}
上面就是一个标准的自定义注解处理器,首先我们需要继承AbstractProcessor, 然后重写四个方法
说到注解处理器,那么怎么注册注解处理器呢?
方案1
卧槽,怎么这么复杂? 有没有简单的方法?答案是有的!
方案2
直接使用Google提供的Auto-service注解,纳:
@AutoService(Processor.class)
public class TestProcessor extends AbstractProcessor{
.......
}
就这就注册了,然后省去了很多步骤,因为这个注解会帮我们执行方案1的步骤。
好,说完了注解处理器和注册处理器,接下来说一下,在process方法中生成Java类。
JavaPoet
Github 传送门
实际上JavaPoet,也就是Java诗人,也就是通过写代码方式,然后自动根据规则生成Java类,这个网上太多博客了,并且人家写的太优秀了 = - =。
JavaPoet简介
JavaPoet的使用指南
我只说一点吧,Element元素,是一个接口,有五个子接口:
举个??那就走一个!生成如下的方法:
/**
* author: liumengqiang
* Date : 2019/10/27
* Description :
*/
public class MakeMainJava {
public static void main(String[] args) {
//第一步 生成main函数
MethodSpec main = MethodSpec.methodBuilder("main")
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
.returns(void.class)
.addParameter(String[].class, "args")
.addStatement("$T.out.println($S)", System.class, "Hello, JavaPoet!")
.build();
//第二步 生成类
TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld")
.addModifiers(Modifier.PUBLIC, Modifier.FINAL)
.addMethod(main)
.build();
//第三步 生成java文件对象
JavaFile javaFile = JavaFile.builder("com.liumengqiang.helloworld", helloWorld).build();
//第四步 输出到控制台
try {
javaFile.writeTo(System.out);
} catch (IOException e) {
e.printStackTrace();
}
}
}
然后打印在控制台:
好了,基本知识点完了之后,叉腰歇会儿。。。
想一下,我们使用BufferKnife的时候,肯定用到了@BindView注解,首先我们需要定义@BindView注解,那么我们新建一个Javalibrary专门存放注解; 然后是不是还得有个注解处理器?我们在新建一个JavaLibrary专门用于存放注解处理器。
再看一下Bufferknife生成的Java代码是不是继承自一个接口,也就是说所有生成的Java代码都得实现该接口,也就是说规范生成的代码。所以我们也需要约束我们生成的Java类规范。 创建一个apiLibrary, 这是个Android的library,然后定义接口规范。
总体的项目结构如下:
app : 测试module
annotations:存放注解
handleAnnotations:存放注解处理器
applibrary: 存放约束规范接口
他们的依赖关系:
整体项目架构搭建好了,那么先定义注解:
/**
* FileName:BindView
* Create By:liumengqiang
* Description:TODO
*/
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface BindView {
int value() default -1;
}
注意是:作用在字段上 & 编译时。注解定义好了 我们需要创建注解处理器。在书写注解处理器之前,我们先理一下思路: 一个类中可能有多个注解字段;每个类都是不相关的。基于这两点,我们得出结论:在注解处理器中,我们可以创建一个HashMap,然后key:类;value:注解字段。
/**
* FileName:BindViewHandle
* Create By:liumengqiang
* Description:TODO
*/
@AutoService(Processor.class)
public class BindViewHandle extends AbstractProcessor {
private Elements elementUtils; //
private Filer filer; //
//key:包含注解的类; value:注解的字段对象
private HashMap annotationClassHashMap;
@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
super.init(processingEnvironment);
elementUtils = processingEnvironment.getElementUtils();
filer = processingEnvironment.getFiler();
annotationClassHashMap = new HashMap<>();
}
@Override
public boolean process(Set extends TypeElement> set, RoundEnvironment roundEnvironment) {
//需要每次都清空map集合,因为process在扫描过程中可能进入多次,然后多次生成Java类
annotationClassHashMap.clear();
//创建AnnotationClass对象,该对象代表一个类。这个类对象塞入注解的字段信息
for (Element element : roundEnvironment.getElementsAnnotatedWith(BindView.class)) {
TypeElement typeElement = (TypeElement) element.getEnclosingElement();
AnnotationClass annotationClass = getAnnotationClass(typeElement);
BindViewField bindViewField = new BindViewField(elementUtils.getPackageOf(typeElement).getQualifiedName().toString(), element, element.getAnnotation(BindView.class).value());
annotationClass.addField(bindViewField);
}
//再过滤onClick注解,塞入上面创建的AnnotationClass对象中
for(Element element : roundEnvironment.getElementsAnnotatedWith(onClick.class)) {
TypeElement typeElement = (TypeElement) element.getEnclosingElement();
ExecutableElement executableElement = (ExecutableElement) element;
AnnotationClass annotationClass = annotationClassHashMap.get(typeElement.getQualifiedName().toString());
if(annotationClass != null) {
annotationClass.setMethodAnnotation((ExecutableElement) element, executableElement.getAnnotation(onClick.class).value());
} else {
throw new IllegalArgumentException("需要设置BindView注释!");
}
}
//根据创建的AnnotationClass生成Java类
for (AnnotationClass annotationClass : annotationClassHashMap.values()) {
try {
annotationClass.generateJavaFile().writeTo(filer);
} catch (IOException e) {
e.printStackTrace();
}
}
return false;
}
private AnnotationClass getAnnotationClass(TypeElement typeElement) {
String qualifiedName = typeElement.getQualifiedName().toString();
AnnotationClass annotationClass = annotationClassHashMap.get(qualifiedName);
if (annotationClass == null) {
annotationClass = new AnnotationClass(elementUtils.getPackageOf(typeElement).getQualifiedName().toString(), typeElement);
annotationClassHashMap.put(qualifiedName, annotationClass);
}
return annotationClass;
}
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
@Override
public Set getSupportedAnnotationTypes() {
Set set = new LinkedHashSet();
//扫描处理BindView和onClick注解
set.add(BindView.class.getCanonicalName());
set.add(onClick.class.getCanonicalName());
return set;
}
}
生成的最终Java类:
public class MainActivity$ViewBinder implements ViewBinder, OnClickListener {
MainActivity host;
@Override
public void bindView(MainActivity host, Object o, ViewFinder viewFinder) {
this.host = host;
host.textView = (TextView)(viewFinder.findView(o, 2131165265));
host.textView.setOnClickListener(this);
host.button = (Button)(viewFinder.findView(o, 2131165264));
host.button.setOnClickListener(this);
}
@Override
public void unBindView(MainActivity host) {
host.textView = null;
host.button = null;
host = null;
}
@Override
public void onClick(View v) {
switch (v.getId()) {
case 2131165265:
host.onClick(v);
break;
case 2131165264:
host.onClick(v);
break;
default:
}
}
}
至于具体的代码demo我会附上地址。
现在注解处理器我们有了,接下来是不是该使用注解处理器?
把我们的app module 依赖 handleAnnotations:
annotationProcessor project(':handleAnnotations')
然后二者是关联了,在MainActivity中使用@BindView也能生成相应的Java代码。但是,但是,但是二者有关联么? 答案是肯定的,我们根本都没用到生成的MainActivity$ViewBinder类。怎么用呢? 我们需要用到反射!
首先类是有了,但是我们可以根据我们生成该 Java类的命名规则反射获得该类。
/**
* author: liumengqiang
* Date : 2019/10/26
* Description :
*/
public class MyButterKnife {
private static final ActivityViewFinder activityViewFinder = new ActivityViewFinder();
private static final FragmentViewFinder fragmentViewFinder = new FragmentViewFinder();
public static final HashMap map = new HashMap<>();
/**
* Activity
* @param activity
*/
public static void init(Activity activity) {
init(activity, activity, activityViewFinder);
}
/**
* Fragment
* @param fragment
* @param view
*/
public static void init(Fragment fragment, View view) {
init(fragment, view, fragmentViewFinder);
}
public static void init(Object host, Object o, ViewFinder viewFinder) {
String hostName = host.getClass().getName();
ViewBinder hostViewBinder = map.get(hostName);
if(hostViewBinder == null) {
try {
//通过反射获取到该class
Class> aClass = Class.forName(hostName + "$ViewBinder");
然后创建该类对象
hostViewBinder = (ViewBinder) aClass.newInstance();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
}
//将对象存入map中
map.put(hostName, hostViewBinder);
}
//执行该类的bindView方法,完成绑定。
hostViewBinder.bindView(host, o, viewFinder);
}
public static void unBind(Object host) {
String hostName = host.getClass().getName();
ViewBinder viewBinder = map.get(hostName);
viewBinder.unBindView(host);
map.remove(hostName);
}
}
首先我们创建Map的构想是:希望第一次我们反射获取该类,但是之后不再通过反射,而是通过从map中获取,达到节约资源的目的。
上述代码的过程就是我们通过反射创建xxx$ViewBinder,然后根据IOC编程思想,直接调用接口的方法即可达到调用实现类的方法。
看下效果?行,看下效果。
至此我们的BufferKnife就写完了,实际上这篇博客不是跟你讲解代码,而是说一下怎么写出Bufferknife,重点是思路,以及那些知识点构成了BufferKnife。
对了得感谢一下:感谢享学Zero大佬指导。
纯手打,如果有错,希望小伙伴指正。
附上GitHub Demo :Demo 传送门
附上GitHub Demo :Demo 传送门