暴力突破 Android 编译插桩(五)- APT 之 Butterknife 源码解析

专栏暴力突破 Android 编译插桩系列

一、前言


相信大家都用过 ButterKnife ,它是基于编译时注解的框架,能够帮助我们减去每次写 FindViewById 的麻烦。下面我们来分析一下 ButterKnife 的源码。

 

二、使用


关于 ButterKnife 的使用相信不用多说了,大家可以去看一下官网 https://github.com/JakeWharton/butterknife,或者看一下这篇《Android常用之Butterknife使用详解》。下面我们来写个示例:

public class MainActivity extends AppCompatActivity {

    @BindView(R.id.tv_content)
    TextView mContentTv;

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

    @OnClick(R.id.tv_content)
    void click(View v){
        Toast.makeText(this, "click", Toast.LENGTH_SHORT).show();
    }

}

build 一下后,生成代码:

public class MainActivity_ViewBinding implements Unbinder {
  private MainActivity target;

  private View view2131165356;

  @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_content, "field 'mContentTv' and method 'click'");
    target.mContentTv = Utils.castView(view, R.id.tv_content, "field 'mContentTv'", TextView.class);
    view2131165356 = view;
    view.setOnClickListener(new DebouncingOnClickListener() {
      @Override
      public void doClick(View p0) {
        target.click(p0);
      }
    });
  }

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

    target.mContentTv = null;

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

下一节我们来分析一下它的原理。

 

三、源码解析


总的来说,大概可以分为以下两步:

  1. 在编译的时候扫描注解并做相应的处理,调用 javapoet 库生成 java 代码。
  2. 当我们调用 ButterKnife.bind(this) 方法的时候,他会根据类的全限定类型找到相应的代码,并执行完成 findViewById()、setOnClick() 、setOnLongClick() 等操作。

3.1、扫描注解、生成代码

我们知道 ButterKnife 自定义很多的注解,有 BindArray,BindBitmap,BindColor,BindView 等,这里我们以 BindView 为例子讲解就可以了,其他的也是基本类似的,这里就不再讲解了。

首先来看看 BindView 注解的代码:

@Retention(CLASS) @Target(FIELD)
public @interface BindView {
  /** View ID to which the field will be bound. */
  @IdRes int value();
}

接下来我们看 ButterKnife 注解处理器的代码:

@AutoService(Processor.class)
public final class ButterKnifeProcessor extends AbstractProcessor {
  
  private Elements elementUtils;
  private Types typeUtils;
  private Filer filer;
  private Trees trees;
  private int sdk = 1;

  @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.");
      }
    }

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

  @Override public Set<String> getSupportedOptions() {
    return Collections.singleton(OPTION_SDK_INT);
  }

  @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(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(BindInt.class);
    annotations.add(BindString.class);
    annotations.add(BindView.class);
    annotations.add(BindViews.class);
    annotations.addAll(LISTENERS);

    return annotations;
  }

  @Override public boolean process(Set<? extends TypeElement> elements, RoundEnvironment env) {
    //1、获取、解析注解
    Map<TypeElement, BindingSet> bindingMap = findAndParseTargets(env);
    //2、生成代码
    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;
  }

...
}

关于注解处理器之前我们已经详细介绍过了,这里就不再赘述,大家可以去看我的《Android 注解处理器解析》,我们直接看一下最关键的 process() 方法代码。process() 方法中分了两步,第一步是获取、解析注解,第二步是生成代码。我们来分别看一下。

3.1.1、获取、解析注解

这里我们进入 findAndParseTargets() 方法,看里面到底是怎样将注解信息存进 map 集合的。这个方法里面针对每一个自定义注解(BindArray,BindBitmap,BindColor,BindView 等) 都做了处理,这里我们重点关注 @BindView 的处理即可。

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

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

    // 后半部分,待会再讲
    return bindingMap;
  }

