Android程序员必会技能---运行时动态生成类---之编译期注解

除了动态代理,还有哪些方法能够方便的自动帮咱们生成类呢?

应该说,除了动态代理,还有注解和字节码注入(主要是asm和aspectj)两种方法能够方便的帮咱们自动生成类。 关于字节码注入,咱们下一节再说,今天只说注解,尤为是编译期注解,运行时注解由于大量要在运行期用到反射 因此不少人都不用了改用编译期注解了。比方说eventbus这个框架 2.0到3.0 主要就是修改的这个。因此运行时z 注解就留给感兴趣的人本身看看吧,比编译期注解简单多了java

编译期注解能帮咱们解决什么问题?

假设如今有个需求: 输入商品编码,返回对应的服装品牌并输出他们的价格。 显然这是一个工厂模式就能解决的简单问题。 首先定义一个接口:android

package com.longfor.retentiontest;

public interface IClothes {
    void getPrice();
}

复制代码

而后定义三个服装品牌api

package com.longfor.retentiontest;

import android.util.Log;

import com.longfor.annotationprocessorjavalib.Factory;


public class BandNike implements IClothes {

    @Override
    public void getPrice() {
        Log.v("wuyue", "nike price is 500");
    }
}

复制代码
package com.longfor.retentiontest;

import android.util.Log;

import com.longfor.annotationprocessorjavalib.Factory;

public class BandLevis implements IClothes {
    @Override
    public void getPrice() {
        Log.v("wuyue", "levis price is 5000");
    }
}
复制代码
package com.longfor.retentiontest;

import android.util.Log;

import com.longfor.annotationprocessorjavalib.Factory;

public class BandGucci implements IClothes {
    @Override
    public void getPrice() {
        Log.v("wuyue", "gucci price is 5000");
    }
}

复制代码

而后一个工厂解决咱们全部问题:bash

package com.longfor.retentiontest;

public class ClothesFactory {
    public static IClothes getBandClothesByNumber(int number) {
        if (number == 1) {
            return new BandNike();
        } else if (number == 2) {
            return new BandGucci();
        } else if (number == 3) {
            return new BandLevis();
        }
        return null;
    }
}

复制代码

这段代码其实还有优化空间的,好比说这品牌咱们暂时只有3个,若是新增到30个 300个 怎么处理?尤为是这个方法要提供 给别的模块使用的时候?不能每新加一个就从新写一遍工厂类 从新打包吧。app

因此解决问题的方法就是:用编译期注解的方法 来动态的帮咱们生成工厂类,而后新增的品牌只要加上咱们的注解就ok。 由于编译期注解生成的工厂类 会根据咱们的注解来写好对应的语句。框架

如何在 androidstudio中 编写编译期注解代码

首先咱们new一个java libide

注意他的build文件的写法:函数

apply plugin: 'java-library'
tasks.withType(JavaCompile) {
    options.encoding = "UTF-8"
}
dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    //协助咱们生成meta-inf 等信息的
    implementation 'com.google.auto.service:auto-service:1.0-rc2'
    //square公司 协助生成java代码的辅助库
    implementation 'com.squareup:javapoet:1.7.0'

}

sourceCompatibility = "7"
targetCompatibility = "7"

复制代码

而后看下咱们的app的build文件:工具

apply plugin: 'com.android.application'

android {
    compileSdkVersion 27
    defaultConfig {
        applicationId "com.longfor.retentiontest"
        minSdkVersion 21
        targetSdkVersion 27
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'com.android.support:appcompat-v7:27.1.1'
    implementation 'com.android.support.constraint:constraint-layout:1.1.0'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'com.android.support.test:runner:1.0.2'
    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
    //主要就是下面的代码
    compile project(":AnnotationProcessorJavaLib")
    //注意不要遗漏这个
    annotationProcessor project(":AnnotationProcessorJavaLib")

}

复制代码

而后咱们能够开始写咱们的注解,先看一下基本结构:学习

而后看一下具体写法:

package com.longfor.annotationprocessorjavalib;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.CLASS)
@Target({ElementType.TYPE})
public @interface Factory {
    //咱们生成的工程类 就根据这个id的值 来判断到底要new 哪一个对象
    int id();
}

