随笔 - 632  文章 - 17  评论 - 54  阅读 - 93万

ButterKnife原理以及源代码分析

一、概述

  相信绝大多数Android开发都用过ButterKnife这个框架,因为其老牌且知名。其通过注解来绑定视图,把开发从烦琐的findViewById中解放出来。

  ButterKnife有两种实现形式:

  1.使用注解编译器来实现,其结果就是编译略微耗时,但其运行基本无损。

  2.完全使用反射来实现,其结果就是编译基本无损,但是运行时就比较吃性能了。案例源代码

  一般情况下我们都会选择使用注解编译器来实现,因为其对于性能来说无损。

  基本原理:

  1.使用BufferKnife.bind(target)方法拿到目标类的Class对象(target.getClass())

  2.根据class获取目标Class的包名+类名,根据拿到的包名+类名+_ViewBinding构造一个构造函数,然后传入targe和target.getWindow().getDecorView(),实例化构造函数。此时bind方法的任务就完成了。

  3.利用注解编译器AnnotationProcessor获取用指定注解标注的类,并解析用注解标注的方法和属性,例如:BindView、OnClick等。

  4.利用JavaPoet根据注解编译器解析出来的信息生成辅助类(包名+类名+ _ViewBinding),而在第二步中实例化的那个对象就是利用JavaPoet生成的。

  

二、使用方法

  使用方法非常的简单,我就注解贴一个小例子上去了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class MainActivity extends AppCompatActivity {
    @BindView(R.id.tv_content)
    TextView tv_content;
    @BindView(R.id.btn_click)
    Button btn_click;
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ButterKnife.bind(this);
 
 
    }
 
    @OnClick(R.id.btn_click)
    public void onClick(View view) {
        switch (view.getId()) {
            case R.id.btn_click:
                Toast.makeText(this, "执行了点击事件", Toast.LENGTH_LONG).show();
                break;
        }
    }
}

  

三、源代码分析

  根据上面的示例我们知道,ButterKnife的入口是bind方法,那我们就直接从bind方法开始讲,看看其都了些什么事情。

1
2
3
4
5
@NonNull @UiThread
 public static Unbinder bind(@NonNull Activity target) {
   View sourceView = target.getWindow().getDecorView();
   return bind(target, sourceView);
 }

  在bind方法中会根据Activity获取Activity所绑定的Window对象,然后利用Window获取顶层视图DecorView,并将Activity和DecorView向下传递

1
2
3
4
5
6
7
8
9
10
11
12
13
@NonNull @UiThread
  public static Unbinder bind(@NonNull Object target, @NonNull View source) {<br>  //获取Activity的Class对象
    Class<?> targetClass = target.getClass();
    //构造一个包名+类名+_ViewBinding的构造函数
    Constructor<? extends Unbinder> constructor = findBindingConstructorForClass(targetClass);
   ...省略了一些代码
    //noinspection TryWithIdenticalCatches Resolves to API 19+ only type.
    try {
      //实例化新类的构造函数,在这个构造函数中会根据传递进去的View、Object或者Activity,解析属性或者方法上的注解(BindView或者OnClick注解)进行相应的findViewById和setonClick操作,从而完成最终的绑定
      return constructor.newInstance(target, source);
     ....省略了一些代码
    }
  }

 以上源码首先会获取目标类的class对象,然后通过findBindingConstructorForClass(targetClass)方法获取一个包名+类名_ViewBinding的构造函数,然后实例化这个构造函数,然后把targe和顶层视图view传递进去。