我们先来看一下 findAndParseTargets() 方法的前半部分,遍历 env.getElementsAnnotatedWith(BindView.class) 集合,并调用 parseBindView 方法去转化。我们跟进去看看:

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

    // 判断是否被注解在属性上,如果该属性是被 private 或者 static 修饰的,则出错
    // 判断是否被注解在错误的包中,若包名以“android”或者“java”开头,则出错
    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();
    // 判断元素是不是View及其子类或者Interface
    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();
    // 根据所在的类元素去查找 builder
    BindingSet.Builder builder = builderMap.get(enclosingElement);
    QualifiedId qualifiedId = elementToQualifiedId(element, id);
    // 如果相应的 builder 已经存在
    if (builder != null) {
        // 验证 ID 是否已经被绑定
        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,就需要重新生成,并别存放到  builderMap 中
        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() 方法里面,主要判断被注解 @BindView 修饰的成员变量是不是合法的,private 或者 static 修饰的则出错。parseBindView() 方法分析完毕之后,我们在回过头来看一下 findAndParseTargets() 方法的后半部分,主要做的工作是对 bindingMap 进行重排序。

private Map<TypeElement, BindingSet> findAndParseTargets(RoundEnvironment env) {
    // 省略前半部分
    // 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();
        //获取 type 的父类的 TypeElement
        TypeElement parentType = findParentType(type, erasedTargetNames);
        // 为空,存进 map
        if (parentType == null) {
            bindingMap.put(type, builder.build());
        } else {
             // 获取 parentType 的 BindingSet
            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;
}

到这里为止,我们已经分析完 ButterKnifeProcessor 是怎样处理注解的相关知识,并存进 map 集合中的。

3.1.2、生成代码

下面我们回到 process() 方法,看一下是怎样生成 java 模板代码的:

public boolean process(Set<? extends TypeElement> elements, RoundEnvironment env) {
    // 拿到所有的注解信息,TypeElement 作为 key,BindingSet 作为 value
    Map<TypeElement, BindingSet> bindingMap = findAndParseTargets(env);
    // 遍历 map 里面的所有信息,并生成 java 代码
    for (Map.Entry<TypeElement, BindingSet> entry : bindingMap.entrySet()) {
        TypeElement typeElement = entry.getKey();
        BindingSet binding = entry.getValue();
         // 生成 javaFile 对象
        JavaFile javaFile = binding.brewJava(sdk);
        try {// 生成 java 模板代码              
            javaFile.writeTo(filer);
        } catch (IOException e) {
            error(typeElement, "Unable to write binding for type %s: %s", typeElement, e.getMessage());
        }
    }

    return false;
}

可以发现这里是调用 JavaPoet 库来生成代码的,关于 javaPoet 的使用可以看我的《Android JavaPoet 使用解析》。我们继续跟进去看看:

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

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 {
        result.addSuperinterface(UNBINDER);
    }

    if (hasTargetField()) {
        result.addField(targetTypeName, "target", PRIVATE);
    }
    // 如果是 View 或者是 View 的子类的话,添加构造方法
    if (isView) {
        result.addMethod(createBindingConstructorForView());
    } else if (isActivity) { // 如果是 Activity 或者是 Activity 的子类的话,添加构造方法
        result.addMethod(createBindingConstructorForActivity());
    } else if (isDialog) {  // 如果是 Dialog 或者是 Dialog 的子类的话,添加构造方法
        result.addMethod(createBindingConstructorForDialog());
    }
    //  如果构造方法不需要 View 参数,添加 需要 View 参数的构造方法
    if (!constructorNeedsView()) {
        // Add a delegating constructor with a target type + view signature for reflective use.
        result.addMethod(createBindingViewDelegateConstructor());
    }
    result.addMethod(createBindingConstructor(sdk));

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

    return result.build();
}

接着我们一起来看一下 createBindingConstructor(sdk) 方法:

private MethodSpec createBindingConstructor(int sdk) {
    MethodSpec.Builder constructor = MethodSpec.constructorBuilder()
            .addAnnotation(UI_THREAD)
            .addModifiers(PUBLIC);
    // 如果有方法绑定,比如 @onClick,那么增加一个 targetTypeName 类型  的方法参数 target,并且是 final 类型的
    if (hasMethodBindings()) {
        constructor.addParameter(targetTypeName, "target", FINAL);
    } else { // 如果没有 ,不是 final 类型的
        constructor.addParameter(targetTypeName, "target");
    }
    //如果有注解的 View,那么添加 VIEW 类型 source 参数
    if (constructorNeedsView()) {
        constructor.addParameter(VIEW, "source");
    } else {
        //  添加 Context 类型的 context 参数
        constructor.addParameter(CONTEXT, "context");
    }

    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 绑定 View,添加 @SuppressLint("ClickableViewAccessibility")
    if (hasOnTouchMethodBindings()) {
        constructor.addAnnotation(AnnotationSpec.builder(SUPPRESS_LINT)
                .addMember("value", "$S", "ClickableViewAccessibility")
                .build());
    }
    // 如果 parentBinding 不为空,调用父类 的构造方法
    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");
    }

    if (hasViewBindings()) {
        if (hasViewLocal()) {
            // Local variable in which all views will be temporarily stored.
            constructor.addStatement("$T view", VIEW);
        }
        //   遍历  viewBindings,生成  source.findViewById($L) 代码
        for (ViewBinding binding : viewBindings) {
            addViewBinding(constructor, binding);
        }
        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();
}

大概做的事情就是

  • 判断是否有设置监听,如果有监听,将 View 设置为 final。
  • 遍历 viewBindings ,调用 addViewBinding() 生成 findViewById 形式的代码。

下面我们一起来看一下 addViewBinding() 方法是怎样生成代码的。

private void addViewBinding(MethodSpec.Builder result, ViewBinding binding) {
    if (binding.isSingleFieldBinding()) {
        // Optimize the common case where there's a single binding directly to a field.
        FieldViewBinding fieldBinding = binding.getFieldBinding();
        // 注意这里直接使用了 target. 的形式,所以属性肯定是不能 private 的
        CodeBlock.Builder builder = CodeBlock.builder()
                .add("target.$L = ", fieldBinding.getName());

        boolean requiresCast = requiresCast(fieldBinding.getType());
        if (!requiresCast && !fieldBinding.isRequired()) {
            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;
    }

3.2 ButterKnife 代码注入

ButterKnife 是通过 bind() 方法来实现注入的,即自动帮我们 findViewById ,解放我们的双手提高工作效率。下面我们一起来看一下 bind() 方法是怎样实现注入的:

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

可以看到 bind() 方法很简单,逻辑基本都交给 createBinding() 方法去完成。我们一起进入 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());
    // 从 Class 中查找 constructor
    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);
    }
}

其实 createBinding() 主要做了这几件事情:

  • 传入 class ,通过 findBindingConstructorForClass 方法来实例化 constructor
  • 利用反射来初始化 constructor 对象
  • 初始化 constructor 失败会抛出异常

下面我们一起来看一下 findBindingConstructorForClass() 方法是怎样实现的。

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;
    }
    // 如果是 android ,java 原生的文件,不处理
    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 = cls.getClassLoader().loadClass(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);
    }
    // 存进 LinkedHashMap 缓存
    BINDINGS.put(cls, bindingCtor);
    return bindingCtor;
}

它的实现思想是这样的:

  • 读取缓存,若缓存命中则直接返回,这样有利于提高效率。从代码中可以看到缓存是通过存进 map 集合实现的。
  • 是否是我们目标文件,是的话进行处理,不是的话直接返回,并打印相应的日志。
  • 利用类加载器加载我们自己生成的 class 文件,并获取其构造方法,获取到则直接返回。获取不到则会抛出异常,在异常的处理中,我们再从当前 class 文件的父类去查找。并把结果存进 map 集合中做缓存处理。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值