欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

ButterKnife v10.2.1 源码分析

程序员文章站 2024-02-26 22:40:58
...


介绍

Field and method binding for Android views which uses annotation processing to generate boilerplate code for you.

Butter Knife使用注解的方法生成代码模板,帮助开发者将视图同方法和字段进行绑定,常用的注解包括@BindView绑定视图,@OnClick绑定点击方法。

使用

添加依赖

// Butterknife requires Java 8.
compileOptions {
	sourceCompatibility JavaVersion.VERSION_1_8
	targetCompatibility JavaVersion.VERSION_1_8
}
dependencies {
    implementation 'com.jakewharton:butterknife:10.2.1'
    annotationProcessor 'com.jakewharton:butterknife-compiler:10.2.1'
}

相关功能代码

public class MainActivity extends AppCompatActivity {

    @BindView(R.id.tv) TextView textView;
    @OnClick(R.id.tv) void showToast() {
        Toast.makeText(this,"Click", Toast.LENGTH_LONG).show();
    }
    @Override
    protected void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ButterKnife.bind(this);
    }
}

布局activity_main.xml代码很简单,只有一个TextView,这里就不贴了,分别使用 @BindView@OnClick注解来绑定视图控件和点击事件。

提个问题

相信很多使用过ButterKnife的同学都会知道,ButterKnife是基于编译时注解的,那么我这里提个问题:ButterKnife是否由使用反射,如果有用到,在哪里用到? 接下来,我们带着这个问题一起探讨ButterKnife的奥妙。

源码分析

本文分析的源码为最新的10.2.1版本

ButterKnife 主要包括以下三个部分:

  • butterknife-annotations: 存放自定义注解,我们前面用到的@OnClick@BindView就是在这里定义

  • butterknife-compiler: 扫描自定义注解,并进行相应的处理,生成模板代码等

  • butterknife: 完成代码注入,ButterKnife.bind(Activity)函数就在这里实现

ButterKnife流程总览

在详细拆解之前,我们先总体介绍下butterknife执行流程:

  1. 在编译时使用APT(Annotation Processing Tool)扫描注解,并生成Java代码,主要步骤包括:
    • 自定义注解
    • 自定义注解处理器,重写process()方法,用于编译时处理注解,存放到Map集合中
    • 根据注解集合Map,调用Javapoet生成Java代码
  2. 调用ButterKnife.bind(Activity)时,完成代码注入
生成模板代码

为了在Android Studio上查看相关源码,我们需要修改下依赖:

From:

annotationProcessor 'com.jakewharton:butterknife-compiler:10.2.1'

To:

implementation 'com.jakewharton:butterknife-compiler:10.2.1'

Gradle同步之后,就可以在Project视图上看到相关注解器代码:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8kxn4imH-1587378010107)(D:\Project\2020 - BP\ButterKnife\image-20200418141803204.png)]

自定义注解

从上图看到,ButterKnife提供了许多自定义注解,我们以@BindView为例,窥探其中奥妙:

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

@Retention(RUNTIME):保留时间,默认值为CLASS,保留时间按生命周期划分,可以分为以下3类:

  • SOURCE:注解只保留在源文件,当Java文件编译成class文件的时候,注解被遗弃;
  • CLASS:注解被保留到class文件,但JVM加载class文件时候被遗弃,这是默认的生命周期
  • RUNTIME:注解不仅被保存到class文件中,JVM加载class文件之后,仍然存在;

如果需要在运行时去动态获取注解信息,只能用 RUNTIME 注解;如果要在编译时进行一些预处理操作,就用 CLASS注解;如果只是做一些检查性的操作,比如 @Override@SuppressWarnings,则可选用 SOURCE 注解。
说到这,可能你会有疑惑,@BindView注解前面不是说在编译时期使用么,为啥要用RUNTIME,而不使用CLASS?这是个好问题,其实在10.2.1版本之前,源码确实使用了CLASS, 而在10.2.1中加入了反射处理机制,如果感兴趣,可以看看JakeWharton大神在GitHub上的这个Commit