复制代码

看下咱们最关键的注解处理器的写法:

package com.longfor.annotationprocessorjavalib;

import com.google.auto.service.AutoService;
import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.JavaFile;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.TypeSpec;

import java.io.IOException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;

import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Filer;
import javax.annotation.processing.Messager;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.Processor;
import javax.annotation.processing.RoundEnvironment;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;
import javax.lang.model.util.Elements;
import javax.lang.model.util.Types;

/**
 * 自定义注解处理器
 */
@AutoService(Processor.class)
public class FactoryProcessor extends AbstractProcessor {
    Types mTypeUtils;
    /**
     * 用来处理Element的工具类,Element表明源代码 好比类名 包名 方法名 等等
     */
    Elements mElementUtils;
    Filer filer;
    //日志输出用的,不须要能够不用
    Messager messager;


    /**
     * 这个初始化的代码不要忘记了
     *
     * @param processingEnvironment
     */
    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
        mTypeUtils = processingEnvironment.getTypeUtils();
        mElementUtils = processingEnvironment.getElementUtils();
        filer = processingEnvironment.getFiler();
        messager = processingEnvironment.getMessager();
    }

    HashMap<Integer, String> idClassMap = new HashMap<>();
    private String mSupperClsQualifiedName; //被注解的类的父类的彻底限定名称(即包名+类名)

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {

        //由于Element能够是 类 方法 或者变量
        for (Element annotatedElement : roundEnvironment.getElementsAnnotatedWith(Factory.class)) {
            //因此这里要判断 只有是注解到类的才处理
            if (annotatedElement.getKind() == ElementKind.CLASS) {
                //TypeElement能够获取类的名字 可是获取不到 类的信息
                TypeElement typeElement = (TypeElement) annotatedElement;
                Factory annotation = typeElement.getAnnotation(Factory.class);
                //把咱们的id 取出来
                int id = annotation.id();
                //把咱们的类名取出来
                String className = typeElement.getQualifiedName().toString();
                //而后放到map里面 创建咱们的id--class的 对应关系
                idClassMap.put(id, className);

            }
        }

        //而后生成咱们的类
        generateCode();
        return false;
    }

    /**
     * 其实生成java代码 就是写一个字符串的事。。。你能够本身拼字符串 拼成 java 源代码的
     * <p>
     * 这里示范 咱们用javapoet 来协助咱们生成java源代码, 其实对于简单的注解来说
     * <p>
     * 直接写字符串可能更方便。javapoet适合构建比较复杂的java源码
     */
    public void generateCode() {
        //整体来讲 (TypeSpec)用于建立类或者接口,(FieldSpec)用来建立字段,(MethodSpec)用来建立方法和构造函数,(ParameterSpec)用来建立参数,(AnnotationSpec)用于建立注解

        //建立一个class对象 来告诉MethodSpec 咱们要生成的方法 返回值的类型是什么
        ClassName iClothes = ClassName.get("com.longfor.retentiontest", "IClothes");

        MethodSpec.Builder method = MethodSpec.methodBuilder("getBandClothesByNumber")
                .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
                .returns(iClothes)
                .addParameter(Integer.class, "number");

        Iterator iterator = idClassMap.entrySet().iterator();
        //设置循环次数
        int times = 0;
        //这边是生成方法体的语句,对于咱们来讲 重要的是理解 $L $S $T $N 的区别
        while (iterator.hasNext()) {
            Map.Entry entry = (Map.Entry) iterator.next();
            Integer id = (Integer) entry.getKey();
            String className = (String) entry.getValue();
            times++;
            ClassName cls = ClassName.get("com.longfor.retentiontest", className);
            if (times == 1) {
                method.beginControlFlow("if (number==$L)", id)
                        .addStatement("return new $T()", cls).endControlFlow();
            } else {
                method.beginControlFlow("else if (number==$L)", id)
                        .addStatement("return new $T()", cls).endControlFlow();
            }
        }
        method.addStatement("return null");

        /**
         * TypeSpec用来建立类或者接口
         */
        TypeSpec annotationClothesFactory = TypeSpec.classBuilder("AnnotationClothesFactory")
                .addModifiers(Modifier.PUBLIC)
                .addMethod(method.build())
                .build();

        JavaFile javaFile = JavaFile.builder("com.longfor.retentiontest", annotationClothesFactory)
                .build();

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

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> annotations = new LinkedHashSet<>();
        //配置须要处理的注解 这里只处理咱们本身的注解
        annotations.add(Factory.class.getCanonicalName());
        return annotations;
    }

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

