手撕黄油刀--探究ButterKnife实现原理

上篇文章《Java编译时注解处理器(APT)详解》中学习了Java APT技术在Android中的使用,并且我们知道,当前Android开发中常用的许多框架都使用了APT技术,并且ButterKnife就是利用APT来实现的。那么本篇内容我们就来探究一下ButterKnife的实现原理。

一、ButterKnife的bind过程

当然,在探究源码,还是首先应该了解如何使用,我们以ButterKnife的BindView为例。首先在Activity的onCreate方法中通过ButterKnife.bind()方法绑定,然后为TextView添加BindView注解即可拿到TextView的实例。代码如下:

package com.zhpan.app;

public class MainActivity extends AppCompatActivity {
	 @BindView(R.id.text_view)
	 TextView mTextView;
	
	    @Override
	    protected void onCreate(Bundle savedInstanceState) {
	        super.onCreate(savedInstanceState);
	        setContentView(R.layout.activity_main);
	        ButterKnife.bind(this);
	    }
    }

ButterKnife是怎么做到通过注解获取TextView的实例的呢?我们就从bind方法开始吧,点击ButterKnife的bind方法,其代码如下:

//	ButterKnife中有很多bind的重载方法,仅以此为例
 public static Unbinder bind(@NonNull Activity target) {
    View sourceView = target.getWindow().getDecorView();
    return createBinding(target, sourceView);
  }

可以看到在bind方法中通过Activity拿到了DecorView(至于不懂DecorView是什么的同学可以自行Google)。然后调用了createBinding方法,并传入Activity对象和DecorView两个参数。我们追踪createBinding的代码如下:

private static Unbinder createBinding(@NonNull Object target, @NonNull View source) {
    Class<?> targetClass = target.getClass();
    //	获取到了与target相关的类的构造方法
    Constructor<? extends Unbinder> constructor = findBindingConstructorForClass(targetClass);

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

    //noinspection TryWithIdenticalCatches Resolves to API 19+ only type.
    try {
    //	通过构造方法反射实例化了这个类,这个类接收两个参数,分别是上边的Activity和DecorView
      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);
    }
  }

上述代码中通过findBindingConstructorForClass()方法得到了某个类的构造方法,并在接下来的代码中通过反射 constructor.newInstance(target, source)实例化了这个类,我们点进findBindingConstructorForClass()方法看这里做了什么。

private static Constructor<? extends Unbinder> findBindingConstructorForClass(Class<?> cls) {
	//	从BINDINGS中获取cls对应的Unbinder
    Constructor<? extends Unbinder> bindingCtor = BINDINGS.get(cls);
    if (bindingCtor != null) { 如果已存在直接return
      if (debug) Log.d(TAG, "HIT: Cached in binding map.");
      return bindingCtor;
    }
    //	这里拿到的clsName应该为“com.zhpan.app.MainActivity”
    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 {
    //	从类加载器中获取“com.zhpan.app.MainActivity_ViewBinding”
      Class<?> bindingClass = cls.getClassLoader().loadClass(clsName + "_ViewBinding");
      // 获取“com.zhpan.app.MainActivity_ViewBinding”类中的构造方法
      bindingCtor = (Constructor<? extends Unbinder>) bindingClass.getConstructor(cls, View.class);
    } 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;
  }

直接看第7行,通过 cls.getName()得到了bind方法中参数的名字,由于我们是再MainActivity中调用的bind方法,因此,这里拿到的名字就是“com.zhpan.app.MainActivity",接下来再第13行中通过ClassLoader拿到了一个名字是”clsName"+"_ViewBinding"的类,也就是名字为MainActivity_ViewBinding的类。我们在项目里搜索一下,发现果真能找到这个类,其代码如下:

package com.zhpan.app;

public class MainActivity_ViewBinding implements Unbinder {
  private MainActivity target;

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

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

    target.mTextView = Utils.findRequiredViewAsType(source, R.id.text_view, "field 'mTextView'", TextView.class);
  }

  @Override
  @CallSuper
  public void unbind() {
    MainActivity target = this.target;
    if (target == null) throw new IllegalStateException("Bindings already cleare![在这里插入图片描述](https://img-blog.csdnimg.cn/20190825162306549.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzIwNTIxNTcz,size_16,color_FFFFFF,t_70)d.");
    this.target = null;

    target.mTextView = null;
  }
}

在这个类的构造方法中我们可以看到

target.mTextView = Utils.findRequiredViewAsType(source, R.id.text_view, "field 'mTextView'", TextView.class);

通过这句代码给Activity中的mTextView赋了值,到这里也就不奇怪为什通过一个BindView的注解就得到了mTextView的对象了。而MainActivity_ViewBinding这个类是从哪里来的呢?想必看过上篇文章同学应该都知道了,就是通过APT在代码编译期间自动生成的。其实后面的代码其实已经没有必要去看了,无非就是通过AbstractProcessor来处理注解,然后根据注解自动生成所需代码的。但是写文章要有始有终,也本着负责任的态度,还是深入ButtereKnife内部一探究竟。

二、探究ButterKnife的注解处理器ButterKnifeProcessor

现在我们来看一下ButterKnife的代码的模块结构:

在这里插入图片描述
看到butterknife-annotations和butterknife-compiler很眼熟?和上篇文章我们自己写的代码结构是一样的,butterknife-annotations模块下存放的是butterknife的所有注解,butterknife-compiler是用来处理butterknife-annotations注解的。所以们直接看butterknife-compiler模块下的ButterKnifeProcessor类。

1.ButterKnifeProcessor的getSupportedAnnotationTypes()方法

