Paste_Image.png

大家好,今天老衲给大家带来的是Android另一款注解框架,ButterKnife的使用介绍及代码分析。

使用方式:

  1. 导入Butterknife的jar包。
    不需要修改配置文件有木有,超级简单有木有,→_→
  2. 添加AndroidStudio插件(可选,需要依赖ButterKnife的jar包)
    下载一个插件Android ButterKnife Zelezny来配合Butterknife自动生成View。


    JfQ73eI.gif

注意,需要绑定的View或者资源的声明必须是public,不能是private或者static,至于原因,我们会在下面的分析中讲到

Butterknife常用的注解:
Butterknife支持Activity,Fragment,View,Dialog,ViewHolder类内部的View绑定@BindTextView mTextView//最常用的注解,用来绑定View,避免findViewById,也可以用在ViewHolder里,必须是public@Bind({ R.id.first_name, R.id.middle_name, R.id.last_name })List nameViews//绑定多个view,只能用List不能用ArrayList@OnClick(R.id.submit)public void submit(View view) {...}//绑定点击事件,支持多个id绑定同一个方法@OnItemSelected(R.id.list_view)void onItemSelected(int position) {...}//selected事件@OnItemClick(R.id.example_list) void onItemClick(int position) {...}//itemClick事件@OnFocusChange(R.id.example) void onFocusChanged(boolean focused){...}//焦点改变监听@OnItemLongClick(R.id.example_list) boolean onItemLongClick(int position){...}//长按监听@OnPageChange(R.id.example_pager) void onPageSelected(int position){...}//Viewpager切换监听@OnTextChanged(R.id.example) void onTextChanged(CharSequence text)//内容改变监听@BindInt//用来绑定Integer类型的resource ID@BindString//用来绑定string.xml里的字符串@BindDrawable//用来绑定图片@BindColor//用来绑定颜色@BindDimen//用来绑定dimens

ButterKnife所提供的注解的着重点放在了View的处理上,减少了开发时View处理的时间,相对于AndroidAnnotation来说,功能较为的单一。

Butterknife的实现流程

概述:Butterknife在编译时刻利用APT分析程序代码,扫描每一个有注解的类,找出类中带有注解的字段
@Bind生成ViewBinding的子类,
监听类的生成ListenerBinding的子类,
通过Java的FilerAPI生成多个包含注入代码的辅助类,程序中调用ButterKnife.bind()方法时加载这些辅助类实现依赖注入。

1.绑定XML布局

为Android的View绑定ID或者方法