接下来看下findBindingConstructorForClass(targetClass)方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@Nullable @CheckResult @UiThread
  private static Constructor<? extends Unbinder> findBindingConstructorForClass(Class<?> cls) {
    //先从缓存中拿,如果缓存中有就直接使用,如果没有就创建一个
    Constructor<? extends Unbinder> bindingCtor = BINDINGS.get(cls);
    if (bindingCtor != null || BINDINGS.containsKey(cls)) {
      return bindingCtor;
    }
    //获取类名全称
    String clsName = cls.getName();
    if (clsName.startsWith("android.") || clsName.startsWith("java.")
        || clsName.startsWith("androidx.")) {//为了兼容androidx
      if (debug) Log.d(TAG, "MISS: Reached framework class. Abandoning search.");
      return null;
    }
    try {
      //获取cls的classloader并加载cls.getName()+_ViewBinding从而创建一个新类的Constructor的Class。而加载的这个新的类是项目在编译期间用注解编辑器生成的。
      Class<?> bindingClass = cls.getClassLoader().loadClass(clsName + "_ViewBinding");
      //noinspection unchecked
      //从bindingClass类中获取到Constructor对象
      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);
    }
    //把心类的构造函数放入缓存中一遍下次使用,key=subscriber.getClass(),value=包名+subscriber类名_ViewBinding
    BINDINGS.put(cls, bindingCtor);
    return bindingCtor;
  }

 在findBindingConstructorForClass(targetClass)内部会首先从缓存中查找是否已经有目标类的Class为key的Constructor对象,如果有就直接用,如果没有就提出目标类的包名+类名,然后在后面拼接_ViewBinding,利用classLoader,加载这个新类的对象。然后根据这个类对象获取其构造函数,然后把构造函数放入BINDINGS缓存,最终返回新建的Constructor对象。分析到这里ButterKnife.bind方法的流程已经全部走完了。其实核心及一句话,实例化新类的构造函数。

  这个“包名+类名+_ViewBinding”的类其实是在编译代码的时候利用AnnotationProcessor+JavaPoet生成的。具体代码如下:

  生成的类在:build->generated->ap_generated_sources->debug->out->项目包名->MainActivity_ViewBinding

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public class MainActivity_ViewBinding implements Unbinder {
  private MainActivity target;
 
  private View view7f070042;
 
  @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;
    target.tv_content = Utils.findRequiredViewAsType(source, R.id.tv_content, "field 'tv_content'", TextView.class);
    view = Utils.findRequiredView(source, R.id.btn_click, "field 'btn_click' and method 'onClick'");
    target.btn_click = Utils.castView(view, R.id.btn_click, "field 'btn_click'", Button.class);
    view7f070042 = view;
    view.setOnClickListener(new DebouncingOnClickListener() {
      @Override
      public void doClick(View p0) {
        target.onClick(p0);
      }
    });
  }
 
  @Override
  @CallSuper
  public void unbind() {
    MainActivity target = this.target;
    if (target == null) throw new IllegalStateException("Bindings already cleared.");
    this.target = null;
 
    target.tv_content = null;
    target.btn_click = null;
 
    view7f070042.setOnClickListener(null);
    view7f070042 = null;
  }
}

  在MainActivity_ViewBinding内部会给用BindView标记的视图属性赋值,其最终还是通过findViewById实现的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static View findRequiredView(View source, @IdRes int id, String who) {<br>  //非常熟悉的一行代码,findViewById来查找View并给View赋值
   View view = source.findViewById(id);
   if (view != null) {
     return view;
   }
   String name = getResourceEntryName(source, id);
   throw new IllegalStateException("Required view '"
       + name
       + "' with ID "
       + id
       + " for "
       + who
       + " was not found. If this view is optional add '@Nullable' (fields) or '@Optional'"
       + " (methods) annotation.");
 }

  看到findViewById方法是不是很熟悉,其实本质上依然是通过findViewById来查找view的,只是这部分代码不需要程序员去做了而已。框架在编译时自动帮我们生成好了。

  再来看下点击事件:

1
2
3
4
5
6
view.setOnClickListener(new DebouncingOnClickListener() {
     @Override
     public void doClick(View p0) {
       target.onClick(p0);
     }
   });

  上述代码的target就是MainActivity,而onClick就是我们上面小例子中定义的onClick函数,这个地方就是标准的设置点击事件的地方,通过View的点击事件回调target.onClick函数,从而让demo中的点击事件能够执行。

  以上都只分析了ButterKnife的常规流程,其实ButterKnife的核心是AnnotationProcessor(注解编译器)和JavaPoet。AnnotationProcessor用来将注解标注的类一一找出来,然后解析注解标注的属性、方法。然后JavaPoet利用注解编译器找出注解标注的属性和方法进行生成对应的辅助类。例如:MainActivity_ViewBinding.java就是AnnotationProcessor和JavaPoet合作搞出来的。有了这个辅助类之后我们就可以从大量的findViewById和setOnClickListener中解放出来,因为辅助类已经提前帮我们做好了。

  下面先讲一下AnnotationProcessor

  先来看一下ButterKnife源代码的目录结构:

 

   ButterKnife的注解处理器由ButterKnifeProcessor来完成的,其继承了AbstractProcessor抽象类,源码如下:

1
2
3
4
@AutoService(Processor.class)
@IncrementalAnnotationProcessor(IncrementalAnnotationProcessorType.DYNAMIC)
@SuppressWarnings("NullAway") // TODO fix all these...
public final class ButterKnifeProcessor extends AbstractProcessor {

  要想使用AbstractProcessor则必须要是实现其抽象方法

  1.init(ProcessingEnvironment env)

  init方法完成sdk版本的判断以及相关帮助类的初始化如:Elements:注解处理器运行扫描文件时获取元素相关信息,如: 包(PackageElement)类(TypeElement)成员变量(VariableElement)方法(ExecutableElement)。、Types、Filer、Trees

  2.getSupportedAnnotationTypes()

  返回一个Set<String> 代表要处理的类的名称集合

  3.getSupportedSourceVersion()

  返回当前系统支持的Java版本

  4.getSupportedOptions()

  返回注解处理器可以处理的注解操作

  5.process()

  完成目标类信息的收集以及生成相对应的辅助类