我们知道ButterKnife除了BindView注解之外还有许多其它注解,比如常用的BindColor注解、OnClick注解等。这些注解的名字都会被添加到getSupportedAnnotationTypes方法中的Set集合里。来看getSupportedAnnotationTypes的源码:

@Override public Set<String> getSupportedAnnotationTypes() {
    Set<String> types = new LinkedHashSet<>();
    //	遍历ButterKnife中的所有注解,并将注解名字添加到Set集合
    for (Class<? extends Annotation> annotation : getSupportedAnnotations()) {
      types.add(annotation.getCanonicalName());
    }
    return types;
  }

2.ButterKnifeProcessor的process方法

我们知道process方法是整个注解处理其的核心方法,对注解的处理以及代码的生成都是在这个方法里边实现的。那么接下来,就一步步看ButterKnife是怎么处理注解和生成代码的。
(1)解析RoundEnviroment
在process方法的第一行我们看到通过findAndParseTargets方法得到了一个 Map<TypeElement, BindingSet>集合,代码如下:

public boolean process(Set<? extends TypeElement> elements, RoundEnvironment env) {
    //	通过findAndParseTargets处理注解得到BindingSet的集合
    Map<TypeElement, BindingSet> bindingMap = findAndParseTargets(env);
    
     ...
  
    return false;
  }

而在findAndParseTargets(env)是对一系列注解的处理,这里我们仅以处理BindView为例:

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

    for (Element element : env.getElementsAnnotatedWith(BindView.class)) {
      try {
        parseBindView(element, builderMap, erasedTargetNames);
      } catch (Exception e) {
        logParsingError(element, BindView.class, e);
      }
    }

	//	省略处理其它注解的代码
		...
		
	

第9行在parseBindView方法中对BindView注解进行了一系列的校验,通过校验后解析BindView的数据,比如view的Id等信息封装到了BindingSet.Builder中,并最终将封装好的BindingSet.Builder放入builderMap 中。由于parseBindView方法的代码比较多,这里就不再贴出了,有兴趣的可以直接下载源码查看。
接下来得到builderMap的集合之后,由于findAndParseTargets方法返回值是Map<TypeElement, BindingSet>,因此接下来又对builderMap集合进行了遍历并最终存储到了 Map<TypeElement, BindingSet>中。代码如下:

private Map<TypeElement, BindingSet> findAndParseTargets(RoundEnvironment env) {
		
		...
	
	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方法中,在拿到bindingMap 之后遍历bindingMap ,并调用BindingSet的brewJava来生成Java文件,代码如下:

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, useAndroidX);
      try {
        javaFile.writeTo(filer);
      } catch (IOException e) {
        error(typeElement, "Unable to write binding for type %s: %s", typeElement, e.getMessage());
      }
    }

    return false;
  }

3.JavaPoet生成代码

BindingSet 的brewJava方法中同样是使用了JavaPoet来生成代码的,无非就是拼接类,构造方法、方法,这些感觉真的没什么说的了。

JavaFile brewJava(int sdk, boolean debuggable, boolean useAndroidX) {
    TypeSpec bindingConfiguration = createType(sdk, debuggable, useAndroidX);
    return JavaFile.builder(bindingClassName.packageName(), bindingConfiguration)
        .addFileComment("Generated code from Butter Knife. Do not modify!")
        .build();
  }
private TypeSpec createType(int sdk, boolean debuggable, boolean useAndroidX) {
    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);
    }

    if (isView) {
      result.addMethod(createBindingConstructorForView(useAndroidX));
    } else if (isActivity) {
      result.addMethod(createBindingConstructorForActivity(useAndroidX));
    } else if (isDialog) {
      result.addMethod(createBindingConstructorForDialog(useAndroidX));
    }
    if (!constructorNeedsView()) {
      // Add a delegating constructor with a target type + view signature for reflective use.
      result.addMethod(createBindingViewDelegateConstructor(useAndroidX));
    }
    result.addMethod(createBindingConstructor(sdk, debuggable, useAndroidX));

    if (hasViewBindings() || parentBinding == null) {
      result.addMethod(createBindingUnbindMethod(result, useAndroidX));
    }

    return result.build();
  }

关于ButterKnife的源码就说这么多吧。总结一下,其实就是根据注解使用APT及JavaPoet在项目编译期间自动生成代码,并达到为注解元素赋值或者添加监听的目的。在阅读Butterknife时候不一定要把每一句代码都搞懂,能做到掌握其核心原理便可。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
黄油ButterKnife)是一款Android视图的字段和方法绑定快速注解框架。它是由JakeWharton开发的,可以帮助Android开发者简化代码,省去繁琐的findViewById操作。 使用黄油可以在代码中使用注解的方式来绑定视图,避免了动查找和绑定视图的过程,提高了开发效率。在使用黄油前,需要在build.gradle文件中添加依赖: implementation 'com.jakewharton:butterknife:10.2.3'// 添加此依赖 annotationProcessor 'com.jakewharton:butterknife-compiler:10.2.3'// 添加此规则 然后,在需要使用黄油的Activity或Fragment中,可以通过注解的方式绑定视图。黄油使用的注解并不是在运行时反射的,而是在编译时生成新的class,所以对性能基本没有损失。 总的来说,黄油是一个方便易用的Android视图绑定框架,可以帮助开发者简化代码,提高开发效率。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *3* [Android Butterknife黄油) 使用方法总结](https://blog.csdn.net/donkor_/article/details/77879630)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] - *2* [Android(ButterKnife黄油使用详解](https://blog.csdn.net/ojbk99267710/article/details/126098708)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值