Android编译时注解——APT技术

APT(Annotation Processing Tool)即注解处理器,是一种处理注解的工具,确切的说它是javac的一个工具,它用来在编译时扫描和处理注解。注解处理器以Java代码(或者编译过的字节码)作为输入,生成.java文件作为输出。
简单来说就是在编译期,通过注解生成.java文件。

运行时注解(@Retention(RetentionPolicy.RUNTIME)):之前讲得运行时注解,是在程序运行的时候,获取和解析注解的,并且在获取一些信息后通过反射达到效果。运行时注解的例子

编译时注解(@Retention(RetentionPolicy.CLASS)):编译时注解在编译的时候需要用到代理模式,生成对应java文件,在程序运行的时候,使用该java文件,并调用方法来达到预期的效果。

下面来一步步实现APT:

1.创建注解类:

将注解类放到单独的一个子module中,如:apt_annotation

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

2.创建AbstractProcessor实现类:

一般将这些相关的类放在一个子module中管理,如:apt_processor

@AutoService(Processor.class)
public class InjectViewProcessor extends AbstractProcessor {

    private Messager mMessager;
    private Elements mElementUtils;
    private Map<String, ClassGenerateProxy> mProxyMap = new HashMap<>();

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

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        HashSet<String> supportTypes = new LinkedHashSet<>();
        /*** 这里是将使用到的注解类加入到集合中 ***/
        supportTypes.add(InjectView.class.getCanonicalName());
        return supportTypes;
    }

    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        /*** 这里主要是生成需要的Java文件,因此这里是关键的地方 ***/
        ...
        return true;
    }

其中的init、 getSupportedAnnotationTypes、getSupportedSourceVersion三个方法的写法是固定的,关键的地方是process方法,这里需要用来创建Java类文件,可以使用比较好用的第三方库:JavaPoet。

那么AbstractProcessor实现类的方法什么时候执行呢?这个时候需要运用到SPI技术。注意到BindViewProcessor类上面有一个注解@AutoService(Processor.class),这个是google的第三方库,需要在gradle里面引用:

implementation 'com.google.auto.service:auto-service:1.0-rc2'

但是有时候这种方法不生效,原因我也不是很清楚。。。

因此最稳的方法就是自己手动创建相关文件夹:

其中javax.annotation.processing.Processor里面的内容是AbstractProcessor实现类:BindViewProcessor类的路径:

com.zy.apt_processor. InjectViewProcessor

 3.build,生成需要的Java类:

app模块需要编译子模块,如:

implementation project(':apt_annotation')
annotationProcessor project(':apt_processor')

build app模块,此时就会在build目录下生成对应的Java文件,如:

 4.使用注解:

在Activity中使用注解:

    @InjectView(R.id.tv)
    TextView mTextView;
    @InjectView(R.id.btn)
    Button mButton;

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

此时虽然咱们已经生成了Java类,但是还没有使用,那么使用如下:

1.Android Studio会在Build里面生成相应的Java类,所以,可以直接在程序中调用该类方法,XXXViewBinding.bind(this);

2.也可以自己编写一个注解注入的工具类:

这里是一个简单例子,其中使用反射拿到这个类的class对象和Method对象,来执行 注解注入的方法“public void bind(Activity activity)”

public class InjectViewTools {

    public static void bind(Activity activity) {

        Class clazz = activity.getClass();
        try {
            Class bindViewClass = Class.forName(clazz.getName() + "_ViewBinding");
            Method method = bindViewClass.getMethod("bind", activity.getClass());
            method.invoke(bindViewClass.newInstance(), activity);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
    }
}

那么,到这里咱们的APT的简单实践就完成了,这里是使用编译时注解来省略程序员写的findViewById,当然实际上是没有省去的,因为编译时注解只是在默默地帮你写了段findViewById的程序,咱们就只需要使用注解将View注入,就达到了解耦的目的。很多第三方都是这么做的,比如号称超级解耦的retrofit也是使用编译时注解。

 

此处补充一下AbstractProcessor实现类的方法体代码(本文的例子):

@AutoService(Processor.class)
public class InjectViewProcessor extends AbstractProcessor {

