框架开发使用注解处理器APT优雅提效

概述

在现在的很多开源框架中,我们经常能在源码中看到注解处理器的影子,比如我们熟悉的阿里的ARouter,Android开发中的替代findViewById神器黄油刀ButterKnif,事件总线EventBus等都使用了注解处理器APT技术,下图是ARouter的项目结构图部分截图:

图片.png

图中红圈部分就是ARouter注解处理器技术的实现模块。使用注解处理器还有一个好处就是可以解决部分功能因反射而带来的损耗问题,注意,这里不是说注解处理器的技术是取代反射的哈,我们使用反射其实就是想在程序运行期间动态获取到某个对象,再操作这个对象的对应方法完成我们想要实现的功能。但是这个过程是耗时的。而注解处理器是在编译时生成我们想要实现的功能的对应代码,比较典型的就是EventBus的实现从反射技术转为注解处理器的技术的应用。接下来,本文会介绍什么是注解处理器和如何使用注解处理器优雅的提高我们的开发效率。

1.什么是注解处理器APT

注解处理器(Annotation Process Tool)顾名思义就是一种处理注解的工具,它可以极大的优化我们平时写的冗余代码,比较典型的就是Android开发者经常写的findViewById,这类代码基本都是差不多的,人工去写不仅多,而且还容易出错,使用注解处理器技术优化后就只需要两行代码:

  @DIView(value = R.id.tv_text)
    TextView textView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }

如上面的代码中,@DIView(value = R.id.tv_text)就是注解处理器的一部分,这个注解就是提供给调用方使用的,然后在自定义的注解中传入关键的参数值,注解处理器拿到这个关键的参数值后再将一些重复性高的代码统一生成,最后再和打包到应用程序中,所以我们在使用APT技术的时候的自定义注解都是设置在编译期间有效。如ARounter的自定义注解Route的有效范围就是编译期间有效:@Retention(RetentionPolicy.CLASS)

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.CLASS)
public @interface Route {

    /**
     * Path of route
     */
    String path();

    /**
     * Used to merger routes, the group name MUST BE USE THE COMMON WORDS !!!
     */
    String group() default "";

    /**
     * Name of route, used to generate javadoc.
     */
    String name() default "";

    /**
     * Extra data, can be set by user.
     * Ps. U should use the integer num sign the switch, by bits. 10001010101010
     */
    int extras() default Integer.MIN_VALUE;

    /**
     * The priority of route.
     */
    int priority() default -1;
}

其实很好理解,因为我们使用APT技术主要是为我们在编译期间生成一些我们不想写的重复性代码,当代码生成完后,这些注解也就完成了它们的工作。除非这些注解在代码运行的时候还需要反射使用,否则我们都没有必要将这些注解范围定义成运行时有效。

至此,相信读者已经明白注解处理器是做啥的了,用一句话概括就是,注解处理器是处理我们自定义注解的工具,它帮助我们处理生成那些重复性比较高的代码,让我们不用去关心和处理那些重复的逻辑。

2.应用场景

那么注解处理器的应用场景主要有哪些呢,其实注解处理器是为了解放开发者的,它是一个面向开发者的工具,它的主要应用场景就是做框架的开发,例如阿里的ARouter路由框架,网络请求框架Retrofit,事件总线EventBus等,所以APT注解处理器技术是一个简化我们开发的工具,我们开发框架的时候,可以提取出框架中的很多逻辑差不多,但又大量重复的内容,使用注解处理器去优化。对注解处理器感兴趣的读者,建议去阅读下上面提到的几个框架源码,写的特别好。

3.如何使用

接下来,到了最关键的时刻了,说一千道一万,不会使用就完蛋,接下来利用我在视频网站上学到的一个类似黄油刀的例子,来介绍APT技术的使用方法。我们只实现一个功能,就是使用我们定义的注解去注解Android的View,然后直接使用这个View,如下所示:

@DIActivity
public class MainActivity extends AppCompatActivity {
    @DIView(value = R.id.tv_text)
    TextView textView;

    @DIView(value = R.id.tv_text_1)
    TextView textView1;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        // bindView方法其实就是取代findViewById的替代
        DIMainActivity.bindView(this);

        textView.setText("hello apt!!!!");

        textView1.setText("hello apt again!!!!");
        textView1.setTextSize(15);
    }
}

我们使用一个DIActivity注解我们的类,然后使用@DIView(value = R.id.tv_text) 注解我们声明的View,然后直接在程序中使用被我们注解的属性。然后框架就能帮我们实现这个对应的findViewByID 的逻辑。接下来是实现这个功能的步骤:

3.1 创建注解API模块

这里我们需要新建一个Java Library模块,记住一定是Java Library,这个模块主要是用于做自定义注解的定义,给注解处理器和调用者使用。

图片.png

然后定义好我们需要使用到的自定义注解

DIActivity 注解用于我们要使用的Activity上,我们就是通过它来将生成的代码创建出的对象赋值给我们使用的Activity的。

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.CLASS)
public @interface DIActivity {

}

DIView 用于我们要使用的View上,调用者只需使用这个注解传入View的ID值就可以得到一个View对象了

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

3.2 创建注解处理器模块

创建完自定义注解类,我们接下来就是对注解处理器模块的介绍了,框架的主要工作都是在注解处理器内完成,我们同样新建一个Java Library模块,用于处理我们定义的注解。如下图所示

