加入收藏 | 设为首页 | 会员中心 | 我要投稿 李大同 (https://www.lidatong.com.cn/)- 科技、建站、经验、云计算、5G、大数据,站长网!
当前位置: 首页 > 百科 > 正文

butterknife源码分析:代码分析

发布时间:2020-12-14 01:49:37 所属栏目:百科 来源:网络整理
导读:butterknife源码分析系列 : 谈一谈Java的注解 http://www.jb51.cc/article/p-srjxthnw-bpg.html 如何处理注解—反射与注解处理器 http://www.jb51.cc/article/p-ppftpdqn-bpg.html 代码分析 http://www.jb51.cc/article/p-rnvspoko-bpg.html 前面两篇讲解了

butterknife源码分析系列:
谈一谈Java的注解
http://www.52php.cn/article/p-srjxthnw-bpg.html
如何处理注解—反射与注解处理器
http://www.52php.cn/article/p-ppftpdqn-bpg.html
代码分析
http://www.52php.cn/article/p-rnvspoko-bpg.html

前面两篇讲解了注解的定义,以及如何用反射与注解处理器的方法来处理注解。在第二篇文章中,我们用反射的方式模拟了butterknife的findViewById来简化代码,同时,我们也说过butterknife其实是用注解处理器来实现的。

虽然butterknife的源码解析文章已有许多,这里借其肩膀,总结总结。

流程介绍

上文我们详细介绍了注解处理器,这里再结合butterknife再次强调下。在编译源文件时,会分析扫描注解,当扫描到butterknife定义的@BindView、@OnClick等注解时,会使用JavaPoet来生成代码。生成后的文件会再次分析,直到没有分析到需要处理的注解位置。

JavaPoet简介

Poet译为诗人,JavaPoet可以帮助便捷地生成代码,而不是手动繁琐的拼接语句。简要介绍下比较关键的几个类:

  • MethodSpec 代表一个构造函数或方法声明。
  • TypeSpec 代表一个类,接口,或者枚举声明。
  • FieldSpec 代表一个成员变量,一个字段声明。

详细的使用方法可以看github上的介绍:

https://github.com/square/javapoet/

从结果入手

直接从源码分析容易一头雾水。既然ButterKnife会在编译时生成代码,那我们从结果入手,看看生成的代码长什么样。

源文件:

public class MainActivity extends Activity {

    @BindView(R.id.tv_title)
    public TextView tvTitle;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
         ButterKnife.bind(this);
    }

    @OnClick(R.id.tv_title)
    void titleClick() {

    }

}

编译生成的文件可以在build/source/apt下可以看到:

public class MainActivity_ViewBinding implements Unbinder {
  private MainActivity target;

  private View view2131034112;

  @UiThread
  public MainActivity_ViewBinding(MainActivity target) {
    this(target,target.getWindow().getDecorView());
  }

  @UiThread
  public MainActivity_ViewBinding(final MainActivity target,View source) {
    this.target = target;

    View view;
    view = Utils.findRequiredView(source,R.id.tv_title,"field 'tvTitle' and method 'titleClick'");
    target.tvTitle = Utils.castView(view,"field 'tvTitle'",TextView.class);
    view2131034112 = view;
    view.setOnClickListener(new DebouncingOnClickListener() {
      @Override
      public void doClick(View p0) {
        target.titleClick();
      }
    });
  }

  @Override
  @CallSuper
  public void unbind() {
    MainActivity target = this.target;
    if (target == null) throw new IllegalStateException("Bindings already cleared.");
    this.target = null;

    target.tvTitle = null;

    view2131034112.setOnClickListener(null);
    view2131034112 = null;
  }
}

注意到源文件名是MainActivity,而生成的文件是MainActivity_ViewBinding。

在构造函数内,使用

target.tvTitle = Utils.findRequiredViewAsType(source,R.id.tv_title,TextView.class);

