Android编译时注解初级之ButterKnife

本文的主要目的在于了解编译时注解,并能初步运用。代码在最后。

1.编译时注解 VS 运行时注解

1.1 运行时注解

这种注解在运行时,依赖反射,获得需要的信息,比如:

    @SyntaxRun(R.id.id_text)
    public Button mText;

    ...

    private void processAnnotations() {
        Field[] fields = this.getClass().getDeclaredFields();
        for (Field f : fields) {
            SyntaxRun sr = f.getAnnotation(SyntaxRun.class);
            if (sr == null) {
                continue;
            }

            int id = sr.value();
            if (id == -1) {
                continue;
            }

            try {
                f.set(this, findViewById(id));
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }
        }
    }

这样,就可以实现自动findview了。但是这样会在初始化的时侯,依赖反射去找到,并且设置,这一过程又会导致性能上的损失。

1.2 编译时注解

顾名思义,这个注解是在编译的时侯生效,那么唯一的做法就是帮我们生成那些findview的类。先看一个例子,后面再看完成代码,比如:

    private void processGen(RoundEnvironment roundEnv) {
        Set<? extends Element> elements =
                roundEnv.getElementsAnnotatedWith(SyntaxGen.class);

        String enclosingClass = null;
        List<FieldDesc> views = new ArrayList<>();

        for (Element each : elements) {

            Element enclosingElement = each.getEnclosingElement();
            JCTree tree = (JCTree) mTrees.getTree(enclosingElement);
            //tree.accept(new DeprecatedTranslator(roundEnv, each, mTreeMaker, mMethodHelper, mElementUtils));

            if (!(each instanceof Symbol.VarSymbol)) {
                continue;
            }

            Symbol.VarSymbol symbol = (Symbol.VarSymbol) each;
            String viewClass = symbol.asType().toString();
            System.out.println("each : " + viewClass);
            System.out.println("name : " + each.getSimpleName());
            System.out.println("tree enclosing : " + enclosingElement);
            
            //1.拿到资源id
            int resId = each.getAnnotation(SyntaxGen.class).value();
            if (enclosingClass == null) {
                enclosingClass = enclosingElement.toString();
                System.out.println("class : " + enclosingClass);
            }

            views.add(new FieldDesc(viewClass, each.getSimpleName().toString(), resId));
        }
        //2. 生成代码
        if (enclosingClass != null) {
            generateClass(enclosingClass, views);
        }
    }

先粗略看一下编译时注解,后面会解释。要做的就是通过注解拿到resid,然后生成代码帮我们绑定。
与运行时注解相比,只是实现方式不同罢了。

2.ButterKnife

早期版本的BK也是运行时注解,也是后来改成了编译时注解。
现在分析一下这个库的实现,分析开源库首先抓它的核心,不用太在意细枝末节。
依我们的用途,无非就是不再频繁的写findview,setonclick等,那么BK的BindView注解就是我们最常用的,先看一下,它时如何实现的。

看ButterKnifeProcessor.java这个类,以BindView为例,其他都类似的。
1.查找注解修饰的对象

从process方法开始分析

@Override public boolean process(Set<? extends TypeElement> elements, RoundEnvironment env) {
    //1.获取注解的信息等
    Map<TypeElement, BindingSet> bindingMap = findAndParseTargets(env);

    for (Map.Entry<TypeElement, BindingSet> entry : bindingMap.entrySet()) {
      TypeElement typeElement = entry.getKey();
      BindingSet binding = entry.getValue();
      //2.生成java类文件
      JavaFile javaFile = binding.brewJava(sdk, debuggable, useAndroidX);
      try {
        javaFile.writeTo(filer);
      } catch (IOException e) {
        error(typeElement, "Unable to write binding for type %s: %s", typeElement, e.getMessage());
      }
    }

    return false;
  }

两步走:
一,找注解拿信息
二,生成java类文件。

private Map<TypeElement, BindingSet> findAndParseTargets(RoundEnvironment env) {
  ...
    // Process each @BindView element.
    for (Element element : env.getElementsAnnotatedWith(BindView.class)) {
      // we don't SuperficialValidation.validateElement(element)
      // so that an unresolved View type can be generated by later processing rounds
      try {
        System.out.println("BindView Element : " + element);
        parseBindView(element, builderMap, erasedTargetNames);
      } catch (Exception e) {
        logParsingError(element, BindView.class, e);
      }
    }
  ...
}

