ButterKnife编译时生成代码原理:butterknife-compiler源码分析

Posted Steadyoung

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了ButterKnife编译时生成代码原理:butterknife-compiler源码分析相关的知识,希望对你有一定的参考价值。

1.butterknife-compiler介绍

上篇文章:注解框架源码分析(XUtils、ButterKnife),根据代码运行流程分析了xUtils和ButterKnife,ButterKnife最终实现注解方法的代码是通过编译运行时生成的,也就是gradle依赖中butterknife-compiler实现的:

dependencies 
  implementation 'com.jakewharton:butterknife:8.8.1'
  annotationProcessor 'com.jakewharton:butterknife-compiler:8.8.1'

com.jakewharton:butterknife-compiler 就是自定义的注解处理器,我们在 Gradle 中注册使用它。
然而我在项目结构中找了很久也没有找到这个库的文件,有可能是在编译时才去访问的,如果需要可以在 GitHub 中找到:
butterknife-compiler

2.butterknife-compiler源码分析

我们看看ButterKnifeProcessor这个类:

@Override public synchronized void init(ProcessingEnvironment env) 
    super.init(env);

    String sdk = env.getOptions().get(OPTION_SDK_INT);
    if (sdk != null) 
      try 
        this.sdk = Integer.parseInt(sdk);
       catch (NumberFormatException e) 
        env.getMessager()
            .printMessage(Kind.WARNING, "Unable to parse supplied minSdk option '"
                + sdk
                + "'. Falling back to API 1 support.");
      
    

    debuggable = !"false".equals(env.getOptions().get(OPTION_DEBUGGABLE));

    elementUtils = env.getElementUtils();
    typeUtils = env.getTypeUtils();
    filer = env.getFiler();
    try 
      trees = Trees.instance(processingEnv);
     catch (IllegalArgumentException ignored) 
    
  

int()方法里面进来判断了最低的支持的sdk版本。ProcessingEnviroment参数提供很多有用的工具类Elements, Types和Filer。Types是用来处理TypeMirror的工具类,Filer用来创建生成辅助文件。至于ElementUtils嘛,其实ButterKnifeProcessor在运行的时候,会扫描所有的Java源文件,然后每一个Java源文件的每一个部分都是一个Element,比如一个包、类或者方法。

@Override public Set<String> getSupportedAnnotationTypes() 
    Set<String> types = new LinkedHashSet<>();
    for (Class<? extends Annotation> annotation : getSupportedAnnotations()) 
      types.add(annotation.getCanonicalName());
    
    return types;
  

  private Set<Class<? extends Annotation>> getSupportedAnnotations() 
    Set<Class<? extends Annotation>> annotations = new LinkedHashSet<>();

    annotations.add(BindAnim.class);
    annotations.add(BindArray.class);
    annotations.add(BindBitmap.class);
    annotations.add(BindBool.class);
    annotations.add(BindColor.class);
    annotations.add(BindDimen.class);
    annotations.add(BindDrawable.class);
    annotations.add(BindFloat.class);
    annotations.add(BindFont.class);
    annotations.add(BindInt.class);
    annotations.add(BindString.class);
    annotations.add(BindView.class);
    annotations.add(BindViews.class);
    annotations.addAll(LISTENERS);

    return annotations;
  

getSupportedAnnotationTypes()方法主要是指定ButterknifeProcessor是注册给哪些注解的。我们可以看到,在源代码里面,作者一个一个地把Class文件加到那个LinkedHashSet里面,然后再把LISTENERS也全部加进去。

其实整个类最重要的是process方法:

@Override public boolean process(Set<? extends TypeElement> elements, RoundEnvironment env) 
    Map<TypeElement, BindingSet> bindingMap = findAndParseTargets(env);

    for (Map.Entry<TypeElement, BindingSet> entry : bindingMap.entrySet()) 
      TypeElement typeElement = entry.getKey();
      BindingSet binding = entry.getValue();

      JavaFile javaFile = binding.brewJava(sdk, debuggable);
      try 
        javaFile.writeTo(filer);
       catch (IOException e) 
        error(typeElement, "Unable to write binding for type %s: %s", typeElement, e.getMessage());
      
    

    return false;
  

这个方法的作用主要是扫描、评估和处理我们程序中的注解,然后生成Java文件,也就是前面说的MainActivity_ViewBinding。首先一进这个函数就调用了findAndParseTargets方法,我们就去看看findAndParseTargets方法到底做了什么:

private Map<TypeElement, BindingSet> findAndParseTargets(RoundEnvironment env) 
    Map<TypeElement, BindingSet.Builder> builderMap = new LinkedHashMap<>();
    Set<TypeElement> erasedTargetNames = new LinkedHashSet<>();

    scanForRClasses(env);

    // Process each @BindAnim element.
    for (Element element : env.getElementsAnnotatedWith(BindAnim.class)) 
      if (!SuperficialValidation.validateElement(element)) continue;
      try 
        parseResourceAnimation(element, builderMap, erasedTargetNames);
       catch (Exception e) 
        logParsingError(element, BindAnim.class, e);
      
    
........
........
........
// Associate superclass binders with their subclass binders. This is a queue-based tree walk
    // which starts at the roots (superclasses) and walks to the leafs (subclasses).
    Deque<Map.Entry<TypeElement, BindingSet.Builder>> entries =
        new ArrayDeque<>(builderMap.entrySet());
    Map<TypeElement, BindingSet> bindingMap = new LinkedHashMap<>();
    while (!entries.isEmpty()) 
      Map.Entry<TypeElement, BindingSet.Builder> entry = entries.removeFirst();

      TypeElement type = entry.getKey();
      BindingSet.Builder builder = entry.getValue();

      TypeElement parentType = findParentType(type, erasedTargetNames);
      if (parentType == null) 
        bindingMap.put(type, builder.build());
       else 
        BindingSet parentBinding = bindingMap.get(parentType);
        if (parentBinding != null) 
          builder.setParent(parentBinding);
          bindingMap.put(type, builder.build());
         else 
          // Has a superclass binding but we haven't built it yet. Re-enqueue for later.
          entries.addLast(entry);
        
      
    

    return bindingMap;

这个方法的代码非常多,这里只贴出一部分,这个方法的主要的流程如下:

扫描所有具有注解的类,然后根据这些类的信息生成BindingSet,最后生成以TypeElement为键,BindingSet为值的键值对。
循环遍历这个键值对,根据TypeElement和BindingSet里面的信息生成对应的java类。例如AnnotationActivity生成的类即为MainActivity_ViewBinding类。

这里我们可以看看BindingSet里面的代码:

final class BindingSet 
  static final ClassName UTILS = ClassName.get("butterknife.internal", "Utils");
  private static final ClassName VIEW = ClassName.get("android.view", "View");
  private static final ClassName CONTEXT = ClassName.get("android.content", "Context");
  private static final ClassName RESOURCES = ClassName.get("android.content.res", "Resources");
  private static final ClassName UI_THREAD =
      ClassName.get("android.support.annotation", "UiThread");
  private static final ClassName CALL_SUPER =
      ClassName.get("android.support.annotation", "CallSuper");
  private static final ClassName SUPPRESS_LINT =
      ClassName.get("android.annotation", "SuppressLint");
  private static final ClassName UNBINDER = ClassName.get("butterknife", "Unbinder");
  static final ClassName BITMAP_FACTORY = ClassName.get("android.graphics", "BitmapFactory");
  static final ClassName CONTEXT_COMPAT =
      ClassName.get("android.support.v4.content", "ContextCompat");
  static final ClassName ANIMATION_UTILS =
          ClassName.get("android.view.animation", "AnimationUtils");

  private final TypeName targetTypeName;
  private final ClassName bindingClassName;
  private final boolean isFinal;
  private final boolean isView;
  private final boolean isActivity;
  private final boolean isDialog;
  private final ImmutableList<ViewBinding> viewBindings;
  private final ImmutableList<FieldCollectionViewBinding> collectionBindings;
  private final ImmutableList<ResourceBinding> resourceBindings;
  private final BindingSet parentBinding;

  private BindingSet(TypeName targetTypeName, ClassName bindingClassName, boolean isFinal,
      boolean isView, boolean isActivity, boolean isDialog, ImmutableList<ViewBinding> viewBindings,
      ImmutableList<FieldCollectionViewBinding> collectionBindings,
      ImmutableList<ResourceBinding> resourceBindings, BindingSet parentBinding) 
    this.isFinal = isFinal;
    this.targetTypeName = targetTypeName;
    this.bindingClassName = bindingClassName;
    this.isView = isView;
    this.isActivity = isActivity;
    this.isDialog = isDialog;
    this.viewBindings = viewBindings;
    this.collectionBindings = collectionBindings;
    this.resourceBindings = resourceBindings;
    this.parentBinding = parentBinding;
  

  JavaFile brewJava(int sdk, boolean debuggable) 
    return JavaFile.builder(bindingClassName.packageName(), createType(sdk, debuggable))
        .addFileComment("Generated code from Butter Knife. Do not modify!")
        .build();
  

  private TypeSpec createType(int sdk, boolean debuggable) 
    TypeSpec.Builder result = TypeSpec.classBuilder(bindingClassName.simpleName())
        .addModifiers(PUBLIC);
    if (isFinal) 
      result.addModifiers(FINAL);
    
    ........
    ........
    ........

  static final class Builder 
    private final TypeName targetTypeName;
    private final ClassName bindingClassName;
    private final boolean isFinal;
    private final boolean isView;
    private final boolean isActivity;
    private final boolean isDialog;

    private BindingSet parentBinding;

    private final Map<Id, ViewBinding.Builder> viewIdMap = new LinkedHashMap<>();
    private final ImmutableList.Builder<FieldCollectionViewBinding> collectionBindings =
        ImmutableList.builder();
    private final ImmutableList.Builder<ResourceBinding> resourceBindings = ImmutableList.builder();

    private Builder(TypeName targetTypeName, ClassName bindingClassName, boolean isFinal,
        boolean isView, boolean isActivity, boolean isDialog) 
      this.targetTypeName = targetTypeName;
      this.bindingClassName = bindingClassName;
      this.isFinal = isFinal;
      this.isView = isView;
      this.isActivity = isActivity;
      this.isDialog = isDialog;
    

    void addField(Id id, FieldViewBinding binding) 
      getOrCreateViewBindings(id).setFieldBinding(binding);
    

    void addFieldCollection(FieldCollectionViewBinding binding) 
      collectionBindings.add(binding);
    

    boolean addMethod(
        Id id,
        ListenerClass listener,
        ListenerMethod method,
        MethodViewBinding binding) 
      ViewBinding.Builder viewBinding = getOrCreateViewBindings(id);
      if (viewBinding.hasMethodBinding(listener, method) && !"void".equals(method.returnType())) 
        return false;
      
      viewBinding.addMethodBinding(listener, method, binding);
      return true;
    

    void addResource(ResourceBinding binding) 
      resourceBindings.add(binding);
    

    void setParent(BindingSet parent) 
      this.parentBinding = parent;
    

    String findExistingBindingName(Id id) 
      ViewBinding.Builder builder = viewIdMap.get(id);
      if (builder == null) 
        return null;
      
      FieldViewBinding fieldBinding = builder.fieldBinding;
      if (fieldBinding == null) 
        return null;
      
      return fieldBinding.getName();
    

    private ViewBinding.Builder getOrCreateViewBindings(Id id) 
      ViewBinding.Builder viewId = viewIdMap.get(id);
      if (viewId == null) 
        viewId = new ViewBinding.Builder(id);
        viewIdMap.put(id, viewId);
      
      return viewId;
    

    BindingSet build() 
      ImmutableList.Builder<ViewBinding> viewBindings = ImmutableList.builder();
      for (ViewBinding.Builder builder : viewIdMap.values()) 
        viewBindings.add(builder.build());
      
      return new BindingSet(targetTypeName, bindingClassName, isFinal, isView, isActivity, isDialog,
          viewBindings.build(), collectionBindings.build(), resourceBindings.build(),
          parentBinding);
    
  


这个类的代码也非常多,所以我们也只贴一部分,可以自己去看看源码,这个BindingSet是管理了所有关于这个注解的一些信息还有实例本身的信息。

因为我们之前用的例子是绑定的一个View,所以我们就只贴了解析View的代码。好吧,这里遍历了所有带有@BindView的Element,然后对每一个Element进行解析,也就进入了parseBindView这个方法中:

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();
    if (!isSubtypeOfType(elementType, VIEW_TYPE) && !isInterface(elementType)) 
      if (elementType.getKind() == TypeKind.ERROR) 
        note(element, "@%s field with unresolved type (%s) "
                + "must elsewhere be generated as a View or interface. (%s.%s)",
            BindView.class.getSimpleName(), elementType, qualifiedName, simpleName);
       else 
        error(element, "@%s fields must extend from View or be an interface. (%s.%s)",
            BindView.class.getSimpleName(), qualifiedName, simpleName);
        hasError = true;
      
    

    if (hasError) 
      return;
    

    // Assemble information on the field.
    int id = element.getAnnotation(BindView.class).value();

    BindingSet.Builder builder = builderMap.get(enclosingElement);
    QualifiedId qualifiedId = elementToQualifiedId(element, id);
    if (builder != null) 
      String existingBindingName = builder.findExistingBindingName(getId(qualifiedId));
      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);
    

    String name = simpleName.toString();
    TypeName type = TypeName.get(elementType);
    boolean required = isFieldRequired(element);

    builder.addField(getId(qualifiedId), new FieldViewBinding(name, type, required));

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

