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')

processor源代码下载 提取码:r56n

评论

您确定要删除吗?删除之后不可恢复