    private Messager mMessager;
    private Elements mElementUtils;
    private Map<String, ClassGenerateProxy> mProxyMap = new HashMap<>();

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

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        HashSet<String> supportTypes = new LinkedHashSet<>();
        supportTypes.add(InjectView.class.getCanonicalName());
        return supportTypes;
    }

    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        mMessager.printMessage(Diagnostic.Kind.NOTE, "processing...");
        mProxyMap.clear();
        //得到所有的注解
        Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(BindView.class);
        for (Element element : elements) {
            VariableElement variableElement = (VariableElement) element;
            TypeElement classElement = (TypeElement) variableElement.getEnclosingElement();
            String fullClassName = classElement.getQualifiedName().toString();
            //elements的信息保存到mProxyMap中
            ClassGenerateProxy proxy = mProxyMap.get(fullClassName);
            if (proxy == null) {
                proxy = new ClassGenerateProxy(mElementUtils, classElement);
                mProxyMap.put(fullClassName, proxy);
            }
            InjectView bindAnnotation = variableElement.getAnnotation(BindView.class);
            int id = bindAnnotation.value();
            proxy.putElement(id, variableElement);
        }
        //通过javapoet生成
        for (String key : mProxyMap.keySet()) {
            ClassGenerateProxy proxyInfo = mProxyMap.get(key);
            JavaFile javaFile = JavaFile.builder(proxyInfo.getPackageName(), proxyInfo.generateJavaCode2()).build();
            try {
                // 生成文件
                javaFile.writeTo(processingEnv.getFiler());
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        mMessager.printMessage(Diagnostic.Kind.NOTE, "process finish ...");
        return true;
    }
}

ClassCreatorProxy类:主要完成Java类文件的创建过程 

public class ClassGenerateProxy {
    private String mBindingClassName;
    private String mPackageName;
    private TypeElement mTypeElement;
    private Map<Integer, VariableElement> mVariableElementMap = new HashMap<>();

    public ClassCreatorProxy(Elements elementUtils, TypeElement classElement) {
        this.mTypeElement = classElement;
        PackageElement packageElement = elementUtils.getPackageOf(mTypeElement);
        String packageName = packageElement.getQualifiedName().toString();
        String className = mTypeElement.getSimpleName().toString();
        this.mPackageName = packageName;
        this.mBindingClassName = className + "_ViewBinding";
    }

    public void putElement(int id, VariableElement element) {
        mVariableElementMap.put(id, element);
    }

    /**
     * 创建Java代码
     * javapoet
     *
     * @return
     */
    public TypeSpec generateJavaCode2() {
        TypeSpec bindingClass = TypeSpec.classBuilder(mBindingClassName)
                .addModifiers(Modifier.PUBLIC)
                .addMethod(generateMethods2())
                .build();
        return bindingClass;

    }

    /**
     * 加入Method
     * javapoet
     */
    private MethodSpec generateMethods2() {
        ClassName host = ClassName.bestGuess(mTypeElement.getQualifiedName().toString());
        MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder("bind")
                .addModifiers(Modifier.PUBLIC)
                .returns(void.class)
                .addParameter(host, "host");

        for (int id : mVariableElementMap.keySet()) {
            VariableElement element = mVariableElementMap.get(id);
            String name = element.getSimpleName().toString();
            String type = element.asType().toString();
            methodBuilder.addCode("host." + name + " = " + "(" + type + ")(((android.app.Activity)host).findViewById( " + id + "));");
        }
        return methodBuilder.build();
    }


    public String getPackageName() {
        return mPackageName;
    }
}

注意:这里的mMessager.printMessage是日志打印,这里也可以使用System.out.print(),这些日志的打印信息可以在Build out中找到,也就是在Android Studio最左下角有个build栏,点击打开就可以看到了。有时候代码没有达到预期的效果,可以通过看日志找到原因。如图:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值