android注解Butterknife的使用及代码分析
大家好,今天老衲给大家带来的是Android另一款注解框架,ButterKnife的使用介绍及代码分析。
使用方式:
- 导入Butterknife的jar包。
不需要修改配置文件有木有,超级简单有木有,→_→ -
添加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
2.1.创建类的实例。
举个栗子:
现在我在activity中执行ButterKnife.Bind方法。他会去寻找XXXActivity$$ViewBinder这个类,通过反射加载并创建类的实例对象。至于为什么要找这个类,这个会在下面分析。我们先来看下接下来的操作
private static 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(因篇幅问题,这里只展示重要的代码,见谅)
这样我们就可以看到onCreate中调用了ButterKnife.Bind方法。我们之前分析了Bind方法的业务逻辑,无非两步
- 查找XXX$$ViewBinder并创建实例
- 调用实例的bind方法
此时我们在看下ButterKnife帮助我们生成的中间类
接下来就是分析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注解的介绍与流程分析
- 首先从功能上来说,AA提供的注解数量远多于ButterKnife,功能也是无所不包(View的绑定,线程,监听,动画,balabala...)而ButterKnife仅仅提供针对View的注解。
- 其次从两类框架的实现流程上来说,AA在一开始就已经生成了新的代码XXXActivity_,后续的执行都是依赖于新的代码。生成的方法和代码量较多。ButterKnife在编译时也是会生成新的中间工具类,代码量相对于AA来说略少,但是新增了类文件。并且,在运行时,需要通过一点点反射的技术来实现整体的逻辑。
- 第三,从上手成都上来说,AA的前期工作略麻烦一些,并且后期需要手动修改类名(XXX的后面加上下划线)ButterKnife则需要在类中添加ButterKnife.Bind方法来使用绑定功能。AA稍微麻烦一丢丢。
好了,ButterKnife的使用介绍,流程分析以及它和AA之间的比较已经写完,如果有什么意见或者不对的地方,请大家指正。
接下来想要给带来的是第三款注解框架Dagger2的使用及流程分析,以及现阶段流行的图片加载资源库的分析,但是,由于老衲智商最近一直没上线→_→,导致Dagger2的环境配了一个多星期还没配好。所以推出时间可能会略晚,希望大家见谅。
更多相关文章
- Android中对于onMeasure()的理解
- Android学习笔记——网络技术
- Android(安卓)Fragment---管理Fragment
- 关于android:configChanges="keyboardHidden|orientation"
- android之两种设置全屏或者无标题的方法
- Android(安卓)Studio更新 Gradle错误解决方法
- Unity3d与Android的相互调用
- android studio快捷键集合
- 关于android WebViewClient 的方法解释