尝试手写一个注解框架
前两天在安科网上看到一篇文章: 一个小需求引发的思考。
需求是根据多个EditText是否有输入的值来确定Button是否可点击。很常见的一个需求吧,但是要怎么做到简单优雅呢?文章里也有讲到封装的过程,这里我就不细讲了。最后作者封装成了用注解就可以做到。但是他使用的是反射技术,反射技术会影响App的性能。大家都知道著名的注解框架 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<String> 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);
这两句话到底是怎么生效的呢?
先来看这个注解标记
@Retention(RetentionPolicy.CLASS) @Target(ElementType.FIELD) public @interface WatchEdit { /** * 被观察的输入框的id * * @return */ int[] editIds(); }
它位于annotation模块中,为了观察多个EditText,定义一个注解。参数是 要观察的EditText的id。
再来看ViewAnnoUtl.watch(this)
干了啥:
//最终对外使用的工具类 public class ViewAnnoUtil { private static ActivityWatcher actWatcher = new ActivityWatcher(); private static Map<String, Injector> 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<View, ArrayList<EditText>> 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<EditText> 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<EditText> editList) { for (EditText text : editList) { if (TextUtils.isEmpty(text.getText().toString())) return false; } return true; }
这很好理解了吧,也就是说具体的让Button监听到EditText输入变化的代码在这里。
再来看injector
:
//绑定的接口类 public interface Injector<T> { /** * @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实现类来协调。
现在我们来看看怎么生成这个实现类。
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<String> getSupportedAnnotationTypes() { Set<String> 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<String, WatchEditAnnotatedClass> 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())); } } } public Name getFieldName() { return mFieldElement.getSimpleName(); } public int[] getResIds() { return mEditIds; } public TypeMirror getFieldType() { return mFieldElement.asType(); } public Object getConstantValue(){ return mFieldElement.getConstantValue(); } }