美文网首页
Android编译时注解初级之ButterKnife

Android编译时注解初级之ButterKnife

作者: SyntaxWarnning | 来源:发表于2018-09-25 19:05 被阅读0次

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

    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

    相关文章

      网友评论

          本文标题:Android编译时注解初级之ButterKnife

          本文链接:https://www.haomeiwen.com/subject/ipiloftx.html