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