Android进阶学习(5)-- APT 手写ButterKnief

注解基础

注解的定义

自定义注解非常简单,和定义接口很像,只需要在interface前面加一个@即可

public @interface AutoTest {
}

@AutoTest
public class MainActivity extends AppCompatActivity{
	//do somethings
}
元注解

元注解可以理解为用在注解类上的注解,主要用来限定注解的使用,一共又以下五种元注解:
1. @Retention 意味保留期,我们可以把它理解为注解的"生命周期",一共有三种参数
RetentionPolicy.SOURCE 注解只保留在源码文件中,当.java文件编译成.class文件注解并不会编译进去;
RetentionPolicy.CLASS 注解只保留到编译进行阶段,只保留到.class文件中并不会加载到Jvm中;
RetentionPolicy.RUNTIME 注解可以保留到程序运行时,它也会被加载进Jvm。

2. @Tager 意味目标,用来指定注解可以被运用在哪里(类、方法、参数等),共有以下八种参数:
ElementType.ANNOTATION_TYPE 可以给一个注解进行注解
ElementType.CONSTRUCTOR 可以给构造方法注解
ElementType.FIELD 可以给一个属性注解
ElementType.LOCAL_VARIABLE 可以给局部变量注解
ElementType.METHOD 可以给方法注解
ElementType.PACKAGE 可以给一个包注解
ElementType.PARAMETER 可以给方法中的参数注解
ElementType.TYPE 可以给一个类型进行注解,比如类、接口、枚举

3. @Documented 字面意思,肯定跟文档有关系,它的作用是能够将注解中的元素包含到 Javadoc 中去。

4. @Inherited 表示如果一个超类被@Inherited注解,那么他的子类没有被任何注解应用的话,他的子类将继承超类的注解。

5. @Repeatable 表示注解可以同时取多个值

注解属性

注解中可以定义属性,可以赋默认值,但是注解属性的类型必须是基本类型,外加一个注解本身类型的数组。
注解定义属性:

@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface AutoTest {
    String type() default "默认值";
}

@AutoTest(type = "Activity")
public class MainActivity extends AppCompatActivity{
	//do somethings
}
注解的提取

注解可以通过反射获取,通过获取类的class对象,通过 isAnnotationPresent() 方法判断是否应用某个注解,通过getAnnotation()获取注解对象,或者是 getAnnotations() 方法获取全部注解

Class<MainActivity> mainActivityClass = MainActivity.class;
//判断是否使用某个注解
Log.e("是否使用了AutoTest注解", "" + mainActivityClass.isAnnotationPresent(AutoTest.class));
//获取指定注解对象
AutoTest autoTestClass = mainActivityClass.getAnnotation(AutoTest.class);
//获取注解数组
Annotation[] annotations = mainActivityClass.getAnnotations();
for (int i=0; i<annotations.length; i++){
    Log.e("遍历MainActivity注解", annotations[i].toString());
}

注解与APT

APT(Annotation Processing Tool) 是一种注解处理工具,它对源代码文件进行检测,找到其中对应的注解根据注解生成对应代码。

大体流程:我们自定义一些注解,并且设定这些注解的具体逻辑,将注解标注在类上或方法上。利用谷歌给我们提供的auto-service,在编译时,可以扫描我们指定的注解,获取一些相关信息,再通过square给我们提供的javapoet生成对应的java文件供我们使用。很多框架都是运用这个原理,比如:MvRx,ButterKnief。

手写"ButterKnief"

现在利用上面说到的 auto-service 和 javapoet 来实现一个简单的"ButterKnief",帮助我们去写findviewbyId。
在这里插入图片描述
项目大体结构就是上面图片这样:
annotations:存放自定义的注解
api:定义注解的一些功能
app:就是主工程
processor:在编译时帮我们自动生成java文件
首先,创建annotations,自定义我们的注解:
@AutoBind:

/**
 * 用于识别需要处理那些Java文件
 * 注解在Activity上表示 需要bindview
 */
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.TYPE)
public @interface AutoBind {
    String value();
}

@BindView:

/**
 * 和 ButterKnife 一样 这个注解用于标注在 View上
 */
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface BindView {
    int value (); //用于接受 view 的 id
}

这里要注意以下,这个Module并不是android-library,而是一个java,看一下gradle文件:

//注意这里 是 java
apply plugin: 'java'

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
}
//转码
tasks.withType(JavaCompile){
    options.encoding = "UTF-8"
}

sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8

接着创建api Module,里面定义我们注解的功能,这个module是一个android-library,不需要引入其他库,直接定义我们的注解功能即可:
定义ViewBinder接口抽象功能:

/**
 *  抽象功能
 *
 *  绑定 和 解绑
 */
public interface ViewBinder<T> {
	//T host -- view的容器 activity fragment等
	//Object obj -- view类型
	//ViewFinder finder -- 在容器中绑定view的类 下面创建
	//绑定view 方法
    public void bind(T host, Object obj, ViewFinder finder );

	//解绑view
    public void unBind(T host);
}

定义ViewFinder,用于在容器中绑定view

/**
 *  用于在 Activity中 查找 view
 */
public class ViewFinder {
    public View findView(Object obj, int viewId){
        return ( (Activity) obj).findViewById(viewId);
    }
}

定义MyButterKnife,实现接口中的功能

public class MyButterKnife {

    private static final ViewFinder finder = new ViewFinder();
    private static final Map<String, ViewBinder> binderMap = new LinkedHashMap<>();

    public static void bind(Activity activity){
        bind(activity, activity, finder);
    }

    /**
     * 注解绑定
     * @param host 表示需要注解的类 activity等等
     * @param obj 表示查找 View 的地方 Activity Fragment 等
     * @param finder ui绑定者提供接口
     */
    public static void bind(Object host, Object obj, ViewFinder finder){
        String className = host.getClass().getName();
        ViewBinder binder = binderMap.get(className);
        try {
            if(binder == null){
                Class clazz = Class.forName(className + "$AutoBind");
                binder = (ViewBinder) clazz.newInstance();
                binderMap.put(className, binder);
            }
            if (binder != null) {
                //把finder类跟使用注解类的 类 绑定
                binder.bind(host, obj, finder);
            }
        }catch (Exception e){
            Log.e("MyButterKnife.bind", e.toString());
        }
    }

    public static void unBind(Object host){
        String className = host.getClass().getName();
        ViewBinder binder = binderMap.get(className);
        if(binder != null){
            binder.unBind(host);
        }
        binderMap.remove(className);
    }
}

最后,创建processor Module,主要利用autoservice扫描注解,再通过javapoet生成对应的java文件,processor是一个java module,需要引入auto-service包和javapoet包,并且依赖前面创建的annotations模块。
gradle文件:

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])

    implementation 'com.google.auto.service:auto-service:1.0-rc6'
    annotationProcessor 'com.google.auto.service:auto-service:1.0-rc6'
    implementation 'com.squareup:javapoet:1.7.0'
    implementation project(':annotations')
}
//转码
tasks.withType(JavaCompile){
    options.encoding = "UTF-8"
}

sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8

定义MyProcessor,实现扫描注解和生成代码,类的代码比较多,代码中有大量注释。代码中有两个内部类,AnnotatedClass主要用于 自动生成 class 文件 写入类、方法代码,BindViewFieId 存放被注解 view 的信息;
MyProcessor继承AbstractProcessor,开启注解处理有两种方案,第一种是再META-INF/services 目录文件夹下创建 javax.annotation.processing.Processor 文件,文件中写入MyProcessor全类名;第二种就是再MyProcessor上使用注解@AutoService(Processor.class),这会在编译时帮我们生成第一种方案的文件。
继承自AbstractProcessor后,重写最重要的方法就是process,这个相当于是main方法,入口方法;
代码如下:

/**方案1
 * 运行注解处理器
 * 1、在 processors 库的 main 目录下新建 resources 资源文件夹;
 * 2、在 resources文件夹下建立 META-INF/services 目录文件夹;
 * 3、在 META-INF/services 目录文件夹下创建 javax.annotation.processing.Processor 文件;
 * 4、在 javax.annotation.processing.Processor 文件写入注解处理器的全称,包括包路径;
 */
/** 方案2
 * 每一个注解处理器类都必须有一个空的构造函数,默认不写就行;
 *  类前面标注 @AutoService(Processor.class)
 */
@AutoService(Processor.class)
public class MyProcessor extends AbstractProcessor {

    //Types是一个用来处理TypeMirror的工具
    private Types typeUtils;
    //Elements是一个用来处理Element的工具
    private Elements elementUtils;
    //生成java源码
    private Filer filer;
    //每个需要自动生成的类 对应一个生成类的操作对象
    private Map<String, AnnotatedClass> mAnnotatedClassMap;
    /**
     * Messager提供给注解处理器一个报告错误、警告以及提示信息的途径。
     * 它不是注解处理器开发者的日志工具,
     * 而是用来写一些信息给使用此注解器的第三方开发者的
     */
    private Messager messager;

