APT自动化生成代码整理

说到java的apt技术,其实已经算不是很陌生了,在以前阅读第三方框架butterknifeDagger2等框架的时候,看到过apt的影子。他是squareup公司出的javapoet技术,通过在java的编译时期生成类,提高了在运行时期通过反射调用的效率。大家试想一下,如果butterknife所有的注解在运行时期都通过反射调用相应的findViewById的话,那得多慢啊。所以可以看到butterknife都是通过apt技术来生成相应的_ViewBinding,大家可以看下app-->build-->generated-->source-->apt下面找到对应的_ViewBinding。好了废话不多说,咋们下面来直接来撸码。

实现功能还是跟butterknife框架findViewById的功能一样,经过前几篇的学习反射,注解所以才有今天的apt技术代码,所以不熟悉反射跟注解的伙伴们,还是先看下反射和注解如何使用。

  • android studio中创建一个java library的module,这里我起名字叫binder_annotation,专门用来放注解的,这里生成后是这个样子:


    image.png
  • 接着在刚创建的binder_annotation中创建注解
//在编译期起作用的注解
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface BindView {
    int value();
}
  • 创建java library的module,这里我起名字叫binder_compiler。先创建了后面再说

  • 在app的module的build.gradle通过annotationProcessor添加依赖:

dependencies {
    ....
    annotationProcessor project(':binder_compiler')
    implementation project(':binder_annotation')
}

这里注意了,在gradle tool>=2.2之后直接用annotationProcessor添加apt的依赖,如果是在gradle tool<2.2首先得在project添加:

classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'

然后在app的build.gradle添加:

apply plugin: 'com.neenbedankt.android-apt'

添加依赖的地方:

apt project(':binder_compiler')

此处我用的是gradle tools 3.4.1因此直接用annotationProcessor添加依赖。

说完了整体的架子,下面来到binder_compiler下面,我们创建BinderProcessor类,并且继承于AbstractProcessor,该类是在编译期会进行类扫描的处理类。咋们需要实现,在实现之前需要了解几个方法:

//该方法指定类处理器是什么java版本,一般返回SourceVersion.latestSupported()表示最新的java版本就行
@Override
public SourceVersion getSupportedSourceVersion() {
    return SourceVersion.latestSupported();
}
//指明有哪些注解需要被扫描到,返回注解的路径
@Override
public Set<String> getSupportedAnnotationTypes() {
    //大部分class而已getName、getCanonicalNam这两个方法没有什么不同的。
    //但是对于array或内部类等就不一样了。
    //getName返回的是[[Ljava.lang.String之类的表现形式,
    //getCanonicalName返回的就是跟我们声明类似的形式。
    HashSet<String> supportTypes = new LinkedHashSet<>();
    supportTypes.add(BindView.class.getCanonicalName());
    return supportTypes;
    //因为兼容的原因,特别是针对Android平台,建议使用重载getSupportedAnnotationTypes()方法替代默认使用注解实现
}

在初始化的时候获取到扫描对象:

@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
    super.init(processingEnvironment);
    //processingEnvironment.getElementUtils(); 处理Element的工具类,用于获取程序的元素,例如包、类、方法。
    //processingEnvironment.getTypeUtils(); 处理TypeMirror的工具类,用于取类信息
    //processingEnvironment.getFiler(); 文件工具
    //processingEnvironment.getMessager(); 错误处理工具
    //初始化的时候获取到当前扫描的对象
    //processingEnv是父类定义的ProcessingEnvironment对象,其实就是init方法回传过来的
    mElementUtils = processingEnv.getElementUtils();
}

我们的重头戏来了process方法:

@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
    //扫描整个工程   找出含有BindView注解的元素
    //找到所有带有BindView注解的类,生成对应的****_ViewBinding类
    Set<? extends Element> elements =
            roundEnvironment.getElementsAnnotatedWith(BindView.class);
    //遍历元素
    for (Element element : elements) {
        //BindView限定了只能属性使用,这里强转为VariableElement,如果是在类上面的,那么就是typeElement
        VariableElement variableElement = (VariableElement) element;
        //返回此元素直接封装(非严格意义上)的元素。
        //类或接口被认为用于封装它直接声明的字段、方法、构造方法和成员类型
        //这里就是获取封装属性元素的类元素
        TypeElement classElement = (TypeElement) variableElement.getEnclosingElement();
        //获取简单类名
        String fullClassName = classElement.getQualifiedName().toString();
        //里面放的是BinderClassCreator,关键生成***_ViewBinding类在里面生成的
        BinderClassCreator creator = mCreatorMap.get(fullClassName);
        if (creator == null) {
            creator = new BinderClassCreator(mElementUtils.getPackageOf(classElement),
                    classElement);
            //生成之后就放到map中,方便下次使用
            mCreatorMap.put(fullClassName, creator);
        }
        //获取元素注解
        BindView bindAnnotation = variableElement.getAnnotation(BindView.class);
        //注解值
        int id = bindAnnotation.value();
        creator.putElement(id, variableElement);
    }
    for (String key : mCreatorMap.keySet()) {
        BinderClassCreator binderClassCreator = mCreatorMap.get(key);
        //通过javapoet构建生成Java类文件
        //第一个参数传入包名
        //第二个参数传入TypeSpec
        JavaFile javaFile = JavaFile.builder(binderClassCreator.getPackageName(),
                binderClassCreator.generateJavaCode()).build();
        try {
            javaFile.writeTo(processingEnv.getFiler());
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    return false;
}

process方法里面主要做了几件事:

  • 扫描工程里面带有BindView注解的类
  • 通过注解的类拿到类信息,生成BinderClassCreator对象,然后放到map中,将id和VariableElement对象给BinderClassCreator对象,后面会用到
  • 最后生成javaFile对象,通过writeTo生成对应的***_ViewBinding。

在上面代码中将每一个要生成***_ViewBinding类的工作都交给了BinderClassCreator类,其实最关心的还是该类:
首先看构造方法:

public static final String ParamName = "view";
private TypeElement mTypeElement;
private String mPackageName;
private String mBinderClassName;
//key是view在xml中的id,value是作用在类上面的element对象
private Map<Integer, VariableElement> mVariableElements = new HashMap<>();
/**
 * @param packageElement 包元素
 * @param classElement   类元素
 */
public BinderClassCreator(PackageElement packageElement, TypeElement classElement) {
    this.mTypeElement = classElement;
    mPackageName = packageElement.getQualifiedName().toString();
    mBinderClassName = classElement.getSimpleName().toString() + "_ViewBinding";
}

构造器基本没做什么,主要是初始化包名和class类名。

存储了id和作用在类上面的element对象
public void putElement(int id, VariableElement variableElement) {
    mVariableElements.put(id, variableElement);
}
//生成类的代码
public TypeSpec generateJavaCode() {
    return TypeSpec.classBuilder(mBinderClassName)
            //public 修饰类
            .addModifiers(Modifier.PUBLIC)
            //添加类的方法
            .addMethod(generateMethod())
            //构建Java类
            .build();
}
//生成bindView方法的代码
private MethodSpec generateMethod() {
    //获取所有注解的类的类名
    ClassName className = ClassName.bestGuess(mTypeElement.getQualifiedName().toString());
    //构建方法--方法名
    return MethodSpec.methodBuilder("bindView")
            //public方法
            .addModifiers(Modifier.PUBLIC)
            //返回void
            .returns(void.class)
            //方法传参(参数全类名,参数名)
            .addParameter(className, ParamName)
            //方法代码
            .addCode(generateMethodCode())
            .build();
}
//生成bindView方法里面代码的代码
private String generateMethodCode() {
    StringBuilder code = new StringBuilder();
    for (int id : mVariableElements.keySet()) {
        VariableElement variableElement = mVariableElements.get(id);
        //使用注解的属性的名称
        String name = variableElement.getSimpleName().toString();
        //使用注解的属性的类型
        String type = variableElement.asType().toString();
        //view.name = (type)view.findViewById(id)
        String findViewCode = ParamName + "." + name + "=(" + type + ")" + ParamName +
                ".findViewById(" + id + ");\n";
        code.append(findViewCode);
    }
    return code.toString();
}

上面定义了三个方法,一个生成类,另外两个是对bindView方法的代码生成,相信大家细心点看还是看得懂的。

所有的代码工作做好了后,紧接着需要去注册和启动BinderProcessor

image.png

此处是google提供的AutoService注解,用来扫描工程注解的扫描器,第二个要注意的地方是启动扫描器:
image.png

在main下面生成javax.annotation.processing.Processor文件,里面写上要被启动的扫描器:

com.single.router_compiler.BinderProcessor

使用
由于我们生成的APT代码,肯定只有在运行期才能使用的,所以在编译之前肯定是找不到***_ViewBinding类的,因此咋们得需要写个反射调用该类的工具类:

public class BinderViewTools {
    public static void init(Activity activity) {
        Class clazz = activity.getClass();
        try {
            Class<?> bindClass = Class.forName(clazz.getName() + "_ViewBinding");
            Method bind = bindClass.getMethod("bindView", class);
            bind.invoke(bindClass.newInstance(), activity);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

不熟悉反射的伙伴们可以看下反射如何使用,这里就不多说了。更多反射知识,在activity中直接使用:

BinderViewTools.init(this);

接着编译下工程,在app目录的build下面可以看到生成了activity对应的*** _ViewBinding类:


image.png

完整代码

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,761评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,953评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,998评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,248评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,130评论 4 356
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,145评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,550评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,236评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,510评论 1 291
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,601评论 2 310
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,376评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,247评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,613评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,911评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,191评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,532评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,739评论 2 335

推荐阅读更多精彩内容