Java注解进阶三
本文继续介绍编译时注解,不同于前面两篇偏向理论,本文通过仿写一个ButterKnife示例,辅助我们理解编译时注解是如何通过生成模板代码,达到简化代码,提升开发效率的目的。
除了编译时注解,还有一种注解方式-运行时注解,其实运行时注解也可以帮助我们简化代码提升开发效率,那么ButterKnife与其它框架相比优势在哪呢?我们希望通过注解的三篇进阶文章解决一下几个疑问。
- 我们经常谈论编译时注解,究竟在谈些什么?
- ButterKnife的实现原理或者实现机制是什么?
- ButterKnife与其它运行时注解框架相比有什么优势?
在稍后的博文中,我们通过运行时注解,也来实现一个类似ButterKnife的ORM框架,通过具体示例对比,可能理解上也更深入。
ButterKnife目的
项目中一旦使用ButterKnife,在编译成功之后会生成一个辅助类,在辅助类中有相对应的查找资源或者View的一些操作,还有一些相应的View响应事件。如果仔细分析该辅助类,一定会发现该辅助类完全可以自己去实现,功能非常简单,职责也单一。
通过抽丝剥茧,我们可以了解到ButterKnife就是生成了一个辅助类,然后在辅助类中做了两件事,分别是查找资源和设置View事件。
一般抽取生成一个辅助工具类,可以有两种实现方式,通过将方法抽取为静态方法或者实例方法,Butterknife在生成的辅助类上面采取的是后一种,里面方法都是实例方法。实例方法有自身的优势,一个目标类对应一个辅助类,实例方法只有在被调用时才会占用内存,当目标类被销毁后,可以同步销毁辅助类,这样也可以降低内存占用。
借助于ButterKnife生成的辅助类中没有任何的反射方法实现,我们知道反射很影响代码的性能,运行时注解采用的就是反射的方式,所以相比于运行时注解,ButterKnife的性能提示非常明显的。可能有些开发人员会发现,Butterknife中也使用的反射呢,在生成辅助类时就采用了反射。ButterKnife中确实使用了反射,一般性的框架基本都会涉及到反射,不过ButterKnife执行反射这一步是在加载辅助类并生成辅助类对象时,而对注解中各种属性的处理是在编译时处理的,包括生成辅助类。
自定义ButterKnife
通过自定义ButterKnife实现一个简单的查找View和设置点击事件的方法,一般涉及三个Module,截图如下:
annotation:自定义注解Module,主要定义几个外部需要使用的注解,比如BindView、OnClick注解。
processor:这个是自定义ButterKnife的核心Module,在该Module需要拿到BindView、OnClick注解中的属性,然后生成相应的辅助类,并且在辅助类完成查找资源和设置事件的处理。
butterknife:有人可能对该Module有些疑问,annotation模块定义注解,processor处理注解,那butterknife还有什么用处。其实该Module作用也非常重要,通过前面两步辅助类已经生成了,如何将辅助类和目标类关联在一起,这一步就是在butterknife模块中处理,更详细的介绍可以参看下文。
annotation和processor两个Module都是Java类型的Module,butterknife要定义为Android类型的Module。
自定义annotation
@Retention(RetentionPolicy.CLASS) @Target(ElementType.FIELD) public @interface BindView { int value(); } @Retention(RetentionPolicy.CLASS) @Target(ElementType.METHOD) public @interface OnClick { int[] value(); }
自定义BindView和OnClick注解这里不做过多介绍,有对自定义注解不熟悉的开发人员,可以参看以前注解相关的内容。
自定义的注解使用代码如下;
@BindView(R.id.btn) public Button btn; @BindView(R.id.textView) public TextView textView; @OnClick({R.id.btn, R.id.textView}) public void startOnclick(View view) { if (R.id.textView == view.getId()) { Log.d(TAG, "TextView is clicked"); } else { Log.d(TAG, "Button is clicked"); } }
注解处理器processor
processor的结构截图如下:
这里也可以使用Google的AutoService类,这样就不需要手动在resources下META-INF/services文件夹新建一个javax.annotation.processing.Processor
命名的文件了。
有关init()、getSupportedSourceVersion()和getSupportedAnnotationTypes()三个方法如下,不涉及过多的处理逻辑。
public static final String SUFFIX = "_ViewBinding"; private Elements elementUtils; private Filer filer; @Override public synchronized void init(ProcessingEnvironment processingEnv) { super.init(processingEnv); elementUtils = processingEnv.getElementUtils(); filer = processingEnv.getFiler(); } @Override public SourceVersion getSupportedSourceVersion() { return SourceVersion.latestSupported(); } @Override public Set<String> getSupportedAnnotationTypes() { Set<String> set = new HashSet<>(); set.add(BindView.class.getName()); set.add(OnClick.class.getName()); return Collections.unmodifiableSet(set); }
BindView与OnClick对应的注解元素分别是VariableElement与ExecutableElement,无论是VariableElement还是ExecutableElement都是Element的子类型,我们都可以通过getEnclosingElement()方法获取到封装这些元素的最内层类TypeElement元素名称。既然可以拿到类名称,那么待生成的类名称也有了。
假设一个Activity类对应一个辅助类Activity_ViewBinding,由于Activity中会有多个BindView注解的View成员变量,也会有多个OnClick注解的方法,因此是一对多的关系。这种一对多的关系,我们可以定义一个TargetBinder辅助类,该TargetBinder辅助类仅内部使用,在TargetBinder中存储前面的一对多的关系,在生成Java源代码时,那么就是一个TargetBinder类对应一个Activity_ViewBinding辅助类。
public class TargetBinder { private final Map<Integer, ViewId> viewIdMap = new LinkedHashMap<>(); private final String classPackage; // MainActivity_ViewBinding private final String className; // MainActivity private final String targetClass; public TargetBinder(String classPackage, String className, String targetClass) { this.classPackage = classPackage; this.className = className; this.targetClass = targetClass; } public void addField(int id, String name, String type) { getTargetView(id).field = new FieldInjection(name, type); } public void addMethod(int id, String name, String parameterType) { getTargetView(id).method = new MethodInjection(name, parameterType); } private ViewId getTargetView(int id) { ViewId viewId = viewIdMap.get(id); if (viewId == null) { viewId = new ViewId(id); viewIdMap.put(id, viewId); } return viewId; } // 全类名(FQCN)请求模式 public String getFqcn() { return classPackage + "." + className; } static class FieldInjection { final String name; final String type; FieldInjection(String name, String type) { this.name = name; this.type = type; } } static class MethodInjection { final String name; final String type; MethodInjection(String name, String type) { this.name = name; this.type = type; } } private static class ViewId { final int id; FieldInjection field; MethodInjection method; public ViewId(int id) { this.id = id; } } }
接下来我们看下ButterKnifeProcessor几个核心处理方法。
private Map<TypeElement, TargetBinder> findAndParseTargets(RoundEnvironment roundEnv) { Map<TypeElement, TargetBinder> targetClassMap = new LinkedHashMap<>(); // BindView for (Element element : roundEnv.getElementsAnnotatedWith(BindView.class)) { TypeElement enclosingElement = (TypeElement) element.getEnclosingElement(); String name = element.getSimpleName().toString(); int id = element.getAnnotation(BindView.class).value(); String type = element.asType().toString(); // btn 2131165249 android.widget.Button log(name + " " + id + " " + type); TargetBinder targetBinder = getOrCreateTargetClass(targetClassMap, enclosingElement); targetBinder.addField(id, name, type); } // OnClick for (Element element : roundEnv.getElementsAnnotatedWith(OnClick.class)) { ExecutableElement executableElement = (ExecutableElement) element; TypeElement enclosingElement = (TypeElement) element.getEnclosingElement(); List<? extends VariableElement> parameters = executableElement.getParameters(); String type = null; if (!parameters.isEmpty()) { // Verify that there is only a single parameter. VariableElement variableElement = parameters.get(0); if (parameters.size() == 1) { type = variableElement.asType().toString(); } else { continue; } } // Assemble information on the injection point. String name = executableElement.getSimpleName().toString(); int[] ids = element.getAnnotation(OnClick.class).value(); TargetBinder targetBinder = getOrCreateTargetClass(targetClassMap, enclosingElement); for (int id : ids) { targetBinder.addMethod(id, name, type); } } return targetClassMap; } private TargetBinder getOrCreateTargetClass(Map<TypeElement, TargetBinder> targetClassMap, TypeElement enclosingElement) { TargetBinder targetClass = targetClassMap.get(enclosingElement); if (targetClass == null) { String targetType = enclosingElement.getSimpleName().toString(); String classPackage = getPackageName(enclosingElement); String className = getClassName(enclosingElement, classPackage) + SUFFIX; // com.sunny.demo.MainActivity com.sunny.demo MainActivity_ViewBinding log(targetType + " " + classPackage + " " + className); targetClass = new TargetBinder(classPackage, className, targetType); targetClassMap.put(enclosingElement, targetClass); } return targetClass; } @Override public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { Map<TypeElement, TargetBinder> targetClassMap = findAndParseTargets(roundEnv); for (Map.Entry<TypeElement, TargetBinder> entry : targetClassMap.entrySet()) { TypeElement typeElement = entry.getKey(); TargetBinder targetClass = entry.getValue(); try { JavaFileObject jfo = filer.createSourceFile(targetClass.getFqcn(), typeElement); Writer writer = jfo.openWriter(); writer.write(targetClass.brewJava()); writer.flush(); writer.close(); } catch (IOException e) { log("IOException:" + e.getMessage()); } } return true; }
TargetBinder如何生成Java类的处理逻辑这里就不贴出来来了,可以从下文源代码中下载查看。
ButterKnife辅助类
一般在Activity的onCreate()方法中使用ButterKnife.bind(this),其实是为了建立Activity与辅助类Activity_ViewBinding的关联关系。
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); ButterKnife.bind(this); }
如果不使用ButterKnife.bind(this)方式,一般我们会在Activity中new一个Activity_ViewBinding实例,不过这一步ButterKnife.bind(this)已经帮我们做了。ButterKnife类就是为了创建一个辅助类,它会首先在一个Map缓存中查询,是否已经存在某个类的辅助类,如果存在,则直接从Map中取出即可,如果不存在,则使用反射的方式创建一个辅助类,并将该辅助类存入缓存Map中。
public class ButterKnife { static final Map<Class<? >, Constructor<? extends UnBinder>> BINDINGS = new LinkedHashMap<>(); private ButterKnife() { } public static UnBinder bind(Activity target) { View sourceView = target.getWindow().getDecorView(); return bind(target, sourceView); } public static UnBinder bind(Object target, Activity source) { View sourceView = source.getWindow().getDecorView(); return bind(target, sourceView); } public static UnBinder bind(@NonNull Object target, @NonNull View source) { Class<? > targetClass = target.getClass(); Constructor<? extends UnBinder> constructor = findBindingConstructorForClass(targetClass); if (constructor == null) { return UnBinder.EMPTY; } UnBinder unBinder = null; try { unBinder = constructor.newInstance(target, source); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InstantiationException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } return unBinder == null ? UnBinder.EMPTY : unBinder; } private static Constructor<? extends UnBinder> findBindingConstructorForClass(Class<? > targetClass) { Constructor<? extends UnBinder> constructor = BINDINGS.get(targetClass); if (constructor != null) { return constructor; } String clsName = targetClass.getName(); if (clsName.startsWith("android.") || clsName.startsWith("java.") || clsName.startsWith("androidx.")) { return null; } try { Class<? > bindingClass = targetClass.getClassLoader().loadClass(clsName + "_ViewBinding"); constructor = (Constructor<? extends UnBinder>) bindingClass.getConstructor(targetClass, View.class); } catch (ClassNotFoundException e) { e.printStackTrace(); } catch (NoSuchMethodException e) { e.printStackTrace(); } BINDINGS.put(targetClass, constructor); return constructor; } }
小结
本文介绍的示例,其实是最简单版的ButterKnife,只在辅助我们理解ButterKnife的原理。真正 在使用ButterKnife时,BindView注解的成员变量必须是View或者及其子类才可以,而且成员变量的修饰符不能是static或者private修饰的,这些安全性验证工作都没有做处理。
另外在gradle中引用processor时需要与其它Module区分开来,注解处理器使用的是annotationProcessor方式引入。
implementation project(path: ':butterknife') implementation project(path: ':annotation') annotationProcessor project(path: ':processor')