2.解析找到的element,element是编译时的对象,比如一个变量,一个类等。
主要看核心的点。


  private void parseBindView(Element element, Map<TypeElement, BindingSet.Builder> builderMap,
      Set<TypeElement> erasedTargetNames) {
    TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();

    // Start by verifying common generated code restrictions.
    boolean hasError = isInaccessibleViaGeneratedCode(BindView.class, "fields", element)
        || isBindingInWrongPackage(BindView.class, element);

    // Verify that the target type extends from View.
    TypeMirror elementType = element.asType();
    if (elementType.getKind() == TypeKind.TYPEVAR) {
      TypeVariable typeVariable = (TypeVariable) elementType;
      elementType = typeVariable.getUpperBound();
    }
    Name qualifiedName = enclosingElement.getQualifiedName();
    Name simpleName = element.getSimpleName();

    ...错误判断
    // Assemble information on the field.
    //拿到id
    int id = element.getAnnotation(BindView.class).value();

    //1.得到一个Builder
    BindingSet.Builder builder = builderMap.get(enclosingElement);
    Id resourceId = elementToId(element, BindView.class, id);
    if (builder != null) {
      String existingBindingName = builder.findExistingBindingName(resourceId);
      if (existingBindingName != null) {
        error(element, "Attempt to use @%s for an already bound ID %d on '%s'. (%s.%s)",
            BindView.class.getSimpleName(), id, existingBindingName,
            enclosingElement.getQualifiedName(), element.getSimpleName());
        return;
      }
    } else {
      builder = getOrCreateBindingBuilder(builderMap, enclosingElement);
    }
    
    //2.拿到名字
    String name = simpleName.toString();
    //拿到变量的类型
    TypeName type = TypeName.get(elementType);
    boolean required = isFieldRequired(element);
    
    //3.添加到Builder
    builder.addField(resourceId, new FieldViewBinding(name, type, required));

    // Add the type-erased version to the valid binding targets set.
    erasedTargetNames.add(enclosingElement);
  }

2.1 获取一个Builder,没有则创建。
参考如下创建builder,可以看出,平时用BK时,编译过后生成的类就是在这准备的。
比如:
ClassName bindingClassName = ClassName.get(packageName, className + "_ViewBinding");

如果在MainActivity中使用,那么就有 MainActivity_ViewBinding.java


  static Builder newBuilder(TypeElement enclosingElement) {
    TypeMirror typeMirror = enclosingElement.asType();

    boolean isView = isSubtypeOfType(typeMirror, VIEW_TYPE);
    boolean isActivity = isSubtypeOfType(typeMirror, ACTIVITY_TYPE);
    boolean isDialog = isSubtypeOfType(typeMirror, DIALOG_TYPE);

    TypeName targetType = TypeName.get(typeMirror);
    if (targetType instanceof ParameterizedTypeName) {
      targetType = ((ParameterizedTypeName) targetType).rawType;
    }

    String packageName = getPackage(enclosingElement).getQualifiedName().toString();
    String className = enclosingElement.getQualifiedName().toString().substring(
        packageName.length() + 1).replace('.', '$');
    //这里是不是熟悉了?
    ClassName bindingClassName = ClassName.get(packageName, className + "_ViewBinding");

    boolean isFinal = enclosingElement.getModifiers().contains(Modifier.FINAL);
    return new Builder(targetType, bindingClassName, isFinal, isView, isActivity, isDialog);
  }

2.2 拿到名字和类型等信息。
2.3 添加到Builder中。

这样,我们的信息就准备好了。然后在ButterKnifeProcessor的process中看到
JavaFile javaFile = binding.brewJava(sdk, debuggable, useAndroidX);
会生成一个文件,写入到编译路径里面,那么写了什么呢?
按之前我们分析的BindView的路线,继续往下看。
最终,我们看到会进入如下的方法里面:
这里就比较熟悉了,最终生成了 target.view = source.findViewById(id)这样的语句。
!注意,用的是target.view这种直接调用的方式,这也是变量必须不能是private的原因。

现在这里出现了一些$L $T 等这些东西,是什么呢?这里用了生成java文件的一个库:JavaPoet , github自行搜索,后面也会用到。


   private void addViewBinding(MethodSpec.Builder result, ViewBinding binding, boolean debuggable) {
    if (binding.isSingleFieldBinding()) {
      // Optimize the common case where there's a single binding directly to a field.
      FieldViewBinding fieldBinding = binding.getFieldBinding();
      CodeBlock.Builder builder = CodeBlock.builder()
          .add("target.$L = ", fieldBinding.getName());

      boolean requiresCast = requiresCast(fieldBinding.getType());
      if (!debuggable || (!requiresCast && !fieldBinding.isRequired())) {
        if (requiresCast) {
          builder.add("($T) ", fieldBinding.getType());
        }
        builder.add("source.findViewById($L)", binding.getId().code);
      } else {
        builder.add("$T.find", UTILS);
        builder.add(fieldBinding.isRequired() ? "RequiredView" : "OptionalView");
        if (requiresCast) {
          builder.add("AsType");
        }
        builder.add("(source, $L", binding.getId().code);
        if (fieldBinding.isRequired() || requiresCast) {
          builder.add(", $S", asHumanDescription(singletonList(fieldBinding)));
        }
        if (requiresCast) {
          builder.add(", $T.class", fieldBinding.getRawType());
        }
        builder.add(")");
      }
      result.addStatement("$L", builder.build());
      return;
    }

    List<MemberViewBinding> requiredBindings = binding.getRequiredBindings();
    if (!debuggable || requiredBindings.isEmpty()) {
      result.addStatement("view = source.findViewById($L)", binding.getId().code);
    } else if (!binding.isBoundToRoot()) {
      result.addStatement("view = $T.findRequiredView(source, $L, $S)", UTILS,
          binding.getId().code, asHumanDescription(requiredBindings));
    }

    addFieldBinding(result, binding, debuggable);
    addMethodBindings(result, binding, debuggable);
  }