    /**
     * init()方法会被注解处理工具调用,并输入ProcessingEnviroment参数。
     * ProcessingEnviroment提供很多有用的工具类Elements, Types 和 Filer
     * @param processingEnv 提供给 processor 用来访问工具框架的环境
     */
    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        typeUtils = processingEnv.getTypeUtils();
        elementUtils = processingEnv.getElementUtils();
        filer = processingEnv.getFiler();
        messager = processingEnv.getMessager();
        mAnnotatedClassMap = new TreeMap<>();
    }

    /**
     * 这相当于每个处理器的主函数main(),你在这里写你的扫描、评估和处理注解的代码,以及生成Java文件。
     * 输入参数RoundEnviroment,可以让你查询出包含特定注解的被注解元素
     * @param annotations   请求处理的注解类型
     * @param roundEnvironment  有关当前和以前的信息环境
     * @return  如果返回 true,则这些注解已声明并且不要求后续 Processor 处理它们;
     *          如果返回 false,则这些注解未声明并且可能要求后续 Processor 处理它们
     */
    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnvironment) {
        /**
         * 首先需要把要生成的类所需要的信息都收集到
         *  如:类名,需要的view 和 view 的类型,view 的 resId 等等
         */
        try {
            mAnnotatedClassMap.clear();
            //获取被BindView注解的所有元素
            System.out.println("step ---> Get all @BindView element");
            for(Element element : roundEnvironment.getElementsAnnotatedWith(BindView.class)){
                TypeElement typeElement = (TypeElement) element.getEnclosingElement();
                //获取元素对应的类名
                String viewWithClassName = typeElement.getQualifiedName().toString();
                //如果这个元素对应的类还没添加进mAnnotatedClassMap 则添加进去
                System.out.println("step ---> Get annotatedClass");
                AnnotatedClass annotatedClass = mAnnotatedClassMap.get(viewWithClassName);
                if(annotatedClass == null){
                    annotatedClass = new AnnotatedClass(typeElement, elementUtils);
                    mAnnotatedClassMap.put(viewWithClassName, annotatedClass);
                }
                System.out.println("annotatedClass" + annotatedClass);
                // annotatedClass 中还有对应类中的view信息 根据element获取BindViewFieId对象
                System.out.println("step ---> Get bindViewField");
                BindViewFieId bindViewFieId = new BindViewFieId(element);
                annotatedClass.addField(bindViewFieId);
                System.out.println("bindViewFieId" + bindViewFieId);
            }
        }catch (IllegalArgumentException e){
            System.out.println("Auto bind view is error -----------");
            messager.printMessage(Diagnostic.Kind.ERROR, e.getMessage());
        }
        /**
         *  所有的信息都收集完了之后 开始生成类
         *   view 变量 信息 都在annotatedClass中, 所以 生成类的操作写在 annotatedClass中
         */
        for (AnnotatedClass annotatedClass : mAnnotatedClassMap.values()){
            try {
                System.out.println("step ---> Process start create class");
                System.out.println("annotatedClass:" + annotatedClass);
                annotatedClass.createFile().writeTo(filer);
            } catch (IOException e) {
                System.out.println("process create class error");
                //e.printStackTrace();
                messager.printMessage(Diagnostic.Kind.ERROR, e.getMessage());
            }
        }

        return true;
    }

    /**
     * 这里必须指定,这个注解处理器是注册给哪个注解的。注意,它的返回值是一个字符串的集合,包含本处理器想要处理的注解类型的合法全称
     * @return  注解器所支持的注解类型集合,如果没有这样的类型,则返回一个空集合
     */
    @Override
    public Set getSupportedAnnotationTypes() {
        Set annotataions = new LinkedHashSet();
        annotataions.add(AutoBind.class.getCanonicalName());
        return annotataions;
    }

    /**
     * 指定使用的Java版本,通常这里返回SourceVersion.latestSupported(),默认返回SourceVersion.RELEASE_6
     * @return  使用的Java版本
     */
    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }

    //主要用于 自动生成 class 文件 写入类、方法代码
    static class AnnotatedClass{

        TypeElement typeElement;
        Elements elements;
        //保存被注解 view 的信息
        ArrayList<BindViewFieId> viewList;

        AnnotatedClass(TypeElement typeElement, Elements elements){
            this.typeElement = typeElement;
            this.elements = elements;
            viewList = new ArrayList();
        }

        void addField(BindViewFieId bindViewFieId){
            viewList.add(bindViewFieId);
            System.out.println("AnnotatedClass.viewList : " + viewList);
        }

        static final ClassName BINDER = ClassName.get("com.example.api", "ViewBinder");
        static final ClassName PROVIDER = ClassName.get("com.example.api", "ViewFinder");

        JavaFile createFile(){
            //生成方法 生成自动绑定view的方法 bind
            System.out.println("step ---> createFile - ceate method");
            MethodSpec.Builder bindMethod = MethodSpec
                    .methodBuilder("bind")//方法名字
                    .addModifiers(Modifier.PUBLIC)//修饰符
                    .addAnnotation(Override.class)//方法注解
                    .addParameter(TypeName.get(typeElement.asType()),"host")//方法参数类型 参数名
                    .addParameter(TypeName.OBJECT, "obj")
                    .addParameter(PROVIDER, "viewFinder");
            //方法体 也就是findView
            for (BindViewFieId bindViewFieId : viewList){
                bindMethod.addStatement("host.$N = ($T)(viewFinder.findView(obj, $L))",
                        bindViewFieId.getName(),
                        ClassName.get(bindViewFieId.getType()),
                        bindViewFieId.getId());
            }

            //生成解绑方法 unBind
            MethodSpec.Builder unBindMethod = MethodSpec
                    .methodBuilder("unBind")
                    .addModifiers(Modifier.PUBLIC)
                    .addAnnotation(Override.class)
                    .addParameter(TypeName.get(typeElement.asType()), "host");
            //对绑定过的view 解绑 赋空值
            for (BindViewFieId bindViewFieId : viewList){
                unBindMethod.addStatement("host.$N = null",
                        bindViewFieId.getName());
            }

            //生成类
            System.out.println("step ---> createFile - ceate class");
            TypeSpec bindClass = TypeSpec
                    .classBuilder(typeElement.getSimpleName() + "$AutoBind")//类名
                    .addModifiers(Modifier.PUBLIC)//修饰符
                    .addSuperinterface(ParameterizedTypeName.get(BINDER, TypeName.get(typeElement.asType())))//添加实现的接口
                    .addMethod(bindMethod.build())//添加方法
                    .addMethod(unBindMethod.build())
                    .build();

            String packageName = elements.getPackageOf(typeElement).getQualifiedName().toString();
            JavaFile result = JavaFile.builder(packageName, bindClass).build();
            try {
                result.writeTo(System.out);
            } catch (IOException e) {
                e.printStackTrace();
            }
            return result;
        }
    }

    //被注解 view 的信息
    static class BindViewFieId{
        private VariableElement variableElement;
        private int resId;

        BindViewFieId(Element element){
            //判断是否注解的是 view 变量
            if(element.getKind() == ElementKind.FIELD){
                variableElement = (VariableElement) element;
                System.out.println("BindViewField variableElement: " + variableElement);
                BindView bindView = variableElement.getAnnotation(BindView.class);
                resId = bindView.value();
                System.out.println("BindViewField resId: " + resId);
                //如果 注解中的 view id 不正确 则抛出异常
                if(resId < 0){
                    throw new IllegalArgumentException("@BindView get value is invalid");
                }
            }else { //@BindView不是用在注解上 则抛出异常
                throw new IllegalArgumentException("@BindView only can be annotated with fields");
            }
        }

        // 获取 view 名称
        Name getName(){
            return variableElement.getSimpleName();
        }
        // 获取 view resId
        int getId(){
            return resId;
        }
        // 获取 view 类型
        TypeMirror getType(){
            return variableElement.asType();
        }
    }
}

以上创建完成后,我们在app中依赖三个模块,在gradle文件中添加:

dependencies {
	implementation project(':annotations')
    implementation project(':api')
    annotationProcessor project(':processor')
}

在MainActivity中使用我们的注解:

在这里插入代码片

完成代码后,我们先build以下项目,会发现自动生成了Java文件:
在这里插入图片描述
运行效果:
在这里插入图片描述
在MainActivity中并没有给TextView添加findviewbyid代码,直接可以设置单击事件。当然,这个demo只是为了熟悉apt流程,功能并不完整。

最后附上Demo的地址:https://github.com/RDSunhy/Study_APT

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值