@Target(FIELD):代表作用域是成员变量,除此之外,还支持方法(METHOD)、参数(PARAMETER)、构造函数(CONSTRUCTOR)等

自定义注解处理器
  1. 继承AbstractProcessor,并在init()中初始化辅助类
//@ButterKnifeProcessor.java
public final class ButterKnifeProcessor extends AbstractProcessor {
	...
    public synchronized void init(ProcessingEnvironment env) {
            super.init(env);
            ...
            this.debuggable = !"false".equals(env.getOptions().get("butterknife.debuggable"));
            this.typeUtils = env.getTypeUtils();
            this.filer = env.getFiler();
            ...
 }
  1. 重写 getSupportedAnnotationTypes() 方法,返回需要支持的注解类型
//@ButterKnifeProcessor.java
public Set<String> getSupportedAnnotationTypes() {
    Set<String> types = new LinkedHashSet();
    //->2.1 获取支持的注解类型
    Iterator var2 = this.getSupportedAnnotations().iterator();

    while(var2.hasNext()) {
        Class<? extends Annotation> annotation = (Class)var2.next();
        types.add(annotation.getCanonicalName());
    }

    return types;
}

//2.1 支持的注解类型
private Set<Class<? extends Annotation>> getSupportedAnnotations() {
    Set<Class<? extends Annotation>> annotations = new LinkedHashSet();
    ...
    annotations.add(BindString.class);
    annotations.add(BindView.class);
    annotations.add(BindViews.class);
    annotations.addAll(LISTENERS);
    return annotations;
}
  1. 重写 process () 方法对扫描到的注解信息进行处理
//@ButterKnifeProcessor.java
public boolean process(Set<? extends TypeElement> elements, RoundEnvironment env) {
		//->3.1获取注解信息
        Map<TypeElement, BindingSet> bindingMap = this.findAndParseTargets(env);
        Iterator var4 = bindingMap.entrySet().iterator();
		//3.5遍历注解信息
        while(var4.hasNext()) {
            Entry<TypeElement, BindingSet> entry = (Entry)var4.next();
            TypeElement typeElement = (TypeElement)entry.getKey();
            BindingSet binding = (BindingSet)entry.getValue();
            //->3.2根据注解信息生成javaFile对象
            JavaFile javaFile = binding.brewJava(this.sdk, this.debuggable);

            try {
           		 //生成java模板代码
                javaFile.writeTo(this.filer);
            } catch (IOException var10) {
                this.error(typeElement, "Unable to write binding for type %s: %s", typeElement, var10.getMessage());
            }
        }

        return false;
    }

可以看到process()主要工作是:获取所有注解信息,并保存到bindingMap中,遍历注解信息,并根据信息生成java代码。

findAndParseTargets () 针对每一个自定义注解都做了处理,我们以BindView为例,捋一捋流程:

//3.1获取注解信息 @ButterKnifeProcessor.java
private Map<TypeElement, BindingSet> findAndParseTargets(RoundEnvironment env) {
        Map<TypeElement, butterknife.compiler.BindingSet.Builder> builderMap = new LinkedHashMap();
        Set<TypeElement> erasedTargetNames = new LinkedHashSet();
        Iterator var4 = env.getElementsAnnotatedWith(BindAnim.class).iterator();

        Element element;
        ...

        var4 = env.getElementsAnnotatedWith(BindView.class).iterator();

        while(var4.hasNext()) {
            element = (Element)var4.next();

            try {
            	//-->3.3 处理BindView注解
                this.parseBindView(element, builderMap, erasedTargetNames);
            } catch (Exception var13) {
                this.logParsingError(element, BindView.class, var13);
            }
        }

        ...

        //3.4初始化bindingMap,这部分代码下文贴出

        return bindingMap;
    }

其中,builderMap是一个key为TypeElementvalueBindingSet.Builder的集合。

接下来看看具体怎么处理@bindView注解的

//3.3 处理BindView注解
private void parseBindView(...) {
        TypeElement enclosingElement = (TypeElement)element.getEnclosingElement();
        //如果是private或者static修饰,报错
        //如果是'android' 或者 'java' 开头的包,报错
        boolean hasError = this.isInaccessibleViaGeneratedCode(BindView.class, "fields", element) || this.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();
        //如果元素类型不为View(使用BindView必定为view),报错
        if (!isSubtypeOfType(elementType, "android.view.View") && !this.isInterface(elementType)) {
            if (elementType.getKind() == TypeKind.ERROR) {
                ...
            } else {
                ...
                hasError = true;
            }
        }

        if (!hasError) {
        	//获取需要绑定View的View Id
            int id = ((BindView)element.getAnnotation(BindView.class)).value();
            //从builderMap中获取builder
            butterknife.compiler.BindingSet.Builder builder = (butterknife.compiler.BindingSet.Builder)builderMap.get(enclosingElement);
            Id resourceId = this.elementToId(element, BindView.class, id);
            String existingBindingName;
            if (builder != null) { //判断builder是否为null
                existingBindingName = builder.findExistingBindingName(resourceId);
                if (existingBindingName != null) {
                    ...
                    //已经绑定该View,直接返回
                    return;
                }
            } else {
            	//如果builder为null,重新创建,并存放到builderMap中
                builder = this.getOrCreateBindingBuilder(builderMap, enclosingElement);
            }

            existingBindingName = simpleName.toString();
            TypeName type = TypeName.get(elementType);
            boolean required = isFieldRequired(element);
            //将view信息添加到builder中
            builder.addField(resourceId, new FieldViewBinding(existingBindingName, type, required));
            erasedTargetNames.add(enclosingElement);
        }
    }

总结一下Process()BindView的处理过程:首先,如果注解元素是由privatestatic修饰,或者非View类型,则直接退出不处理,然后,获取需要绑定的View id,并在buildMap中获取builder, 如果builder不为空且该View已经加载过,则直接退出,如果builder为空,则创建builder并把builder存入builderMap中,最后将View信息存放到该builder中。

findAndParseTargets()方法如何获得bindingMap,我们继续看代码。

//3.1获取注解信息 @ButterKnifeProcessor.java
private Map<TypeElement, BindingSet> findAndParseTargets(RoundEnvironment env) {
	//此处省略代码为前文解析注解元素,初始化buildingMap部分
	...
	//3.4 初始化bindingMap
	//使用builderMap对象构建了双向链表
        Deque<Entry<TypeElement, butterknife.compiler.BindingSet.Builder>> entries = new ArrayDeque(builderMap.entrySet());
        //初始化bindingMap
        LinkedHashMap bindingMap = new LinkedHashMap();

        while(!entries.isEmpty()) {
            Entry<TypeElement, butterknife.compiler.BindingSet.Builder> entry = (Entry)entries.removeFirst();
            TypeElement type = (TypeElement)entry.getKey();
            butterknife.compiler.BindingSet.Builder builder = (butterknife.compiler.BindingSet.Builder)entry.getValue();
            //获取 type 的父类的 TypeElement
            TypeElement parentType = this.findParentType(type, erasedTargetNames, classpathBindings.keySet());
            if (parentType == null) {
            	//如果没有父类,构建builder后直接存入bindingMap中
                bindingMap.put(type, builder.build());
            } else {
            	//如果有父类,获取parentBinding
                BindingInformationProvider parentBinding = (BindingInformationProvider)bindingMap.get(parentType);
                if (parentBinding == null) {
                    parentBinding = (BindingInformationProvider)classpathBindings.get(parentType);
                }

                if (parentBinding != null) {
                	//将parentBinding添加到builder中,构建后存放到bindingMap里
                    builder.setParent(parentBinding);
                    bindingMap.put(type, builder.build());
                } else {
                    entries.addLast(entry);
                }
            }
        }

findAndParseTargets()获取所有注解信息后生成builderMap,并将builderMap中builder构建并存入bindingMap这个HashMap中,并返回,在3.5遍历注解信息中遍历每一个BindingSet对象,调用它的brewJava()方法来生成java代码。

生成java代码
//3.2根据注解信息生成java代码 @ButterKnifeProcessor.java
JavaFile brewJava(int sdk, boolean debuggable) {
        TypeSpec bindingConfiguration = this.createType(sdk, debuggable);
        return JavaFile.builder(this.bindingClassName.packageName(), 												bindingConfiguration).addFileComment(...).build();
}

private TypeSpec createType(int sdk, boolean debuggable) {
        com.squareup.javapoet.TypeSpec.Builder result = 														TypeSpec.classBuilder(this.bindingClassName.simpleName()).addModifiers(new Modifier[]{Modifier.PUBLIC}).addOriginatingElement(this.enclosingElement);
        if (this.isFinal) {
            result.addModifiers(new Modifier[]{Modifier.FINAL});
        }

        if (this.parentBinding != null) {
            result.superclass(this.parentBinding.getBindingClassName());
        } else {
            result.addSuperinterface(UNBINDER);
        }

        if (this.hasTargetField()) {
            result.addField(this.targetTypeName, "target", new Modifier[]{Modifier.PRIVATE});
        }
		//根据类型,添加构造方法
        if (this.isView) {
            result.addMethod(this.createBindingConstructorForView());
        } else if (this.isActivity) {
            result.addMethod(this.createBindingConstructorForActivity());
        } else if (this.isDialog) {
            result.addMethod(this.createBindingConstructorForDialog());
        }

        if (!this.constructorNeedsView()) {
            result.addMethod(this.createBindingViewDelegateConstructor());
        }
 	    //-->3.6添加binding构造函数
        result.addMethod(this.createBindingConstructor(sdk, debuggable));
        if (this.hasViewBindings() || this.parentBinding == null) {
        	//自动生成unBind函数
            result.addMethod(this.createBindingUnbindMethod(result));
        }

        return result.build();
    }

这里使用的是JavaPoet对生成java模板代码,我们继续看看相关实现

//3.6添加binding构造函数 @ButterKnifeProcessor.java
private MethodSpec createBindingConstructor(int sdk, boolean debuggable) {
        com.squareup.javapoet.MethodSpec.Builder constructor = MethodSpec.constructorBuilder().addAnnotation(UI_THREAD).addModifiers(new Modifier[]{Modifier.PUBLIC});
        if (this.hasMethodBindings()) {
        	//如果为方法注解元素,target声明为private
            constructor.addParameter(this.targetTypeName, "target", new Modifier[]{Modifier.FINAL});
        } else {
        	//如果不是方法注解元素,target不声明为private
            constructor.addParameter(this.targetTypeName, "target", new Modifier[0]);
        }

        if (this.constructorNeedsView()) {
        	//如果为view类似,使用source参数
            constructor.addParameter(VIEW, "source", new Modifier[0]);
        } else {
            constructor.addParameter(CONTEXT, "context", new Modifier[0]);
        }

        ...
        UnmodifiableIterator var4;
        if (this.hasViewBindings()) {
            if (this.hasViewLocal()) {
                constructor.addStatement("$T view", new Object[]{VIEW});
            }

            var4 = this.viewBindings.iterator();
		   //遍历viewBindings,生成source.findViewById() 代码
            while(var4.hasNext()) {
                ViewBinding binding = (ViewBinding)var4.next();
                this.addViewBinding(constructor, binding, debuggable);
            }

            var4 = this.collectionBindings.iterator();

            while(var4.hasNext()) {
                FieldCollectionViewBinding binding = (FieldCollectionViewBinding)var4.next();
                constructor.addStatement("$L", new Object[]{binding.render(debuggable)});
            }

            if (!this.resourceBindings.isEmpty()) {
                constructor.addCode("\n", new Object[0]);
            }
        }

        if (!this.resourceBindings.isEmpty()) {
            if (this.constructorNeedsView()) {
                constructor.addStatement("$T context = source.getContext()", new Object[]{CONTEXT});
            }

            if (this.hasResourceBindingsNeedingResource(sdk)) {
                constructor.addStatement("$T res = context.getResources()", new Object[]{RESOURCES});
            }

            var4 = this.resourceBindings.iterator();

            while(var4.hasNext()) {
                ResourceBinding binding = (ResourceBinding)var4.next();
                constructor.addStatement("$L", new Object[]{binding.render(sdk)});
            }
        }

        return constructor.build();
    }

至此,我们知道了ButterKnife是如何通过注解自动生成代码的,接下去,我们探讨下自动生成的代码如何注入到我们的工程中。

代码注入

ButterKnife通过 ButterKnife.bind(this)这一行代码实现代码注入,我们来看看是怎么实现的:

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

可以看到,这里会继续调用带有两个入参的bind(arget, sourceView)函数,我们继续查看

  //@ButterKnife.java
  @NonNull @UiThread
  public static Unbinder bind(@NonNull Object target, @NonNull View source) {
    Class<?> targetClass = target.getClass();
    if (debug) Log.d(TAG, "Looking up binding for " + targetClass.getName());
    //-->4.获取需要注入代码的构造函数
    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);
    } 
    ...
  }