3.实现简单版的ButterKnife

3.1 呃呃呃,先去学习一下JavaPoet怎么用......
3.2 创建编译时注解(完整代码见后面)

1.创建类

public class SyntaxProcessor extends AbstractProcessor {
...
}

2.添加到路径
新建文件 resources/META-INF/services/javax.annotation.processing.Processor
并将SyntaxProcessor全路径名写在里面。

3.3 处理生成代码(按JavaPoet的形式生成)
private void generateClass(String originClass, List<FieldDesc> views) {

        String pkg = originClass.substring(0, originClass.lastIndexOf("."));
        String originClassName = originClass.substring(originClass.lastIndexOf(".") + 1);
        String newClass = originClassName + "_Bind";

        MethodSpec main = MethodSpec.methodBuilder("main")
                .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
                .returns(void.class)
                .addParameter(String[].class, "args")
                .addStatement("$T.out.println($S)", System.class, "Hello, JavaPoet!")
                .build();

        ClassName activityClass = ClassName.get(pkg, originClassName);
        FieldSpec activity = FieldSpec.builder(activityClass, "mA", Modifier.PRIVATE)
                .addModifiers(Modifier.PRIVATE, Modifier.PRIVATE)
                .build();

        List<FieldSpec> specs = new ArrayList<>();
        List<IdSt> sts = new ArrayList<>();
        for (FieldDesc desc : views) {
            String val = desc.viewClass;
            String pre = getPre(val);
            String suffix = getSuffix(val);

            ClassName viewClass = ClassName.get(pre, suffix);
            FieldSpec viewDef = FieldSpec.builder(viewClass, "mView_" + desc.id, Modifier.PUBLIC)
                    //.addModifiers(Modifier.PRIVATE, Modifier.PRIVATE)
                    .build();
            specs.add(viewDef);

            String st0 = "$N = $N.$N = this.$N.findViewById($L)";
            String stPre = "mView_" + desc.id;
            String st1 = "mA";
            String st2 = desc.fieldName;
            String st3 = "mA";
            int st4 = desc.id;
            sts.add(new IdSt(st0, stPre, st1, st2, st3, st4));
        }

        MethodSpec.Builder constructor = MethodSpec.constructorBuilder()
                .addModifiers(Modifier.PUBLIC)
                .addParameter(activityClass, "activity")
                .addStatement("this.$N = $N", "mA", "activity");

        for (IdSt st : sts) {
            constructor.addStatement(st.st0, st.stPre, st.st1, st.st2, st.st3, st.st4);
        }

        TypeSpec.Builder builder = TypeSpec.classBuilder(newClass)
                .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
                .addMethod(constructor.build())
                .addMethod(main)
                .addField(activity);

        for (FieldSpec spec : specs) {
            builder.addField(spec);
        }

        JavaFile javaFile = JavaFile.builder(pkg, builder.build())
                .build();

        try {
            javaFile.writeTo(System.out);
            javaFile.writeTo(processingEnv.getFiler());
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

生成后的如下:


package com.syntax.javapoet;

import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView;
import java.lang.String;
import java.lang.System;

public final class MainActivity_Bind {
  private MainActivity mA;

  public Button mView_2131165252;

  public TextView mView_2131165251;

  public ImageView mView_2131165250;

  public MainActivity_Bind(MainActivity activity) {
    this.mA = activity;
    mView_2131165252 = mA.mText = this.mA.findViewById(2131165252);
    mView_2131165251 = mA.mTextView = this.mA.findViewById(2131165251);
    mView_2131165250 = mA.mImage = this.mA.findViewById(2131165250);
  }

  public static void main(String[] args) {
    System.out.println("Hello, JavaPoet!");
  }
}

基本功能已经可以实现,想实现其他功能,再生成即可。

因一些原因,无法给出代码仓库,看这个吧。
代码见 https://pan.baidu.com/s/1oEjXjDiYehM-eIpg1D_TfQ

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

推荐阅读更多精彩内容

  • 用两张图告诉你,为什么你的 App 会卡顿? - Android - 掘金 Cover 有什么料? 从这篇文章中你...
    hw1212阅读 12,711评论 2 59
  • 转载请标明原文地址:http://www.jianshu.com/p/dc163215bc7e 本来打算继续写 M...
    junerver阅读 2,770评论 12 76
  • 在生活中,监控用电量是一个很重要的功能,但并不是大多数家庭重点关注的问题。软件系统的一些功能就像家里的电表一样,这...
    yjaal阅读 574评论 0 3
  • BufferKnife的集成和基本使用 1.使用:在build.gradle里加入 加入之后发现报错,错误大致如下...
    糖葫芦_倩倩阅读 165评论 0 1
  • 故事是从1933年希特勒上台开始,一直到1949年冷战开始,第二次世界大战就此结束。 故事的主角自然而然的变成了《...
    鲸小奇fir阅读 298评论 0 4