@Overrideprotected void onCreate(Bundle savedInstanceState) {    super.onCreate(savedInstanceState);    setContentView(R.layout.activity_main);    //绑定activity    ButterKnife.bind(this);    text.setText("HELLO , WORLD");}
2.ButterKnife.bind内部的处理
static void bind(@NonNull Object target, @NonNull Object source, @NonNull Finder finder) {//当前的activity,dialog,fragment,View等Class<?> targetClass = target.getClass();try {    //1.创建一个类的实例 , 加入到缓存,并返回该实例    ViewBinder viewBinder = findViewBinderForClass(targetClass);    //2.调用了实例中的bind方法    viewBinder.bind(finder, target, source);} catch (Exception e) {  throw new RuntimeException("Unable to bind views for " + targetClass.getName(), e);}}  
2.1.创建类的实例。

举个栗子:
现在我在activity中执行ButterKnife.Bind方法。他会去寻找XXXActivity$$ViewBinder这个类,通过反射加载并创建类的实例对象。至于为什么要找这个类,这个会在下面分析。我们先来看下接下来的操作

private static ViewBinder findViewBinderForClass(Class<?> cls)    throws IllegalAccessException, InstantiationException {ViewBinder viewBinder = BINDERS.get(cls);if (viewBinder != null) {  //首先从缓存中判断是否存在对应的ViewBinder,如果有直接返回  if (debug) Log.d(TAG, "HIT: Cached in view binder map.");  return viewBinder;}String clsName = cls.getName();if (clsName.startsWith("android.") || clsName.startsWith("java.")) {  //ButterKnife提供的注解只支持在应用程序使用,如果扫描的是framework层的类,则返回NOP_VIEW_BINDER  if (debug)     Log.d(TAG, "MISS: Reached framework class. Abandoning search.");  return NOP_VIEW_BINDER;}try {  //根据反射原理,构造了类的实例,其实就是各种监听的生成类,详情如下图  Class<?> viewBindingClass = Class.forName(clsName + "$$ViewBinder");  viewBinder = (ViewBinder) viewBindingClass.newInstance();  if (debug)         Log.d(TAG, "HIT: Loaded view binder class.");} catch (ClassNotFoundException e) {  if (debug)         Log.d(TAG, "Not found. Trying superclass " + cls.getSuperclass().getName());  //如果查找不到就递归去查找SuperClass$$ViewBinder这个类。  viewBinder = findViewBinderForClass(cls.getSuperclass());}  //添加到缓存中  BINDERS.put(cls, viewBinder);return viewBinder;}  
2.2.执行类的实例的Bind方法。这里我们通过反编译APK逆推下

先介绍下ViewBinder,他是一个接口。所有使用过ButterKnife中@Bind注解的类都会生成一个中间工具类用来实现View绑定或者数据绑定的业务逻辑,这个中间工具类会继承原始类(Activity,Fragment)并实现ViewBinder接口,ViewBinder中有一个抽象方法bind,这便是ButterKnife需要来替我们实现的用来绑定数据方法。如下
温馨提示:ButterKnife官网也有类似的介绍,如果下面的看不懂可以去官网查看。

/** * Created by alexshaw on 16-3-26. */public class MainActivity extends AppCompatActivity {        @Bind(R.id.text)        TextView mText;        @Bind(R.id.confirm)        Button mConfirm;        @Bind(R.id.cancle)        Button mCancle;        @BindString(R.string.hello)        String mString;        @Bind(R.id.icon)        ImageView mIcon;                @BindDrawable(R.drawable.ic_launcher)        Drawable drawable;        @Override      protected void onCreate(Bundle savedInstanceState) {           super.onCreate(savedInstanceState);        setContentView(R.layout.activity_main);              ButterKnife.bind(this);              mText.setText(mString);              mIcon.setImageDrawable(drawable);      }        @OnClick(R.id.confirm)        public void onConfirm(View view) {        mText.setText("确认");    }       @OnClick(R.id.cancle)        public void onCancle(View view) {        mText.setText("取消");    }}

如上代码如果编译成APK,生成的新代码会如何呢?如下图
首先是MAinActivity(因篇幅问题,这里只展示重要的代码,见谅)

Paste_Image.png

这样我们就可以看到onCreate中调用了ButterKnife.Bind方法。我们之前分析了Bind方法的业务逻辑,无非两步

  1. 查找XXX$$ViewBinder并创建实例
  2. 调用实例的bind方法
    此时我们在看下ButterKnife帮助我们生成的中间类
Paste_Image.png

接下来就是分析MainActivity$$ViewBinder这个类了

Paste_Image.png

简单介绍下bind方法的参数,第二个参数和第三个参数是相同的,都是Activity或者Fragment等类的实例,第一个参数为Finder,他是一个枚举类型,提供了一系列用来查找指定ID的View或者资源的方法。

这时再来看bind方法,我们就可以理解了。paramT就是被绑定的类(Activity,Fragment)它内部的属性通过Find这个类来查找并赋值。在这里我们就可以解释文章开始时遗留下的问题了。为什么@Bind注解绑定的变量必须声明为public。

原理我们介绍了,接下来。我们就要介绍下实现流程了。

Butterknife的处理流程

但凡涉及到注解的处理,都需要找AbstractProcessor或者是其实现类 , Butterknife的实现入口是ButterKnifeProcessor类,该类继承自AbstractProcessor并重写了process方法来处理添加了注解的Java类。

1. 注解处理的入口

@Override public boolean process(Set<? extends TypeElement> elements, RoundEnvironment env) {//存储BindingClass的集合,重点是findAndParseTargets方法Map targetClassMap = findAndParseTargets(env);for (Map.Entry entry : targetClassMap.entrySet()) {  TypeElement typeElement = entry.getKey();  BindingClass bindingClass = entry.getValue();  try {    bindingClass.brewJava().writeTo(filer);  } catch (IOException e) {    error(typeElement, "Unable to write view binder for type %s: %s", typeElement,        e.getMessage());  }}return true;}

findAndParseTargets方法是用来查找出所有注解标注过的元素他的业务逻辑如下:(因代码是在太长,这里只显示主要代码,其他业务逻辑用注释表示)

private MapfindAndParseTargets(RoundEnvironment     env) {  // 遍历每一个@Bind元素  for (Element element : env.getElementsAnnotatedWith(Bind.class)) {    // 校验是否合法    if (!SuperficialValidation.validateElement(element))      continue;    try {      parseBind(element, targetClassMap, erasedTargetNames);    } catch (Exception e) {      logParsingError(element, Bind.class, e);    }  }  // 遍历每一个监听的注解元素  for (Class<? extends Annotation> listener : LISTENERS) {    findAndParseListener(env, listener, targetClassMap,   erasedTargetNames);  }    // 遍历 @BindArray 元素.    // 遍历 @BindBool 元素.    // 遍历 @BindColor 元素.    // 遍历 @BindDimen 元素.    // 遍历 @BindDrawable 元素.    // 遍历 @BindInt 元素.    // 遍历 @BindString 元素.    // 遍历 @Unbinder 元素. }

2. 接下来就是解析Bind注解元素和各种监听类注解元素的逻辑处理

/** * @Bind注解的处理 *  * @param element * @param targetClassMap * @param erasedTargetNames *///遍历每一个被注解标注过得属性或方法private void parseBindOne(Element element, Map targetClassMap,  Set erasedTargetNames) {//第一步:进行校验...判断目标字段的定义类型是否是View的子类型或者是一个接口类型,  检查目标字段的可访问性,是否只绑定了一个ID,balabala...//第二步:判断集合中是否有元素对应的bindingClassBindingClass bindingClass = targetClassMap.get(enclosingElement);if (bindingClass != null) {//如果有:判断是否重复绑定  //通过ID拿到ViewBinders  ViewBindings viewBindings = bindingClass.getViewBinding(id);  if (viewBindings != null) {    Iterator iterator = viewBindings.getFieldBindings().iterator();    if (iterator.hasNext()) {      FieldViewBinding existingBinding = iterator.next();      error(...);      return;    }  }} else {//如果没:创建一个BindingClass并添加到集合里去  bindingClass = getOrCreateTargetClass(targetClassMap, enclosingElement);}String name = element.getSimpleName().toString();TypeName type = TypeName.get(elementType);boolean required = isFieldRequired(element);FieldViewBinding binding = new FieldViewBinding(name, type, required);bindingClass.addField(id, binding);// Add the type-erased version to the valid binding targets set.erasedTargetNames.add(enclosingElement);/** * 解析监听注解 * @param annotationClass * @param element * @param targetClassMap * @param erasedTargetNames * @throws Exception */private void parseListenerAnnotation(Class<? extends Annotation> annotationClass, Element element,  Map targetClassMap, Set erasedTargetNames)  throws Exception {//第一步做各种判断...//返回类型是否是int[]//是否是private或者static//ID是否重复//监听注解类是否存在//ID是否合法//balabala太长了..愁死我了...//重点!!!!!!!!!!!!Parameter[] parameters = Parameter.NONE;if (!methodParameters.isEmpty()) {    //方法的参数的判断及处理,后面会用到}//通过将方法名称,参数,required组合成一个MethodViewBinding并传入bindingClass,//最后将bindClass传入集合中MethodViewBinding binding = new MethodViewBinding(name, Arrays.asList(parameters), required);BindingClass bindingClass = getOrCreateTargetClass(targetClassMap, enclosingElement);for (int id : ids) {  if (!bindingClass.addMethod(id, listener, method, binding)) {    error(element, "Multiple listener methods with return value specified for ID %d. (%s.%s)",        id, enclosingElement.getQualifiedName(), element.getSimpleName());    return;  }}erasedTargetNames.add(enclosingElement);  /**       * 创建targetClass,即XXX$$ViewBinder类,这里确定了XXX的名字类型等信息     * @param targetClassMap        * @param enclosingElement        * @return        */  private BindingClass getOrCreateTargetClass(Map targetClassMap,    TypeElement enclosingElement) {        BindingClass bindingClass = targetClassMap.get(enclosingElement);       if (bindingClass == null) {            //类或者接口的全名        String targetType = enclosingElement.getQualifiedName().toString();          // BINDING_CLASS_SUFFIX = "$$ViewBinder",新生成的类的名字              String classPackage = getPackageName(enclosingElement);        //包名,类名,完全限定名称          String className = getClassName(enclosingElement, classPackage) + BINDING_CLASS_SUFFIX;              bindingClass = new BindingClass(classPackage, className, targetType);            targetClassMap.put(enclosingElement, bindingClass);  }          return bindingClass;    }

3. 属性和方法都解析完了,targetClassMap集合中的数据也齐全了。剩下的就是依靠数据生成新的中间类文件了。

@Override public boolean process(Set<? extends TypeElement> elements, RoundEnvironment env) {Map targetClassMap = findAndParseTargets(env);//遍历map并生成文件for (Map.Entry entry : targetClassMap.entrySet()) {    bindingClass.brewJava().writeTo(filer);    }return true;}

来瞅一眼文件生成的方法。bindingClass.brewJava()方法

  JavaFile brewJava() {//添加类名 public class XXX extends XXX implement XXXTypeSpec.Builder result = TypeSpec.classBuilder(className)    .addModifiers(PUBLIC)    .addTypeVariable(TypeVariableName.get("T", ClassName.bestGuess(targetClass)));if (parentViewBinder != null) {  result.superclass(ParameterizedTypeName.get(ClassName.bestGuess(parentViewBinder),      TypeVariableName.get("T")));} else {  result.addSuperinterface(ParameterizedTypeName.get(VIEW_BINDER, TypeVariableName.get("T")));}//添加方法result.addMethod(createBindMethod());if (hasUnbinder()) {  // Create unbinding class.  result.addType(createUnbinderClass());  //sss  createUnbinderInternalAccessMethods(result);}return JavaFile.builder(classPackage, result.build())    .addFileComment("Generated code from Butter Knife. Do not modify!")    .build();

}

addMethod方法中的代码

 /** * 创建最终反编译得到的class文件中的bind方法的代码 *  */    private MethodSpec createBindMethod() {MethodSpec.Builder result = MethodSpec.methodBuilder("bind")//方法名    .addAnnotation(Override.class)//添加override注解    .addModifiers(PUBLIC) //public    .addParameter(FINDER, "finder", FINAL)//param1    .addParameter(TypeVariableName.get("T"), "target", FINAL)//param2    .addParameter(Object.class, "source");//param3    balabala...    //查看viewIDMap集合是否有数据,即是否有绑定ID的View,如果有则加进去if (!viewIdMap.isEmpty() || !collectionBindings.isEmpty()) {    result.addStatement("$T view", VIEW);  //添加bind方法里的代码:“view = finder.findOptionalView(source, $L, null)”  for (ViewBindings bindings : viewIdMap.values()) {    addViewBindings(result, bindings);  }  //绑定集合  for (Map.Entry entry : collectionBindings.entrySet()) {    emitCollectionBinding(result, entry.getKey(), entry.getValue());  }}balabala....

最后将生成的这些字符串写到java文件中。然后就可以编译成class文件了。。
最后使用FilerAPI创建辅助类文件,BindingClass的brewJava()方法根据模型“酝酿”Java代码,之后使用Java IO流把代码写入文件。

AndroidAnnotation(AA)与ButterKnife的比较,

AA的分析如果没看的话建议先读一下老衲的上一篇AA注解的介绍与流程分析

  1. 首先从功能上来说,AA提供的注解数量远多于ButterKnife,功能也是无所不包(View的绑定,线程,监听,动画,balabala...)而ButterKnife仅仅提供针对View的注解。
  2. 其次从两类框架的实现流程上来说,AA在一开始就已经生成了新的代码XXXActivity_,后续的执行都是依赖于新的代码。生成的方法和代码量较多。ButterKnife在编译时也是会生成新的中间工具类,代码量相对于AA来说略少,但是新增了类文件。并且,在运行时,需要通过一点点反射的技术来实现整体的逻辑。
  3. 第三,从上手成都上来说,AA的前期工作略麻烦一些,并且后期需要手动修改类名(XXX的后面加上下划线)ButterKnife则需要在类中添加ButterKnife.Bind方法来使用绑定功能。AA稍微麻烦一丢丢。

好了,ButterKnife的使用介绍,流程分析以及它和AA之间的比较已经写完,如果有什么意见或者不对的地方,请大家指正。
接下来想要给带来的是第三款注解框架Dagger2的使用及流程分析,以及现阶段流行的图片加载资源库的分析,但是,由于老衲智商最近一直没上线→_→,导致Dagger2的环境配了一个多星期还没配好。所以推出时间可能会略晚,希望大家见谅。

更多相关文章

  1. Android中对于onMeasure()的理解
  2. Android学习笔记——网络技术
  3. Android(安卓)Fragment---管理Fragment
  4. 关于android:configChanges="keyboardHidden|orientation"
  5. android之两种设置全屏或者无标题的方法
  6. Android(安卓)Studio更新 Gradle错误解决方法
  7. Unity3d与Android的相互调用
  8. android studio快捷键集合
  9. 关于android WebViewClient 的方法解释

随机推荐

  1. android自动化测试--monkey runner
  2. python logging 模块(一)
  3. MaterialDesign初探-Theme
  4. Android(安卓)Studio 下载与安装教程(最简
  5. Android开发之ListView详解 以及简单的li
  6. Android中的消息通知-Toast和Notificatio
  7. Android应用开发学习笔记之播放视频
  8. Android(安卓)Day01-电话拨号器案例&Andr
  9. Android-EventBus(手写简易版)
  10. Android中不使用AIDL实现Service的远程调