利用Utils.findRequiredViewAsType得到的结果赋值到我们定义的TextView(tvTitle)上,这也解释了为什么tvTitle需要用public来修饰。

进一步看看findRequiredViewAsType。

public static <T> T findRequiredViewAsType(View source,@IdRes int id,String who,Class<T> cls) {
    View view = findRequiredView(source,id,who);
    return castView(view,who,cls);
  }

castView方法是将得到的View转化成具体的子View,这里是TextView。而findRequiredView里进行了findViewById的操作。

这里我们可能会有疑问,在生成的MainActivity_ViewBinding的构造方法使用到MainActivity,而我们在使用ButterKnife时会使用ButterKnife.bind(this)将Activity传递到ButterKnife里,这之间是怎么一个过程?

进去ButterKnife.bind(this)看看。

@NonNull @UiThread
  public static Unbinder bind(@NonNull Activity target) {
    View sourceView = target.getWindow().getDecorView();
    return createBinding(target,sourceView);
  }

根据activity得到DecorView,再传递到createBinding。

private static Unbinder createBinding(@NonNull Object target,@NonNull View source) {
    Class<?> targetClass = target.getClass();
    if (debug) Log.d(TAG,"Looking up binding for " + targetClass.getName());
    Constructor<? extends Unbinder> constructor = findBindingConstructorForClass(targetClass);

    if (constructor == null) {
      return Unbinder.EMPTY;
    }

    //noinspection TryWithIdenticalCatches Resolves to API 19+ only type.
    try {
      return constructor.newInstance(target,source);
    } catch (IllegalAccessException e) {
      throw new RuntimeException("Unable to invoke " + constructor,e);
    } catch (InstantiationException e) {
      throw new RuntimeException("Unable to invoke " + constructor,e);
    } catch (InvocationTargetException e) {
      Throwable cause = e.getCause();
      if (cause instanceof RuntimeException) {
        throw (RuntimeException) cause;
      }
      if (cause instanceof Error) {
        throw (Error) cause;
      }
      throw new RuntimeException("Unable to create binding instance.",cause);
    }
  }

上面的代码里,获取到Constructor后,再运用反射生成实例,在实例里的findView操作就会被调用到。接下来看看如何获取到Constructor的。

@Nullable @CheckResult @UiThread private static Constructor<? extends Unbinder> findBindingConstructorForClass(Class<?> cls) { Constructor<? extends Unbinder> bindingCtor = BINDINGS.get(cls); if (bindingCtor != null) { if (debug) Log.d(TAG,"HIT: Cached in binding map."); return bindingCtor; } String clsName = cls.getName(); // 过滤掉系统相关的类 if (clsName.startsWith("android.") || clsName.startsWith("java.")) { if (debug) Log.d(TAG,"MISS: Reached framework class. Abandoning search."); return null; } try { Class<?> bindingClass = Class.forName(clsName + "_ViewBinding"); //noinspection unchecked bindingCtor = (Constructor<? extends Unbinder>) bindingClass.getConstructor(cls,View.class); if (debug) Log.d(TAG,"HIT: Loaded binding class and constructor."); } catch (ClassNotFoundException e) { if (debug) Log.d(TAG,"Not found. Trying superclass " + cls.getSuperclass().getName()); bindingCtor = findBindingConstructorForClass(cls.getSuperclass()); } catch (NoSuchMethodException e) { throw new RuntimeException("Unable to find binding constructor for " + clsName,e); } BINDINGS.put(cls,bindingCtor); return bindingCtor; }

从上面可以看到Constructor获取的过程,根据className得到className_ViewBinding,就可以得到Constructor。并且会将得到的Constructor缓存起来,避免反射的性能问题。

这样一来,ButterKnife.bind(this)传递进去的MainActivity会通过反射生成MainActivity_ViewBinding实例。在这个实例的构造函数内,进行findViewById、setOnclickListener等操作

编译过程

MainActivity在编译时会生成处理findViewById等操作的MainActivity_ViewBinding。接下来我们探索下ButterKnife偷偷在注解处理器里做了什么。

关于注解处理器:

http://www.52php.cn/article/p-ppftpdqn-bpg.html

注解处理器里包含下面几个重要的方法:

  • init()
    初始化,得到Elements、Types、Filer等工具类
  • getSupportedAnnotationTypes()
    描述注解处理器需要处理的注解
  • process()
    扫描分析注解,生成代码

从process方法入手:

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

    Map<TypeElement,BindingSet> bindingMap = findAndParseTargets(env);
    // 利用JavaPoet生成代码
    for (Map.Entry<TypeElement,BindingSet> entry : bindingMap.entrySet()) {
      TypeElement typeElement = entry.getKey();
      BindingSet binding = entry.getValue();

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

    return false;
  }

process里主要做了两件事情:

  • findAndParseTargets
    获得TypeElement -> BindingSet的映射关系,TypeElement指的是类或接口,在本文所举的栗子中是MainActivity。BindingSet里包含了生成代码时的一些参数。
  • 运用JavaPoet框架来生成代码
    生成的代码类形式为xxxx_ViewBinding

findAndParseTargets

跟踪进入findAndParseTargets方法。

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

    // 建立view与R的id的关系
    scanForRClasses(env);

    // 省略部分代码

    // 解析BindView注解
    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 {
        parseBindView(element,builderMap,erasedTargetNames);
      } catch (Exception e) {
        logParsingError(element,BindView.class,e);
      }
    }

    // 省略部分代码
    // 将Map.Entry<TypeElement,BindingSet.Builder>转化为Map<TypeElement,BindingSet>
    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;
  }