可以看到,bind()主要通过findBindingConstructorForClass()方法获取待注入代码的构造函数,本例需要注入的代码是MainActivity_ViewBinding,获取其构造函数后,然后通过反射的方法,对其进行实例化并完成注入。我们继续看看怎么获取构造函数

@ButterKnife.java
//BINDINGS用来保存绑定和被绑定对象映射
static final Map<Class<?>, Constructor<? extends Unbinder>> BINDINGS = new LinkedHashMap<>();
//4.获取需要注入代码的构造函数 
private static Constructor<? extends Unbinder> findBindingConstructorForClass(Class<?> cls) {
	//从缓存BINDINGS中获取注入代码构造函数
    Constructor<? extends Unbinder> bindingCtor = BINDINGS.get(cls);
    if (bindingCtor != null || BINDINGS.containsKey(cls)) {
      if (debug) Log.d(TAG, "HIT: Cached in binding map.");
      //从缓存获取注入代码构造函数
      return bindingCtor;
    }
    String clsName = cls.getName();
    //如果需要被绑定代码为Framework代码,返回null
    if (clsName.startsWith("android.") || clsName.startsWith("java.")
        || clsName.startsWith("androidx.")) {
      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);
    }
    //将构造器和被注入被绑定
    BINDINGS.put(cls, bindingCtor);
    return bindingCtor;
  }