复制代码

注释写的很详细了,我就很少啰嗦了,还有看不懂的能够在下面留言问。

而后咱们再回到主app包下,把咱们的注解加上去:

package com.longfor.retentiontest;

import android.util.Log;

import com.longfor.annotationprocessorjavalib.Factory;


@Factory(id = 2)
public class BandGucci implements IClothes {
    @Override
    public void getPrice() {
        Log.v("wuyue", "gucci price is 5000");
    }
}

package com.longfor.retentiontest;

import android.util.Log;

import com.longfor.annotationprocessorjavalib.Factory;

@Factory(id = 3)
public class BandLevis implements IClothes {
    @Override
    public void getPrice() {
        Log.v("wuyue", "levis price is 5000");
    }
}

package com.longfor.retentiontest;

import android.util.Log;

import com.longfor.annotationprocessorjavalib.Factory;


@Factory(id = 1)
public class BandNike implements IClothes {

    @Override
    public void getPrice() {
        Log.v("wuyue", "nike price is 500");
    }
}

复制代码

而后咱们rebuild一下工程,

你看 ,这个自动生成的类就写好了,而后咱们看一下写的符合不符合咱们要求:

package com.longfor.retentiontest;

import java.lang.Integer;

public class AnnotationClothesFactory {
  public static IClothes getBandClothesByNumber(Integer number) {
    if (number==1) {
      return new com.longfor.retentiontest.BandNike();
    }
    else if (number==2) {
      return new com.longfor.retentiontest.BandGucci();
    }
    else if (number==3) {
      return new com.longfor.retentiontest.BandLevis();
    }
    return null;
  }
}

复制代码

嗯 看样子很完美,能够正常使用。 注意这里其实不少信息均可以利用注解相关的类来自动获取,我这里为了简单方便 不少地方是写死值的,若是大家要写的注解真的要作成sdk的话,这边最好能用代码获取判断各类异常状况。

butterknife 是如何实现的?

有了上面的基础,相信butterknife的原理你必定能搞清楚了。实际上butterknife就是利用编译期注解 生成一个名字为 xxxActivity$$ViewBinder的类,在这个类的构造函数里面自动帮咱们执行了findviewById等方法罢了。 而后在ButterKnife.bind方法里面利用反射的技术来手动的调用咱们这个生成的类的构造方法。 至此就完成了绑定的操做了。

如何学习编译器注解

在读完我这篇文章之后,若是想深刻学习编译期注解的话 建议先好好读一下这篇文章 如何更优秀的写出编译期注解

读彻底部弄懂之后仍是建议你们至少在evnetbus和butterknife里面 至少选择一份源码进行精读。 在精读源码的过程当中仔细弄懂注解中Element及其余的子类的各类用法,以及javapoet的各类api的使用。

有了上面的基础再碰到须要编译期注解才能完成的工做时就会事半功倍了。

最后问你们一个小问题,正在学习编译期注解的大家,是如何在android studio中查看注解过程当中的日志的呢? 换句话说,在学习的过程当中我应该如何输出一些日志 到android stuido中方便我学习呢?知道的同窗请留言与我分享谢谢~

相关文章
相关标签/搜索