以BindView为例,省略掉无关的代码,findAndParseTargets分成三部分,第一部分是scanForRClasses,第二部分是解析各种注解,最后部分是根据buildMap来生成bindingMap。

① scanForRClasses

scanForRClasses主要是用来建立view与id的关系。

例如在上面所举的栗子中,MainActivity_ViewBinding里会持有一个全局变量view2131034112,这个其实就是MainActivity的tvTitle,后面的2131034112就是对应在R文件的id。

值得一提的是,这部分代码在旧版本的butterknife并没有出现,估计是为了解决某些潜在的bug。

https://github.com/JakeWharton/butterknife/issues/770

It reads the source code to map the IDs the processor sees back to the resource names for the generated code.

这里简要介绍下,scanForRClasses涉及到Java的语法分析树。

R文件示例:

public final class R {
    public static final class attr {
    }
    public static final class id {
        public static final int tv_title=0x7f050000;
    }
    public static final class layout {
        public static final int activity_main=0x7f030000;
    }
    public static final class mipmap {
        public static final int ic_launcher=0x7f020000;
    }
    public static final class string {
        public static final int app_name=0x7f040000;
    }
}

首先根据element获取到包名,再利用RClassScanner寻找到R文件,在R文件里利用IdScanner寻找到内部类id,在id类里利用VarScanner寻找到tvTitle的id。最后就可以得到view2131034112。

② parseBindView
第二部分是解析各种注解,这里以BindView为例。

private void parseBindView(Element element,Map<TypeElement,BindingSet.Builder> builderMap,Set<TypeElement> erasedTargetNames) {
    // 得到包含注解所属的TypeElement,例如MainActivity
    TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();

    // isInaccessibleViaGeneratedCode检验enclosingElement(MainActivity)是类、不是private,检验element不是private活着static
    // isBindingInWrongPackage检验enclosingElement的包名是不是系统相关的类
    boolean hasError = isInaccessibleViaGeneratedCode(BindView.class,"fields",element) || isBindingInWrongPackage(BindView.class,element);

    TypeMirror elementType = element.asType();
    if (elementType.getKind() == TypeKind.TYPEVAR) {
      TypeVariable typeVariable = (TypeVariable) elementType;
      elementType = typeVariable.getUpperBound();
    }
    Name qualifiedName = enclosingElement.getQualifiedName();
    Name simpleName = element.getSimpleName();
    // 判断element是View的子类或者接口
    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)",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));
      // 检查是否绑定过此id
      if (existingBindingName != null) {
        error(element,"Attempt to use @%s for an already bound ID %d on '%s'. (%s.%s)",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);
  }