然后这里从一进入这个方法到

int id = element.getAnnotation(BindView.class).value();

都是在拿到注解信息,然后验证注解的target的类型是否继承自view,然后上面这一行代码获得我们要绑定的View的id,再从builderMap里面取出BindingSet.Builder对象(这个BindingSet是管理了所有关于这个注解的一些信息还有实例本身的信息,其实最后是通过BindingSet来生成java代码的,上面也已经看了BindingSet的代码),如果builderMap里面不存在的话,就在

builder = getOrCreateBindingBuilder(builderMap, enclosingElement);

这里生成一个,我们进去看一下getOrCreateBindingBuilder:

private BindingSet.Builder getOrCreateBindingBuilder(
      Map<TypeElement, BindingSet.Builder> builderMap, TypeElement enclosingElement) 
    BindingSet.Builder builder = builderMap.get(enclosingElement);
    if (builder == null) 
      builder = BindingSet.newBuilder(enclosingElement);
      builderMap.put(enclosingElement, builder);
    
    return builder;
  

这里面其实很简单,就是获取一些这个注解所修饰的变量的一些信息,然后把这个解析后的builder加入到builderMap里面。

返回刚刚的parseBindView中,根据view的信息生成一个FieldViewBinding,最后添加到上边生成的builder实例中。这里基本完成了解析工作。最后回到findAndParseTargets中:

// Associate superclass binders with their subclass binders. This is a queue-based tree walk
    // which starts at the roots (superclasses) and walks to the leafs (subclasses).
    Deque<Map.Entry<TypeElement, BindingSet.Builder>> entries =
        new ArrayDeque<>(builderMap.entrySet());
    Map<TypeElement, BindingSet> bindingMap = new LinkedHashMap<>();
    while (!entries.isEmpty()) 
      Map.Entry<TypeElement, BindingSet.Builder> entry = entries.removeFirst();

      TypeElement type = entry.getKey();
      BindingSet.Builder builder = entry.getValue();

      TypeElement parentType = findParentType(type, erasedTargetNames);
      if (parentType == null) 
        bindingMap.put(type, builder.build());
       else 
        BindingSet parentBinding = bindingMap.get(parentType);
        if (parentBinding != null) 
          builder.setParent(parentBinding);
          bindingMap.put(type, builder.build());
         else 
          // Has a superclass binding but we haven't built it yet. Re-enqueue for later.
          entries.addLast(entry);
        
      
    

这里主要的工作是建立上面的绑定的所有的实例的解绑的关系,因为我们绑定了,最后在代码中还是会解绑的。这里预先处理好了这些关系。
回到我们的process中, 现在解析完了annotation,该生成java文件了,再看看代码:

@Override public boolean process(Set<? extends TypeElement> elements, RoundEnvironment env) 
    Map<TypeElement, BindingSet> bindingMap = findAndParseTargets(env);

    for (Map.Entry<TypeElement, BindingSet> entry : bindingMap.entrySet()) 
      TypeElement typeElement = entry.getKey();
      BindingSet binding = entry.getValue();

      JavaFile javaFile = binding.brewJava(sdk, debuggable);
      try 
        javaFile.writeTo(filer);
       catch (IOException e) 
        error(typeElement, "Unable to write binding for type %s: %s", typeElement, e.getMessage());
      
    

    return false;
  

遍历刚刚得到的bindingMap,然后再一个一个地通过

javaFile.writeTo(filer);

来生成java文件。然而生成的java文件也是根据上面的信息来用字符串拼接起来的,然而这个工作在brewJava()中完成了:

JavaFile brewJava(int sdk, boolean debuggable) 
    return JavaFile.builder(bindingClassName.packageName(), createType(sdk, debuggable))
        .addFileComment("Generated code from Butter Knife. Do not modify!")
        .build();
  

最后通过writeTo(Filer filer)生成java源文件。

以上是关于ButterKnife编译时生成代码原理:butterknife-compiler源码分析的主要内容,如果未能解决你的问题,请参考以下文章

Android APT编译时技术 ( ButterKnife 原理分析 )

ButterKnife实现原理

ButterKnife 原理

butterknife原理

ButterKnife -- 源码分析 -- 在‘编译期’间生成findViewById等代码

ButterKnife -- 源码分析 -- 在‘编译期’间生成findViewById等代码