1.简介
Element
是一种在编译期描述.java文件静态结构的一种类型,它可能表示一个package、一个class、一个method或者一个field。Element
的比较应该使用equals
,因为编译期间同一个Element
可能会用两个对象表示。JDK提供了以下5种Element
。
2.Element的存储结构
编译器采用类似Html的Dom树来存储Element。我们用下面的Test.java
来具体说明。
//PackageElement
package me.zhangkuo.compile;
//TypeElement
public class Test {
//VariableElement
private String name;
//ExecutableElement
private Test(){
}
//ExecutableElement
public void setName(/* TypeParameterElement */ String name) {
this.name = name;
}
}
Test.java
用Element树结构描述如下:
我们可以看到 setName(String name)
的ExecutableElement
中并没有子节点TypeParameterElement
。这是因为TypeParameterElement
没有被纳入到Element
树中。不过我们可以通过ExecutableElement
的getTypeParameters()
方法来获取。
此外,再给大家介绍两个Element中十分有用的方法。
public interface Element extends AnnotatedConstruct {
//获取父Element
Element getEnclosingElement();
//获取子Element的集合
List<? extends Element> getEnclosedElements();
}
二、TypeMirror
============
Element
有一个asType()
方法用来返回TypeMirror
。TypeMirror
表示 Java 编程语言中的类型。这些类型包括基本类型、声明类型(类和接口类型)、数组类型、类型变量和 null 类型。还可以表示通配符类型参数、executable 的签名和返回类型,以及对应于包和关键字 void
的伪类型。我们一般用TypeMirror进行类型判断。如下段代码,用来比较元素所描述的类型是否是Activity的子类。
/**
- 类型相关工具类
*/
private Types typeUtils;
/**
- 元素相关的工具类
*/
private Elements elementUtils;
private static final String ACTIVITY_TYPE = “android.app.Activity”;
private boolean isSubActivity(Element element){
//获取当前元素的TypeMirror
TypeMirror elementTypeMirror = element.asType();
//通过工具类Elements获取Activity的Element,并转换为TypeMirror
TypeMirror viewTypeMirror = elementUtils.getTypeElement(ACTIVITY_TYPE).asType();
//用工具类typeUtils判断两者间的关系
return typeUtils.isSubtype(elementTypeMirror,viewTypeMirror)
}
三、一个简单的ButterKnife
==================
这一节我们通过编写一个简单的ButterKnife
来介绍一下如何编写一个APT框架。APT应该是编译期插桩最简单的一种技术,通过三步就可以完成。
1、定义编译期注解。
我们新增一个Java Library Module命名为apt_api
,编写注解类BindView。
@Retention(RetentionPolicy.Class)
@Target(ElementType.FIELD)
public @interface BindView {
}
这里简单介绍一下RetentionPolicy
。RetentionPolicy
是一个枚举,它的值有三种:SOURCE、CLASS、RUNTIME。
-
SOURCE:不参与编译,让开发者使用。
-
CLASS:参与编译,运行时不可见。给编译器使用。
-
RUNTIME:参与编译,运行时可见。给编译器和JVM使用。
**2、**定义注解处理器。
同样,我们需要新增一个Java Library Module命名为apt_processor
。
我们需要引入两个必要的依赖:一个是我们新增的module apt_annotation
,另一个是google的com.google.auto.service:auto-service:1.0-rc3
(以下简称auto-service
)。
implementation project(‘:apt_api’)
api ‘com.google.auto.service:auto-service:1.0-rc3’
新增一个类 ButterKnifeProcessor
,继承 AbstractProcessor
。
@AutoService(Processor.class)
public class ButterKnifeProcessor extends AbstractProcessor {
/**
- 元素相关的工具类
*/
private Elements elementUtils;
/**
- 文件相关的工具类
*/
private Filer filer;
/**
- 日志相关的工具类
*/
private Messager messager;
/**
- 类型相关工具类
*/
private Types typeUtils;
@Override
public Set getSupportedAnnotationTypes() {
return Collections.singleton(BindView.class.getCanonicalName());
}
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.RELEASE_7;
}
@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
super.init(processingEnvironment);
elementUtils = processingEnv.getElementUtils();
filer = processingEnv.getFiler();
messager = processingEnv.getMessager();
typeUtils = processingEnv.getTypeUtils();
}
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
return false;
}
}
auto-service
为我们简化了定义注解处理器的流程。@AutoService
是就是由auto-service
提供的,其作用是用来告诉编译器我们定义的ButterKnifeProcessor
是一个编译期注解处理器。这样在编译时ButterKnifeProcessor
才会被调用。
我们还重写了AbstractProcessor提供的四个方法:getSupportedAnnotationTypes
、getSupportedSourceVersion
、init
、process
。
getSupportedAnnotationTypes
表示处理器可以处理哪些注解。这里返回的是我们之前定义的BindView。除了重写方法之外,还可用通过注解来实现。
@SupportedAnnotationTypes(value = {“me.zhangkuo.apt.annotation.BindView”})
getSupportedSourceVersion
表示处理器可以处理的Java版本。这里我们采用最新的JDK版本就可以了。同样,我们也可以通过注解来实现。
@SupportedSourceVersion(value = SourceVersion.latestSupported())
-
init
方法主要用来做一些准备工作。我们一般在这里初始化几个工具类。上述代码我们初始了与元素相关的工具类elementUtils
、与日志相关的工具类messager
、与文件相关的filer
以及与类型相关工具类typeUtils
。我们接下来会看到process
主要就是通过这几个类来生成代码的。 -
process
用来完成具体的程序写代码功能。在具体介绍process
之前,请允许我先推荐一个库:javapoet。javapoet
是由神奇的square
公司开源的,它提供了非常人性化的api,来帮助开发者生成.java源文件。它的README.md
文件为我们提供了丰富的例子,是我们学习的主要工具。
private Map<TypeElement, List> elementPackage = new HashMap<>();
private static final String VIEW_TYPE = “android.view.View”;
private static final String VIEW_BINDER = “me.zhangkuo.apt.ViewBinding”;
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
if (set == null || set.isEmpty()) {
return false;
}
elementPackage.clear();
Set<? extends Element> bindViewElement = roundEnvironment.getElementsAnnotatedWith(BindView.class);
//收集数据放入elementPackage中
collectData(bindViewElement);
//根据elementPackage中的数据生成.java代码
generateCode();
return true;
}
private void collectData(Set<? extends Element> elements){
Iterator<? extends Element> iterable = elements.iterator();
while (iterable.hasNext()) {
Element element = iterable.next();
TypeMirror elementTypeMirror = element.asType();
//判断元素的类型是否是View或者是View的子类型。
TypeMirror viewTypeMirror = elementUtils.getTypeElement(VIEW_TYPE).asType();
if (typeUtils.isSubtype(elementTypeMirror, viewTypeMirror) || typeUtils.isSameType(elementTypeMirror, viewTypeMirror)) {
//找到父元素,这里认为是@BindView标记字段所在的类。
TypeElement parent = (TypeElement) element.getEnclosingElement();
//根据parent不同存储的List中
List parentElements = elementPackage.get(parent);
if (parentElements == null) {
parentElements = new ArrayList<>();
elementPackage.put(parent, parentElements);
}
parentElements.add(element);
}else{
throw new RuntimeException(“错误处理,BindView应该标注在类型是View的字段上”);
}
}
}
private void generateCode(){
Set<Map.Entry<TypeElement,List>> entries = elementPackage.entrySet();
Iterator<Map.Entry<TypeElement,List>> iterator = entries.iterator();
while (iterator.hasNext()){
Map.Entry<TypeElement,List> entry = iterator.next();
//类元素
TypeElement parent = entry.getKey();
//当前类元素下,注解了BindView的元素
List elements = entry.getValue();
//通过JavaPoet生成bindView的MethodSpec
MethodSpec methodSpec = generateBindViewMethod(parent,elements);
String packageName = getPackage(parent).getQualifiedName().toString();
ClassName viewBinderInterface = ClassName.get(elementUtils.getTypeElement(VIEW_BINDER));
String className = parent.getQualifiedName().toString().substring(
packageName.length() + 1).replace(‘.’, ‘$’);
ClassName bindingClassName = ClassName.get(packageName, className + “_ViewBinding”);
try {
//生成 className_ViewBinding.java文件
JavaFile.builder(packageName, TypeSpec.classBuilder(bindingClassName)
.addModifiers(PUBLIC)
.addSuperinterface(viewBinderInterface)
.addMethod(methodSpec)
.build()
).build().writeTo(filer);
如果你进阶的路上缺乏方向,可以加入我们的圈子和安卓开发者们一起学习交流!
-
Android进阶学习全套手册
-
Android对标阿里P7学习视频
-
BATJ大厂Android高频面试题
最后,借用我最喜欢的乔布斯语录,作为本文的结尾:
人这一辈子没法做太多的事情,所以每一件都要做得精彩绝伦。
你的时间有限,所以不要为别人而活。不要被教条所限,不要活在别人的观念里。不要让别人的意见左右自己内心的声音。
最重要的是,勇敢的去追随自己的心灵和直觉,只有自己的心灵和直觉才知道你自己的真实想法,其他一切都是次要。
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门,即可获取!
加入我们的圈子和安卓开发者们一起学习交流!**
-
Android进阶学习全套手册
[外链图片转存中…(img-ytGUED24-1715167816031)]
-
Android对标阿里P7学习视频
[外链图片转存中…(img-BdZcQ5Kq-1715167816031)]
-
BATJ大厂Android高频面试题
[外链图片转存中…(img-3me6p5RX-1715167816031)]
最后,借用我最喜欢的乔布斯语录,作为本文的结尾:
人这一辈子没法做太多的事情,所以每一件都要做得精彩绝伦。
你的时间有限,所以不要为别人而活。不要被教条所限,不要活在别人的观念里。不要让别人的意见左右自己内心的声音。
最重要的是,勇敢的去追随自己的心灵和直觉,只有自己的心灵和直觉才知道你自己的真实想法,其他一切都是次要。
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门,即可获取!