引言
前两天在掘金上看到一篇文章: 一个小需求引发的思考。 需求是根据多个EditText是否有输入的值来确定Button是否可点击。很常见的一个需求吧,但是要怎么做到简单优雅呢?文章里也有讲到封装的过程,这里我就不细讲了。最后作者封装成了用注解就可以做到。但是他全部用的反射,反射技术对APP性能影响还是很大的,作者最后也提到想使butterknife使用的技术。用大家都知道著名的注解框架 butterknife是使用了动态生成java代码的技术,这对性能影响非常小,我就在想我是不是也可以试试呢,正好学习一下butterknife原理以及用到的技术。于是说干就干,就有了下面的尝试!(我也是在查阅了许多资料后学习中摸索的,如果有写的不对的地方请大神指正。另外第一次写博客,排版什么的可能不是很美观。哈哈哈,想把自己的学习心得分享出来。慢慢进步吧!)
分析
首先要知道butterknife使用了什么技术,那就得阅读源码,网上搜一搜文章一大堆哈。这就不废话了哈哈哈。其实最重要的技术点就两个:
- 怎么解析处理注解
- 怎么动态生成java代码文件
阅读源码得知前者使用了 AndroidAnnotations框架,后者则使用了Javapoet框架。这里简单介绍一下吧。
准备工作
AndroidAnnotations
AndroidAnnotations是一个javax的注解解析技术。我们可以通过继承javax.annotation.processing.AbstractProcessor这个类来定义一个自己的注解处理类。(由于Android已经不支持javax编程了,所以需要在一个java lib 中来写)。
@AutoService(Processor.class)
public class TestProcessor extends AbstractProcessor {
/**
* 每一个注解处理器类都必须有一个空的构造函数。然而,这里有一个特殊的init()方法,它会被注解处理工具调用,
* 并输入ProcessingEnviroment参数。ProcessingEnviroment提供很多有用的工具类Elements,Types和Filer。
* @param processingEnvironment
*/
@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
super.init(processingEnvironment);
}
/**
* 这相当于每个处理器的主函数main()。 在这里写扫描、评估和处理注解的代码,以及生成Java文件。
* 输入参数RoundEnviroment,可以让查询出包含特定注解的被注解元素。
* @param set
* @param roundEnvironment
* @return
*/
@Override
public boolean process(Set extends TypeElement> set, RoundEnvironment roundEnvironment) {
return false;
}
/**
* 这个注解处理器是注册给哪个注解的。注意,它的返回值是一个字符串的集合,
* 包含本处理器想要处理的注解类型的合法全称。换句话说,在这里定义你的注解处理器注册到哪些注解上。
* @return
*/
@Override
public Set getSupportedAnnotationTypes() {
return super.getSupportedAnnotationTypes();
}
/**
* 返回支持的java版本
* @return
*/
@Override
public SourceVersion getSupportedSourceVersion() {
return super.getSupportedSourceVersion();
}
}
复制代码
上面的代码中通过注释应该了解每一个方法的作用了。我们处理的主要方法就是process()方法了。 另外这个类上面还有一个注解@AutoService(Processor.class)
它是干嘛的呢?是这样的:在以往的定义注解解析器时,需要在解析器类定义过程中,做以下操作: 在解析类名前定义: @SupportedAnnotationTypes("com.bosssoft.cloin.ViewInjectProcesser")
@SupportedSourceVersion(SourceVersion.RELEASE_7)
同时在java的同级目录新建resources目录,新建META-INF/services/javax.annotation.processing.Processor
文件,文件中填写你自定义的Processor
全类名,这是向JVM声明解析器。
当然幸好我们现在使用的是AndroidStudio,可以用auto-service来替代以上操作。只要在注解类前面加上@AutoService(Processor.class)就可以替代以上操作。它是由谷歌开发的,在gradle中加上:
compile 'com.google.auto.service:auto-service:1.0-rc2'
复制代码
有兴趣的小伙伴可以自行网上搜索了解更多内容....
Javapoet
Javapoet是squareup公司提供的能够自动生成java代码的库。
讲一下javapoet里面常用的几个类: MethodSpec
代表一个构造函数或方法声明。 TypeSpec
代表一个类,接口,或者枚举声明。 FieldSpec
代表一个成员变量,一个字段声明。 JavaFile
包含一个顶级类的Java文件
举个例子:
private void generateHelloworld() throws IOException{
//构建main方法
MethodSpec main = MethodSpec.methodBuilder("main") //main代表方法名
.addModifiers(Modifier.PUBLIC,Modifier.STATIC)//Modifier 修饰的关键字
.addParameter(String[].class, "args") //添加string[]类型的名为args的参数
.addStatement("$T.out.println($S)", System.class,"Hello World")//添加代码,这里其实就是添加了System,out.println("Hello World");
.build();
//构建HelloWorld类
TypeSpec typeSpec = TypeSpec.classBuilder("HelloWorld")//HelloWorld是类名
.addModifiers(Modifier.FINAL,Modifier.PUBLIC)
.addMethod(main) //在类中添加方法
.build();
//生成java文件
JavaFile javaFile = JavaFile.builder("com.example.helloworld", typeSpec)
.build();
javaFile.writeTo(System.out);
}
复制代码
运行一下
看到这应该知道java代码是怎么生成的了吧。现在需要用到的技术点都大致了解了。准备工作做好了,现在进入正题吧。
撸代码
项目结构
annotation:(java lib) 提供注解。 annotation-compiler:(java lib)注解处理。 annotation-api:(Android Lib) 是我们外部用到 api。 app:是调用api进行测试的。
APP模块
public class MainActivity extends AppCompatActivity {
@WatchEdit(editIds = {R.id.ed_1, R.id.ed_2, R.id.ed_3})
Button button1;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
button1 = (Button) findViewById(R.id.bbb11);
ViewAnnoUtil.watch(this);
button1.setEnabled(false);
}
}
复制代码
看看我们使用了什么: 一个注解标记:@WatchEdit
还有一句代码: ViewAnnoUtil.watch(this);
这两句话到底是怎么生效的呢?这就到下一个模块了。
annotation模块
先来看这个注解标记
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface WatchEdit {
/**
* 被观察的输入框的id
*
* @return
*/
int[] editIds();
}
复制代码
它位于annotation模块中,为了观察多个EditText,定义一个注解。参数是 要观察的EditText的id。
api模块
再来看ViewAnnoUtl.watch(this)
干了啥:
//最终对外使用的工具类
public class ViewAnnoUtil {
private static ActivityWatcher actWatcher = new ActivityWatcher();
private static Map WATCH_MAP = new HashMap<>();
public static void watch(Activity activity) {
watch(activity, activity, actWatcher);
}
private static void watch(Object host, Object source, Watcher watcher) {
String className = host.getClass().getName();
try {
Injector injector = WATCH_MAP.get(className);
if (injector == null) {
Class> finderClass = Class.forName(className + "$$Injector");
injector = (Injector) finderClass.newInstance();
WATCH_MAP.put(className, injector);
}
injector.inject(host, source, watcher);
} catch (Exception e) {
e.printStackTrace();
}
}
}
复制代码
看到这个类中的代码,我们知道,watch(this)
实际上调用了watch(Object host, Object source, Watcher watcher)
方法。
其中host
和 source
参数都是传的activity
,watcher
参数则是传的类里实例化的ActivityWatcher
对象实例。做了一些由于使用这里使用了一些反射,所以通过使用内存缓存来进行优化。最后则调用了injector.inject()
方法,那我们看看这些都是什么东西。
//观察类的接口
public interface Watcher {
/**
* 查找view的方法
*
* @param obj view的来源,哪个activity或者fragment
* @param id 要查找的view的id
* @return 查找到的view
*/
EditText findView(Object obj, int id) throws ClassCastException;
/**
* 进行观察
*
* @param editText 被观察的edit
* @param obser 观察的view
*/
void watchEdit(EditText editText, View obser);
}
复制代码
//提供一个默认的通过Activity实现的Watcher
public class ActivityWatcher implements Watcher, TextWatcher {
private HashMap> map = new HashMap<>();
@Override
public EditText findView(Object obj, int id) throws ClassCastException {
return (EditText) ((Activity) obj).findViewById(id);
}
@Override
public void watchEdit(EditText editText, final View obser) {
if (map.get(obser) == null) {
ArrayList itemEditList = new ArrayList<>();
itemEditList.add(editText);
map.put(obser,itemEditList);
} else {
map.get(obser).add(editText);
}
editText.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
@Override
public void afterTextChanged(Editable s) {
if (checkEnable(map.get(obser)))
obser.setEnabled(true);
else obser.setEnabled(false);
}
});
}
private boolean checkEnable(ArrayList editList) {
for (EditText text : editList) {
if (TextUtils.isEmpty(text.getText().toString()))
return false;
}
return true;
}
复制代码
这很好理解了吧,也就是说具体的让Button监听到EditText输入变化的代码在这里。 再来看injector
:
//绑定的接口类
public interface Injector {
/**
* @param host 目标
* @param source 来源
* @param watcher 提供具体使用的方法 查找edit,添加监听
*/
void inject(T host, Object source, Watcher watcher);
}
复制代码
可以发现其实它是一个接口,规定了目标从哪里来,由谁来执行这个监听操作(Wathcer) 那么问题来了,光是接口怎么能够实现功能呢?肯定得有一个接口的实现类才行吧。 别着急,我们看这一段代码:
String className = host.getClass().getName();
Injector injector = WATCH_MAP.get(className);
if (injector == null) {
Class> finderClass = Class.forName(className + "$$Injector");
injector = (Injector) finderClass.newInstance();
WATCH_MAP.put(className, injector);
}
复制代码
可以发现其实我们用反射加载了一个类,类名是 host的类名+ "$$Injector" 是不是很熟悉?使用butterknife的小伙伴肯定遇到过 MainActivity&&ViewBinder这类似的类名吧。没错就是它。他就是我们 Injector的实现类,完成了具体的实现。只是它是由我们前面提到的 javapoet动态生成的。再来看看这个顺序:
ViewAnnoUtil.watch() ----> injector.inject()并传入了目标的Activity,和我们写好的ActivityWacther。
通过动态生成的injector实现类来协调。
复制代码
现在我们来看看怎么生成这个实现类。
compiler模块
annotation-compiler中包含注解处理器,java文件生成等
常量类
//常量工具类
public class TypeUtil {
public static final ClassName WATCHER = ClassName.get("com.colin.annotation_api", "Watcher");
public static final ClassName INJECTOR = ClassName.get("com.colin.annotation_api", "Injector");
}
复制代码
注解处理类
@AutoService(Processor.class)
public class WatchEditProcessor extends AbstractProcessor {
//具体代码我放后面
}
复制代码
前面介绍过怎么定义注解处理器,我们来看看这个类里该干什么
先来简单的,定义我们支持的注解,我们这只支持@WatchEdit这个注解。(可以有多个)
@Override
public Set getSupportedAnnotationTypes() {
Set types = new LinkedHashSet<>();
types.add(WatchEdit.class.getCanonicalName());
return types;
}
复制代码
我们支持的java版本是最高版本
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
复制代码
定义几个成员变量:
//文件工具类
private Filer mFiler;
//处理元素的工具类
private Elements mElementUtils;
//log工具类
private Messager mMessager;
//使用了注解的类的包装类的集合
private Map mAnnotatedClassMap = new HashMap<>();
复制代码
然后在init方法中进行了初始化
@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
super.init(processingEnvironment);
mElementUtils = processingEnvironment.getElementUtils();
mMessager = processingEnvironment.getMessager();
mFiler = processingEnvironment.getFiler();
}
复制代码
最后看最重要的方法 process()
@Override
public boolean process(Set extends TypeElement> set, RoundEnvironment roundEnvironment) {
mAnnotatedClassMap.clear();
try {
processWatchEdit(roundEnvironment);
} catch (IllegalArgumentException e) {
error(e.getMessage());
return true;
}
try {
for (WatchEditAnnotatedClass annotatedClass : mAnnotatedClassMap.values()) {
info("generating file for %s", annotatedClass.getFullClassName());
annotatedClass.generateWatcher().writeTo(mFiler);
}
} catch (Exception e) {
e.printStackTrace();
error("Generate file failed,reason:%s", e.getMessage());
}
return true;
}
复制代码
返回true表示已经处理过。
先看这句processWatchEdit(roundEnvironment);
代码干了什么:
private void processWatchEdit(RoundEnvironment roundEnv) {
//遍历处理 使用了 @WatchEdit 注解的类
//一个element代表一个元素(可以是类,成员变量等等)
for (Element element : roundEnv.getElementsAnnotatedWith(WatchEdit.class)) {
WatchEditAnnotatedClass annotatedClass = getAnnotatedClass(element);
//通过 roundEnv工具构建一个成员变量
WatchEditField field = new WatchEditField(element);
//添加使用了@WatchEdit注解的成员变量
annotatedClass.addField(field);
}
}
private WatchEditAnnotatedClass getAnnotatedClass(Element element) {
//得到一个 类元素
TypeElement encloseElement = (TypeElement) element.getEnclosingElement();
//拿到类全名
String fullClassName = encloseElement.getQualifiedName().toString();
//先从缓存中取
WatchEditAnnotatedClass annotatedClass = mAnnotatedClassMap.get(fullClassName);
if (annotatedClass == null) {
//没有就构建一个
annotatedClass = new WatchEditAnnotatedClass(encloseElement, mElementUtils);
//放入缓存
mAnnotatedClassMap.put(fullClassName, annotatedClass);
}
return annotatedClass;
}
复制代码
这里又用到了两个类: WatchEditField
:被@WatchEdit
注解标记的成员变量的包装类。 WatchEditAnnotatedClass
:使用了@WatchEdit
注解的类。 拿上面的例子来说MainActivity
就是WatchEditAnnotatedClass
而Button button1
这个button1
就是WatchEditField
这两个类里面具体有什么待会看,现在看下一段代码:
for (WatchEditAnnotatedClass annotatedClass : mAnnotatedClassMap.values()) {
info("generating file for %s", annotatedClass.getFullClassName());
annotatedClass.generateWatcher().writeTo(mFiler);
}
复制代码
这里循环我们的包装类,并调用generateWatcher()
方法,并写入到前面提到的文件工具中。 看这个方法名就知道,这里就是生成java代码的核心方法了。至此流程终于连上了。。。不容易啊 =_=
梳理一下:
流程搞明白了,接下来看看,我们费了大力气生成的java文件怎么生成的,也就是generateWatcher()
里做了啥,来看代码:
public JavaFile generateWatcher() {
String packageName = getPackageName(mClassElement);
String className = getClassName(mClassElement, packageName);
//获取到当前使用了注解标记的类名(MainActivity)
ClassName bindClassName = ClassName.get(packageName, className);
//构建出重写的inject方法
MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder("inject")
.addModifiers(Modifier.PUBLIC)
.addAnnotation(Override.class)
.addParameter(TypeName.get(mClassElement.asType()), "host", Modifier.FINAL)
.addParameter(TypeName.OBJECT, "source")
.addParameter(TypeUtil.WATCHER, "watcher");
//添加代码
for (WatchEditField field : mFiled) {
//获得每个button要监听的EditText的id
int[] ids = field.getResIds();
if (ids != null) {
//为每个EditText添加监听
for (int i = 0; i < ids.length; i++) {
//添加监听
methodBuilder.addStatement("watcher.watchEdit(watcher.findView(source,$L),$N)",
ids[i], "host." + field.getFieldName());
// methodBuilder.addStatement("watcher.watchEdit(watcher.findView(source,$L),$N)",
// ids[i], field);
}
}
}
//构建类 MainActivity$$Injector
TypeSpec finderClass = TypeSpec.classBuilder(bindClassName.simpleName() + "$$Injector")
.addModifiers(Modifier.PUBLIC)
.addSuperinterface(ParameterizedTypeName.get(TypeUtil.INJECTOR, TypeName.get(mClassElement.asType())))
.addMethod(methodBuilder.build()) //添加刚刚生成的injector方法
.build();
//生成一个java文件
return JavaFile.builder(packageName, finderClass).build();
}
复制代码
这里就用到了我们前面提到的javapoet库了。通过注释应该很好理解这段代码的意思了。 关于javapoet有兴趣的小伙伴可以自行搜索了解更多内容。
好了,至此一切都结束了!!!至于WatchEditField的代码贴下面了。 测试了一波功能是正常运行了。。不会贴动图。。。然后接下来就要考虑的是接触绑定,释放资源等等优化了。先到这吧。
结束
平时用起来很方便的东西了解一下原理才发现还是很复杂的。一个小小的需求,仔细研究一下也会学习到很多知识,学会了新的知识就可以应用到更多的方面去,有学习才有进步。加油。另外Demo我会放到Github上去。里面也会慢慢更新出更多的东西 GitHub
被@WatchEdit注解标记的成员变量包装类,如一个 button
public class WatchEditField {
private VariableElement mFieldElement;
private int[] mEditIds;
public WatchEditField(Element element) throws IllegalArgumentException {
if (element.getKind() != ElementKind.FIELD) {
throw new IllegalArgumentException(String.format("Only field can be annotated with @%s",
WatchEdit.class.getSimpleName()));
}
mFieldElement = (VariableElement) element;
WatchEdit bindView = mFieldElement.getAnnotation(WatchEdit.class);
if (bindView != null) {
mEditIds = bindView.editIds();
if (mEditIds == null && mEditIds.length <= 0) {
throw new IllegalArgumentException(String.format("editIds() in %s for field % is not valid",
WatchEdit.class.getSimpleName(), mFieldElement.getSimpleName()));
}
}
}
c
public Name getFieldName() {
return mFieldElement.getSimpleName();
}
public int[] getResIds() {
return mEditIds;
}
public TypeMirror getFieldType() {
return mFieldElement.asType();
}
public Object getConstantValue(){
return mFieldElement.getConstantValue();
}
}
复制代码