parseBindView先检测是否有错误,然后将name(变量名,例如tvTitle)、type(类名,例如TextView)、required(是否有@nullable注解)封装成FieldViewBinding放到builder里面。

Map.Entry<TypeElement,BindingSet.Builder> -> Map<TypeElement,BindingSet>

Deque<Map.Entry<TypeElement,builder.build());
        } else {
          // Has a superclass binding but we haven't built it yet. Re-enqueue for later.
          entries.addLast(entry);
        }
      }
    }

遍历builderMap,先通过findParentType判断是否有父类,若有,通过setParent来设置进去。在生成代码时,若有父类,会把父类的注解也注入。findParentType方法是通过TypeMirror来获取父类的信息(TypeMirror可以获取类里面的方法、域、超类等信息)。

运用JavaPoet框架来生成代码

在生成Map<TypeElement,BindingSet>后,下一步就是使用JavaPoet框架生成相关的代码。

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

    for (Map.Entry<TypeElement,e.getMessage());
      }
    }

    return false;
  }
遍历Map<TypeElement,BindingSet>,根据TypeElement(MainActivity)利用Filer工具类来生成文件TypeElement_ViewBinding(MainActivity_ViewBinding)

看看brewJava方法:

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

brewJava比较简单,逻辑都在createType(sdk)这里面。

private TypeSpec createType(int sdk) {
    TypeSpec.Builder result = TypeSpec.classBuilder(bindingClassName.simpleName())
        .addModifiers(PUBLIC);
    if (isFinal) {
      result.addModifiers(FINAL);
    }
    // 如果有父类,继承自父类。
    if (parentBinding != null) {
      result.superclass(parentBinding.bindingClassName);
    } else { // 否则,实现UNBINDER接口。
      result.addSuperinterface(UNBINDER);
    }
    // 增加target全局变量。例如上面的栗子,MainActivity target。
    if (hasTargetField()) {
      result.addField(targetTypeName,"target",PRIVATE);
    }
    // 入参为target的构造函数①,主要是调用下面的构造函数②
    if (isView) {
      result.addMethod(createBindingConstructorForView());
    } else if (isActivity) {
      result.addMethod(createBindingConstructorForActivity());
    } else if (isDialog) {
      result.addMethod(createBindingConstructorForDialog());
    }
    if (!constructorNeedsView()) {
      // Add a delegating constructor with a target type + view signature for reflective use.
      result.addMethod(createBindingViewDelegateConstructor());
    }
    // 构造函数②
    result.addMethod(createBindingConstructor(sdk));

    // 生成unbind方法
    if (hasViewBindings() || parentBinding == null) {
      result.addMethod(createBindingUnbindMethod(result));
    }

    return result.build();
  }

createType方法返回的是TypeSpec,TypeSpec是JavaPoet框架中用来生成类的。这个类里面最重要的代码是在构造函数。跟着进去生成构造函数的方法createBindingUnbindMethod。