图片.png

我们需要注意的是在这个模块中,我们需要去引入对应的库依赖,来协助我们完成一些复杂繁琐的工作,我们需要引入下面几个库:

    implementation project(path: ':annotation') // 我们定义的注解模块,因为要处理注解,
      // 所以必须依赖上
    // auto-service 是Google提供的,辅助我们开发注解处理器
   
    annotationProcessor 'com.google.auto.service:auto-service:1.0-rc7'
    compileOnly 'com.google.auto.service:auto-service:1.0-rc7'
    // 这个是一个代码生成框架,利用它可以优雅的生成我们想要的代码
    implementation 'com.squareup:javapoet:1.10.0'

注意:引入com.google.auto.service:auto-service:1.0-rc7这个依赖时使用的是annotationProcessor ,需要注意,不然不会报错,但是也无法生成我们想要的代码 配置好了就可以进行开发了,处理前面注解的代码如下:

@AutoService(Processor.class)
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class DIProcessor extends AbstractProcessor {
private Elements elementUtils;
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        return Collections.singleton(DIActivity.class.getCanonicalName());
    }

    @Override
    public boolean process(Set<? extends TypeElement> set,
     RoundEnvironment roundEnvironment) 
    {
        Set<? extends Element> elements = 
        roundEnvironment.getElementsAnnotatedWith(DIActivity.class);
        for (Element element:elements){
            // 判断是否为class
            TypeElement typeElement = (TypeElement) element;
            List<? extends Element> allMembers = 
            elementUtils.getAllMembers(typeElement);
            MethodSpec.Builder bindViewMethodSpecBuilder = 
            MethodSpec.methodBuilder("bindView")
                    .addModifiers(Modifier.PUBLIC,Modifier.STATIC)
                    .returns(TypeName.VOID)
                    .addParameter(ClassName.get(typeElement.asType()),"activity");

            for(Element item :allMembers){
                DIView diView = item.getAnnotation(DIView.class);
                if (diView == null) {
                    continue;
                }

                bindViewMethodSpecBuilder.addStatement(String.format("activity.%s = (%s)" 
                +"activity.findViewById(%s)",item.getSimpleName(),
                ClassName.get(item.asType()).toString(),diView.value()+""));
            }

            TypeSpec typeSpec = TypeSpec.classBuilder("DI" + element.getSimpleName())
                    .superclass(TypeName.get(typeElement.asType()))
                    .addModifiers(Modifier.PUBLIC,Modifier.FINAL)
                    .addMethod(bindViewMethodSpecBuilder.build())
                    .build();

            JavaFile javaFile = 
            JavaFile.builder(getPackageName(typeElement),typeSpec).build();
            try {
                javaFile.writeTo(processingEnv.getFiler());
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
        return false;
    }

    private String getPackageName(TypeElement typeElement) {
        return elementUtils.getPackageOf(typeElement).getQualifiedName().toString();
    }

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        elementUtils = processingEnv.getElementUtils();
        super.init(processingEnv);
    }
}

上面的代码很简单,大致意思就是,使用工具类,获取注解的信息,然后生成我们想要的代码,最后写到文件里面。当我们要使用对应功能的时候,直接在程序中调用就可以了。在上面代码中出现的如下代码块:

TypeSpec typeSpec = TypeSpec.classBuilder("DI" + element.getSimpleName())
                    .superclass(TypeName.get(typeElement.asType()))
                    .addModifiers(Modifier.PUBLIC,Modifier.FINAL)
                    .addMethod(bindViewMethodSpecBuilder.build())
                    .build();

就是com.squareup:javapoet:1.10.0框架干的事情,以面向对象的方式生成代码,如果没有这个框架,那么我们需要使用字符串来拼接出我们想要生成的类。比如EventBus的注解处理器就是用的字符拼接:

图片.png

3.3 使用注解

接下来是使用注解,使用之前我们需要引入相关的依赖,首先,我们需要使用我们自定义的注解,所以需要引入注解模块的依赖,其次我我们需要依赖注解处理器模块,如下所示:

  implementation project(':annotation')
  annotationProcessor project(':annotation-processor')

注意:引用注解处理器模块时要用:annotationProcessor project(':annotation-processor'),是annotationProcessor 不是Implementation,如果使用错误会导致无法生成我们想要的目标代码

接下来就是使用注解就可以了。

@DIActivity
public class MainActivity extends AppCompatActivity {
    @DIView(value = R.id.tv_text)
    TextView textView;

    @DIView(value = R.id.tv_text_1)
    TextView textView1;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        // bindView方法其实就是取代findViewById的替代
        DIMainActivity.bindView(this);

        textView.setText("hello apt!!!!");

        textView1.setText("hello apt again!!!!");
        textView1.setTextSize(15);
    }
}

当我们编译代码的时候,会在下图路径中生成我们在注解处理器中拼接出的类,看到类的内容其实我们就理解了框架帮我们干的事情了。如果没有生成我们想要生成的类,建议读者去检查下自己的代码中是否又错误,引入依赖是否引错。

在这里插入图片描述至此,注解处理器APT的内容就讲完了,读者还是很有必要了解这个内容的,因为现在很多框架中都使用这个技术,项目的源码比较简单,这里就不提供了,需要的可以评论区留言,留下邮箱,建议读者手动去实现一遍。

猜你喜欢

转载自juejin.im/post/7255968185681149989
今日推荐