查看原文
其他

编译时注解详解及实现ButterKnife

jzman 躬行之 2022-08-26

PS:人是一种很乐于接受自我暗示的生物,你给了自己消极暗示,那么你很容易变得颓废,如果你给了自己积极暗示,那么你也会变得积极起来。

今天看一下编译时注解的相关知识,相信手动实践后你会更容易理解像 Dagger、ARouter、ButterKnife 等这种使用了编译时注解的框架,也更容易理解其内部源码实现,内容如下:

  1. 编译时和运行时注解

  2. 注解处理器APT

  3. AbstractProcessor

  4. Element和Elements

  5. 自定义注解处理器

  6. 使用自定义注解处理器

编译时和运行时注解

先了解一下编译时和运行时的区别:

  1. 编译时:指编译器将源代码翻译成机器能够识别的代码的过程,Java 中也就是将 Java 源代码编译为 JVM 识别的字节码文件的过程。

  2. 运行时:指 JVM 分配内存、解释执行字节码文件的过程。

元注解 @Retention 决定注解是在编译时还是在运行时,其可配置策略如下:

public enum RetentionPolicy {
    SOURCE,  //在编译时会被丢弃,仅仅在源码中存在
    CLASS,   //默认策略,运行时就会被丢弃,仅仅在 class 文件中
    RUNTIME  //编译时会将注解信息记录到class文件,运行时任然保留,可以通过反射获取注解信息
}

编译时注解和运行时注解除了上面的区别之外,实现方式也不一样,编译时注解一般是通过注解处理器(APT)来实现的,运行时注解一般是通过反射来实现的。关于注解可以参考下面这篇文章:

什么是APT

APT(Annotation Processing Tool)是 javac 提供的一种可以处理注解的工具,用来在编译时扫描和处理注解的,简单来说就是可以通过 APT 获取到注解及其注解所在位置的信息,可以使用这些信息在编译器生成代码。编译时注解就是通过 APT 来通过注解信息生成代码来完成某些功能,典型代表有 ButterKnife、Dagger、ARouter等。

AbstractProcessor

AbstractProcessor 实现 Processor 是注解处理器的抽象类,要实现注解处理器都需要继承 AbstractProcessor 进行扩展,主要方法说明如下:

  • init:初始化Processor,可从参数 ProcessingEnvironment 中获取工具类 ElementsTypesFiler 以及 Messager 等;

  • getSupportedSourceVersion:返回使用的 Java 版本;

  • getSupportedAnnotationTypes:返回要处理的的所有的注解名称;

  • process:获取所有指定的注解进行处理;

process 方法可能会在运行过程中执行多次,知道没有其他类产生为止。

Element和Elements

Element 类似于 XML 中的标签一样,在 Java 中Element 表示程序元素,如类、成员、方法等,每个 Element 都表示仅表示某种结构,对 Element 对象类的操作,请使用 visitor 或 getKind( 方法进行判断再具体处理,Element 的子类如下:

  • ExecutableElement

  • PackageElement

  • Parameterizable

  • QualifiedNameable

  • TypeElement

  • TypeParameterElement

  • VariableElement

上述元素结构分别对应下面代码结构:

// PackageElement
package manu.com.compiler;

// TypeElement
public class ElementSample {
    // VariableElement
    private int a;
    // VariableElement
    private Object other;

    // ExecutableElement
    public ElementSample() {
    }
    // 方法参数VariableElement
    public void setA(int newA) {
    }
    // TypeParameterElement表示参数化类型,用在泛型参数中
}

Elements 是处理 Element 的工具类,只提供接口,具体有 Java 平台实现。

自定义编译注解处理器

下面使用 APT 实现一个注解 @Bind 来替换来模仿 ButterKnife 的注解 @BindView,案例项目结构如下:

APT

在 api 模块中定义注解 @Bind 如下:

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.CLASS)
public @interface Bind {
    int value();
}

compiler 模块是 Java 模块,引入 Google 的 auto-service 用来生成 META-INFO 下的相关文件,javapoet 用于更方便的创建 Java 文件:

// 用来生成META-INF/services/javax.annotation.processing.Processor文件
annotationProcessor 'com.google.auto.service:auto-service:1.0-rc7'
// 用于创建Java文件
implementation 'com.squareup:javapoet:1.12.1'

自定义注解处理器 BindProcessor 如下:

/**
 * BindProcessor
 */

@AutoService(Processor.class)
public class BindProcessor extends AbstractProcessor {
    private Elements mElements;
    private Filer mFiler;
    private Messager mMessager;

    // 存储某个类下面对应的BindModel
    private Map<TypeElement, List<BindModel>> mTypeElementMap = new HashMap<>();

    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
        mMessager = processingEnvironment.getMessager();
        print("init");
        // 初始化Processor