  根据以上ButterKnifeProcessor的五个方法我们分别看一下其源代码,把从代码生成到实例化这一条线给串联起来

  init()方法

1
2
3
4
5
6
7
8
@Override
   public synchronized void init(ProcessingEnvironment env) {
       super.init(env);
    ...省略了上面的代码
       typeUtils = env.getTypeUtils();
       filer = env.getFiler();
      ...省略了下面的代码
   }

  这个目录结构就非常清晰了,它只做了两件事情,初始化TypeUtils和Filer。

  getSupportedAnnotationTypes()

1
2
3
4
5
6
7
8
@Override
 public Set<String> getSupportedAnnotationTypes() {
     Set<String> types = new LinkedHashSet<>();
     for (Class<? extends Annotation> annotation : getSupportedAnnotations()) {
         types.add(annotation.getCanonicalName());
     }
     return types;
 }

  把可用的注解都放入LinkedHashSet中返回。

  接下来直接看process这个方法比较关键,ps:这里我们只分析BindView注解,其他的都是类似的大家可以自行分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@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();
     //利用JavaPoet生成辅助类
          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;
  }

  我们看下findAndParseTargets都干了些什么事情

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
private Map<TypeElement, BindingSet> findAndParseTargets(RoundEnvironment env) {
        Map<TypeElement, BindingSet.Builder> builderMap = new LinkedHashMap<>();
        Set<TypeElement> erasedTargetNames = new LinkedHashSet<>();
        // 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);
            }
        }
 
         
        Map<TypeElement, ClasspathBindingSet> classpathBindings =
                findAllSupertypeBindings(builderMap, erasedTargetNames);
 
        // 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, classpathBindings.keySet());
            if (parentType == null) {
                bindingMap.put(type, builder.build());
            } else {
                BindingInformationProvider parentBinding = bindingMap.get(parentType);
                if (parentBinding == null) {
                    parentBinding = classpathBindings.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;
    }

  首先将扫描到的注解相关的信息保存的builderMap中,然后将这些信息重新组合返回一个key=TypeElement,value=BindingSet的Map集合。其中TypeElement代表使用了ButterKnife的类,而BindingSet存储的是生成类的基本信息以及注解元素的基本信息。其中parseView方法用来解析使用了BindView注解的元素。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
private void parseBindView(Element element, Map<TypeElement, BindingSet.Builder> builderMap,
                               Set<TypeElement> erasedTargetNames) {
        // 首先要注意,此时element是VariableElement类型的,即成员变量
        // enclosingElement是当前元素的父类元素,一般就是我们使用ButteKnife时定义的View类型成员变量所在的类,可以理解为之前例子中的MainActivity
        TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();
 
        // 进行相关校验
        // 1、isInaccessibleViaGeneratedCode(),先判断当前元素的是否是private或static类型,
        // 再判断其父元素是否是一个类以及是否是private类型。
        // 2、isBindingInWrongPackage(),是否在系统相关的类中使用了ButteKnife注解
        boolean hasError = isInaccessibleViaGeneratedCode(BindView.class, "fields", element)
                || isBindingInWrongPackage(BindView.class, element);
 
        // TypeMirror表示Java编程语言中的一种类型。 类型包括基元类型,声明的类型(类和接口类型),数组类型,类型变量和空类型。
        // 还表示了通配符类型参数,可执行文件的签名和返回类型,以及与包和关键字void相对应的伪类型。
        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 的子类,或者是接口,不是的话抛出异常
        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;
        }
 
        // 获得元素使用BindView注解时设置的属性值,即 View 对应的xml中的id
        int id = element.getAnnotation(BindView.class).value();
        // 尝试获取父元素对应的BindingSet.Builder
        BindingSet.Builder builder = builderMap.get(enclosingElement);
        // QualifiedId记录了当前元素的包信息以及id
        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)",
                        BindView.class.getSimpleName(), id, existingBindingName,
                        enclosingElement.getQualifiedName(), element.getSimpleName());
                return;
            }
        } else {
            // 创建一个新的BindingSet.Builder并返回,并且以enclosingElement 为key添加到builderMap中
            builder = getOrCreateBindingBuilder(builderMap, enclosingElement);
        }
 
        String name = simpleName.toString();
        TypeName type = TypeName.get(elementType);
        // 判断当前元素是否使用了Nullable注解
        boolean required = isFieldRequired(element);
        // 创建一个FieldViewBinding,它包含了元素名、类型、是否是Nullable
        // 然后和元素id一同添加到BindingSet.Builder
        builder.addField(getId(qualifiedId), new FieldViewBinding(name, type, required));
 
        // 记录当前元素的父类元素
        erasedTargetNames.add(enclosingElement);
    }

  

  

  

posted on   飘杨......  阅读(1300)  评论(0编辑  收藏  举报
编辑推荐:
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
阅读排行:
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
< 2025年3月 >
23 24 25 26 27 28 1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30 31 1 2 3 4 5

点击右上角即可分享
微信分享提示