android注解Butterknife的使用及代码分析

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绑定


@Bind
TextView mTextView//最常用的注解,用来绑定View,避免findViewById,也可以用在ViewHolder里,必须是public

@Bind({ R.id.first_name, R.id.middle_name, R.id.last_name })
List<EditText> 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或者方法

@Override
protected 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<Object> 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<Object> findViewBinderForClass(Class<?> cls)
    throws IllegalAccessException, InstantiationException {
ViewBinder<Object> 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<Object>) 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<TypeElement, BindingClass> targetClassMap = findAndParseTargets(env);

for (Map.Entry<TypeElement, BindingClass> 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 Map<TypeElement, BindingClass>findAndParseTargets(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<TypeElement, BindingClass> targetClassMap,
  Set<TypeElement> erasedTargetNames) {
//第一步:进行校验...判断目标字段的定义类型是否是View的子类型或者是一个接口类型,
  检查目标字段的可访问性,是否只绑定了一个ID,balabala...

//第二步:判断集合中是否有元素对应的bindingClass
BindingClass bindingClass = targetClassMap.get(enclosingElement);
if (bindingClass != null) {//如果有:判断是否重复绑定
  //通过ID拿到ViewBinders
  ViewBindings viewBindings = bindingClass.getViewBinding(id);
  if (viewBindings != null) {
    Iterator<FieldViewBinding> 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<TypeElement, BindingClass> targetClassMap, Set<TypeElement> 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<TypeElement, BindingClass> 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<TypeElement, BindingClass> targetClassMap = findAndParseTargets(env);
//遍历map并生成文件
for (Map.Entry<TypeElement, BindingClass> entry : targetClassMap.entrySet()) {
    bindingClass.brewJava().writeTo(filer);
    }
return true;
}

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

  JavaFile brewJava() {
//添加类名 public class XXX extends XXX implement XXX
TypeSpec.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<FieldCollectionViewBinding, int[]> 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的环境配了一个多星期还没配好。所以推出时间可能会略晚,希望大家见谅。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 156,907评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,546评论 1 289
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,705评论 0 238
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,624评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,940评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,371评论 1 210
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,672评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,396评论 0 195
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,069评论 1 238
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,350评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,876评论 1 256
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,243评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,847评论 3 231
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,004评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,755评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,378评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,266评论 2 259

推荐阅读更多精彩内容