private MethodSpec createBindingConstructor(int sdk) {
    // public修饰、@UiThread注解
    MethodSpec.Builder constructor = MethodSpec.constructorBuilder()
        .addAnnotation(UI_THREAD)
        .addModifiers(PUBLIC);
    // 如果注解是修饰方法的,则入参的target为final
    // 当注解是修饰方法时,都是些监听注解,例如OnClick、OnTouch等。这些生成代码时,会用到匿名内部类,所以需要target为final的。
    if (hasMethodBindings()) {
      constructor.addParameter(targetTypeName,FINAL);
    } else {
      constructor.addParameter(targetTypeName,"target");
    }

    if (constructorNeedsView()) {
      constructor.addParameter(VIEW,"source");
    } else {
      constructor.addParameter(CONTEXT,"context");
    }
    // 当需要传入R.xx.xx时,传入数字会提示expected resource of type xx。因此增加@SuppressWarnings(“ResourceType”)注解。
    if (hasUnqualifiedResourceBindings()) {
      // Aapt can change IDs out from underneath us,just suppress since all will work at runtime.
      constructor.addAnnotation(AnnotationSpec.builder(SuppressWarnings.class)
          .addMember("value","$S","ResourceType")
          .build());
    }
    // 有onTouch注解时,增加@SuppressLint("ClickableViewAccessibility")
    if (hasOnTouchMethodBindings()) {
      constructor.addAnnotation(AnnotationSpec.builder(SUPPRESS_LINT)
          .addMember("value","ClickableViewAccessibility")
          .build());
    }
    // 调用父类构造函数
    if (parentBinding != null) {
      if (parentBinding.constructorNeedsView()) {
        constructor.addStatement("super(target,source)");
      } else if (constructorNeedsView()) {
        constructor.addStatement("super(target,source.getContext())");
      } else {
        constructor.addStatement("super(target,context)");
      }
      constructor.addCode("n");
    }
    if (hasTargetField()) {
      constructor.addStatement("this.target = target");
      constructor.addCode("n");
    }
    // View绑定
    if (hasViewBindings()) {
      if (hasViewLocal()) { // 临时变量 View view;
        // Local variable in which all views will be temporarily stored.
        constructor.addStatement("$T view",VIEW);
      }
      // 单个view处理,findView、onclick等
      for (ViewBinding binding : viewBindings) {
        addViewBinding(constructor,binding);
      }
      // BindViews
      for (FieldCollectionViewBinding binding : collectionBindings) {
        constructor.addStatement("$L",binding.render());
      }

      if (!resourceBindings.isEmpty()) {
        constructor.addCode("n");
      }
    }
    // 资源绑定
    if (!resourceBindings.isEmpty()) {
      if (constructorNeedsView()) {
        constructor.addStatement("$T context = source.getContext()",CONTEXT);
      }
      if (hasResourceBindingsNeedingResource(sdk)) {
        constructor.addStatement("$T res = context.getResources()",RESOURCES);
      }
      for (ResourceBinding binding : resourceBindings) {
        constructor.addStatement("$L",binding.render(sdk));
      }
    }

    return constructor.build();
  }

addViewBinding方法进行了findView的操作和setOnClickListener等监听操作。具体生成后的代码如下所示:

view = Utils.findRequiredView(source,"field 'tvTitle' and method 'titleClick'");
    target.tvTitle = Utils.castView(view,TextView.class);
    view2131034112 = view;
    view.setOnClickListener(new DebouncingOnClickListener() {
      @Override
      public void doClick(View p0) {
        target.titleClick();
      }
    });

总结下在使用JavaPoet生成代码的步骤:

① 遍历Map<TypeElement,BindingSet>,根据TypeElement(MainActivity)利用Filer工具类来生成文件TypeElement_ViewBinding(MainActivity_ViewBinding)。

② TypeElement_ViewBinding实现Unbinder接口,在unbind方法内进行解除绑定等操作,例如setOnclickListener(null)等。

③ 在构造函数内,完成注入等操作,例如findViewById,setOnclickListener,资源绑定等操作,若有父类,则会调用父类构造函数。

讲到这里,关于butterknife的分析就结束了。在分析时,最好结合生成后的代码进行分析,这样清晰很多。

后记

学习的过程总是循序渐进的,你们可以发现我学习butterknife源码的过程中也总结了两篇注解相关的文章。学习过程如下面的思维图所示:

若有疑问或错误,请在评论中写出,谢谢!

(编辑:李大同)

【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容!

    推荐文章
      热点阅读