        mElements = processingEnvironment.getElementUtils();
        mFiler = processingEnvironment.getFiler();
    }

    @Override
    public SourceVersion getSupportedSourceVersion() {
        print("getSupportedSourceVersion");
        // 返回使用的Java版本
        return SourceVersion.RELEASE_8;
    }

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        print("getSupportedAnnotationTypes");
        // 返回要处理的的所有的注解名称
        Set<String> set = new HashSet<>();
        set.add(Bind.class.getCanonicalName());
        set.add(OnClick.class.getCanonicalName());
        return set;
    }

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        print("process");
        mTypeElementMap.clear();
        // 获取指定Class类型的Element
        Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(Bind.class);
        // 遍历将符合条件的Element存储起来
        for (Element element : elements) {
            // 获取Element对应类的全限定类名
            TypeElement typeElement = (TypeElement) element.getEnclosingElement();
            print("process typeElement name:"+typeElement.getSimpleName());
            List<BindModel> modelList = mTypeElementMap.get(typeElement);
            if (modelList == null) {
                modelList = new ArrayList<>();
            }
            modelList.add(new BindModel(element));
            mTypeElementMap.put(typeElement, modelList);
        }

        print("process mTypeElementMap size:" + mTypeElementMap.size());

        // Java文件生成
        mTypeElementMap.forEach((typeElement, bindModels) -> {
            print("process bindModels size:" + bindModels.size());
            // 获取包名
            String packageName = mElements.getPackageOf(typeElement).getQualifiedName().toString();
            // 生成Java文件的文件名
            String className = typeElement.getSimpleName().toString();
            String newClassName = className + "_ViewBind";

            // MethodSpec
            MethodSpec.Builder constructorBuilder = MethodSpec.constructorBuilder()
                    .addModifiers(Modifier.PUBLIC)
                    .addParameter(ClassName.bestGuess(className), "target");
            bindModels.forEach(model -> {
                constructorBuilder.addStatement("target.$L=($L)target.findViewById($L)",
                        model.getViewFieldName(), model.getViewFieldType(), model.getResId());
            });
            // typeSpec
            TypeSpec typeSpec = TypeSpec.classBuilder(newClassName)
                    .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
                    .addMethod(constructorBuilder.build())
                    .build();
            // JavaFile
            JavaFile javaFile = JavaFile.builder(packageName, typeSpec)
                    .addFileComment("AUTO Create")
                    .build();

            try {
                javaFile.writeTo(mFiler);
            } catch (IOException e) {
                e.printStackTrace();
            }
        });

        return true;
    }

    private void print(String message) {
        if (mMessager == nullreturn;
        mMessager.printMessage(Diagnostic.Kind.NOTE, message);
    }
}

其中 BindModel 是对注解 @Bind 信息的简单封装,如下:

/**
 * BindModel
 */

public class BindModel {
    // 成员变量Element
    private VariableElement mViewFieldElement;
    // 成员变量类型
    private TypeMirror mViewFieldType;
    // View的资源Id
    private int mResId;

    public BindModel(Element element) {
        // 校验Element是否是成员变量
        if (element.getKind() != ElementKind.FIELD) {
            throw new IllegalArgumentException("element is not FIELD");
        }
        // 成员变量Element
        mViewFieldElement = (VariableElement) element;
        // 成员变量类型
        mViewFieldType = element.asType();
        // 获取注解的值
        Bind bind = mViewFieldElement.getAnnotation(Bind.class);
        mResId = bind.value();
    }

    public int getResId(){
        return mResId;
    }

    public String getViewFieldName(){
        return mViewFieldElement.getSimpleName().toString();
    }

    public TypeMirror getViewFieldType(){
        return mViewFieldType;
    }
}

在 bind 模块使用反射创建要生成的文件如下:

/**
 * 初始化
 */

public class BindKnife {
    public static void bind(Activity activity) {
        // 获取activity的全限定类名
        String name = activity.getClass().getName();
        try {
            // 反射创建并注入Activity
            Class<?> clazz = Class.forName(name + "_ViewBind");
            clazz.getConstructor(activity.getClass()).newInstance(activity);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

当然 ButterKnife 应该是把创建的对象缓存起来,不会每次都会去创建新的对象,虽然也是用了反射,但是相较运行时注解来说反射的测试减少了很多,所以性能也较运行时注解更好,这也算是编译时注解和运行时注解的区别。

使用自定义注解处理器

使用方式和 ButterKnife 类似,如下:

public class MainActivity extends AppCompatActivity{

    @Bind(R.id.tvData)
    TextView tvData;

    @Override
     public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        BindKnife.bind(this);
        tvData.setText("data");
    }
}
了解编译时注解,可能并不会去立马造轮子,但是对在看其他框架源码的时候非常有帮助,在解决问题的时候也就多了一个途径,编译时注解到此为止。

推荐阅读:

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存