在代码上我初步给了注释,结合注释,我们总结下findBindingConstructorForClass()的流程:

1、首先从缓存BINDINGS中获取被注入类对象(MainActivity)对应的注入对象(MainActivity_ViewBinding)的构造器,获取成功返回,失败继续往下执行。
2、如果是FrameWork代码,返回null
3、通过被注入类对象的类加载器加载出对应的注入类对象,通过getConstructor()获得构造器,如果获取不到,会从当前 class 文件的父类中再去查找。如果找到了,会将bindingCtor对象存到BINDINGS缓存中。

到这里,我们也就明白了在开篇中提到问题的答案了,ButterKnife中用到了反射,通过反射,ButterKnife主要完成两个工作:

  • 获取模板代码的构造器

  • 对生成模板代码进行实例化

至此,ButterKnife核心源码已经分析完毕,我们在复习下:

  1. 模板生成:在编译的时候扫描注解,并通过自定义的ButterKnifeProcessor解析得到bindingMap对象,最后,调用 Javapoet 库生成java模板代码。

  2. 代码注入:当我们调用 ButterKnife.bind(this)时,ButterKnife会根据调用类,找到相应的模板代码,注入操作。

写在最后

Attention: This tool is now deprecated. Please switch to view binding. Existing versions will continue to work, obviously, but only critical bug fixes for integration with AGP will be considered. Feature development and general bug fixes have stopped.

上面是写在ButterKnife Github上的一段话,目前ButterKnife已经停更,类似功能可以直接使用View Binding,这个是Android官方支持功能,大家可以升级到最新的Android Studio,体验这个功能~

参考

http://jakewharton.github.io/butterknife/

自定义注解之运行时注解(RetentionPolicy.RUNTIME)

butterknife 源码分析