Android 注解的使用 xUtils3和ButterKnife控件的注解注入对比

Java注解的定义:
java注解(Annotation),是JDK1.5开始加入的源代码的一种特殊语法元信息。可以用于标注Java语言中的类、方法、变量、参数和包,然后在编译或运行时进行解析和使用,起到说明,配置的功能。注解的功能位于java.lang.annotation包中。


JDK里常见的有@Override、@Deprecated、@SuppressWarnings。
我刚开始对注解的认识,更多来自于以前做的Java Web开发,比如Spring的注解,@Controller、@ Service、@ Repository,Spring是利用这些注解达到标记的效果,从而实现IOC的功能,把本应该代码new出来的实体交给Spring容器来管理。


想要实现(定义)一个注解,就需要用到元注解了。比如Override,它的代码是这样的,上面两个就是元注解

@Target(ElementType.METHOD) @Retention(RetentionPolicy.SOURCE) public @interface Override { }


元注解共有4种, @Retention、@Target、@Document、@Inherited四种。

@Document:表示带有这个注解的元素会被javadoc工具记录,不常用。

@Inherited:表示被注解的类,继承它的子类会自动继承此注解,一般常用。

@Target:用来确定注解的作用目标,包括以下几种
@Target(ElementType.TYPE)//接口、类、枚举、注解
@Target(ElementType.FIELD) //字段、枚举的常量
@Target(ElementType.METHOD) //方法
@Target(ElementType.PARAMETER) //方法参数
@Target(ElementType.CONSTRUCTOR)//构造函数
@Target(ElementType.LOCAL_VARIABLE)//局部变量
@Target(ElementType.ANNOTATION_TYPE)//注解
@Target(ElementType.PACKAGE) ///包

@Retention:定义注解的保留策略。分为如下几种
@Retention(RetentionPolicy.SOURCE)//注解仅存在于源码中,在class字节码文件中不包含
@Retention(RetentionPolicy.CLASS)// 默认的保留策略,注解会在class字节码文件中存在,但运行时无法获得,
@Retention(RetentionPolicy.RUNTIME)// 注解会在class字节码文件中存在,在运行时可以通过反射获取到


----------------------------------------------------分割线----------------------------------------------------

下面来对比一下两个著名的框架,xUtils3和ButterKnife它们两个对于控件所使用的注解的不同。先上两个控件注解的源码,
这是xUtils3的
@Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) public @interface ViewInject {int value(); /* parent view id */ int parentId() default 0; }

这是ButterKnife的,版本为8.5.1
@Retention(CLASS) @Target(FIELD) public @interface BindView { /** View ID to which the field will be bound. */ @IdRes int value(); }

相同点是@Target的值都是FIELD,表示这是一个类属性注解。

然后最大的不同点就是@Retention了,一个是@Retention(RetentionPolicy.RUNTIME),一个是@Retention(CLASS),代码省略了,实际上就是@Retention(RetentionPolicy.CLASS)。


xUtils3实现UI注解的原理
代码写法
它的写法是这样的,在BaseActivity里面有句注入代码
public class BaseActivity extends AppCompatActivity {@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); x.view().inject(this); } }

然后在任一个子类Activity里这么写。意思是这个Activity使用R.layout.activity_main这个布局,然后对于mViewPager这个控件类的成员变量,注入id为R.id.container的控件。
@ContentView(R.layout.activity_main) public class MainActivity extends BaseActivity {@ViewInject(R.id.container) private ViewPager mViewPager; @ViewInject(R.id.toolbar) private Toolbar toolbar; ......






代码实现
整个过程开始的地方在x.view().inject(this);
x相当于一个工具类,里面有一个内部类Ext,用来管理xUtils里4大模块的Manager,所以x是Manager中的Manager。
x.view()作用是使用懒汉模式获取(或创建)一个ViewInjectorImpl的单例实体。人如其名,其中ViewInjectorImpl继承了ViewInjector。
下面看看ViewInjectorImpl的inject方法的处理过程。

@Override public void inject(Activity activity) { //获取Activity的ContentView的注解 //获取Activity的Class类型 Class handlerType = activity.getClass(); try { //根据类名,获取ContentView这个注解 ContentView contentView = findContentView(handlerType); if (contentView != null) { //获取注解里value的值,也就是@ContentView(R.layout.activity_main)括号里这个int型资源ID int viewId = contentView.value(); if (viewId > 0) { //利用反射获取Activity的setContentView这个方法,通过Method.invoke最终完成setContentView(布局id)这句代码 Method setContentViewMethod = handlerType.getMethod("setContentView", int.class); setContentViewMethod.invoke(activity, viewId); } } } catch (Throwable ex) { LogUtil.e(ex.getMessage(), ex); }//用来给控件成员注入的方法 injectObject(activity, handlerType, new ViewFinder(activity)); }private static ContentView findContentView(Class thisCls) { //IGNORED是一个HashSet>,放置Object、Activity、Fragment以后supportV4包的Activity、Fragment //由于这是一个递归调用,会一直往父类去查询,所以当到了这几个类时候则结束递归。 if (thisCls == null || IGNORED.contains(thisCls)) { return null; } //利用反射从Class实体中尝试获取ContentView这个注解,找不到就递归往父类去查找,找到就返回 ContentView contentView = thisCls.getAnnotation(ContentView.class); if (contentView == null) { return findContentView(thisCls.getSuperclass()); } return contentView; }@SuppressWarnings("ConstantConditions") //handler即是XXActivity类,handlerType是这个类的Class,这个ViewFinder里面是一个Activity或View,主要是对findViewById代码的封装 private static void injectObject(Object handler, Class handlerType, ViewFinder finder) {//IGNORED过滤,同上 if (handlerType == null || IGNORED.contains(handlerType)) { return; }// 从父类到子类递归 injectObject(handler, handlerType.getSuperclass(), finder); // inject view 获取所有的类变量 Field[] fields = handlerType.getDeclaredFields(); if (fields != null && fields.length > 0) { for (Field field : fields) {Class fieldType = field.getType(); if ( /* 不注入静态字段 */Modifier.isStatic(field.getModifiers()) || /* 不注入final字段 */Modifier.isFinal(field.getModifiers()) || /* 不注入基本类型字段 */fieldType.isPrimitive() || /* 不注入数组类型字段 */fieldType.isArray()) { continue; }//尝试获取变量ViewInject这个注解(可能为null) ViewInject viewInject = field.getAnnotation(ViewInject.class); if (viewInject != null) { try { //ViewInject 可以在括号里填的两个值,一个是控件id,一个是控件的父控件id(不填默认值为0) //这就是实现我们一般在Activity内写的findViewById方法。 View view = finder.findViewById(viewInject.value(), viewInject.parentId()); if (view != null) { //这句话等于打开一个权限,从而可以使我们对private控件赋值。详情看下面setAccessible说明 field.setAccessible(true); field.set(handler, view); } else { throw new RuntimeException("Invalid @ViewInject for " + handlerType.getSimpleName() + "." + field.getName()); } } catch (Throwable ex) { LogUtil.e(ex.getMessage(), ex); } } } } // end inject view// inject event 关于事件比如点击事件的注解,代码略......}


setAccessible说明,这里有篇文章有介绍AccessibleObject revisited: a study in immutability里面有段话
The setAccessible method allows you to bypass the access control semantics of the Java language. By calling setAccessible on the Method object for a private method, you can call that method from outside the class it is defined in, using Method.invoke. By calling setAccessible on the Field object for a private field, you can read or write that field from any other class. As of Tiger, you can even modify a final field in this way.
大概意思是使用了setAccessible方法可以在类外面调用类的私有方法,读写类的私有属性,甚至可以修改一个final关键字修饰的变量。


所以小总结一下,xUtils里面的控件注解,就是在程序运行时,利用反射获取当前Activity类,反射获取并遍历所有类属性,挑出有@ViewInject注解的属性,获取里面的value值(即控件id),执行我们熟悉的findViewById代码得到View,最后把这个View用反射注入回到这个属性里。

ButterKnife实现UI注解的原理
关于编译时注解,有一篇博客讲解的很好。自定义注解之编译时注解(RetentionPolicy.CLASS)(一)
适合没接触过这个知识点的同学看。


这里拿ButterKnife最常用的BindView注解讲讲,写个简单demo

public class TestActivity extends AppCompatActivity {@BindView(R.id.tvContent) TextView tvContent; @BindView(R.id.btnOk) Button btnOk; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_test); ButterKnife.bind(this); } }




编译时注解需要继承抽象类AbstractProcessor,所以搜索框架源码找到了ButterKnifeProcessor.java
@AutoService(Processor.class) public final class ButterKnifeProcessor extends AbstractProcessor {@Override public Set getSupportedAnnotationTypes() { Set types = new LinkedHashSet<>(); for (Class annotation : getSupportedAnnotations()) { types.add(annotation.getCanonicalName()); } return types; }private Set> getSupportedAnnotations() { Set> annotations = new LinkedHashSet<>(); annotations.add(BindArray.class); annotations.add(BindBitmap.class); annotations.add(BindBool.class); annotations.add(BindColor.class); annotations.add(BindDimen.class); annotations.add(BindDrawable.class); annotations.add(BindFloat.class); annotations.add(BindInt.class); annotations.add(BindString.class); annotations.add(BindView.class); annotations.add(BindViews.class); annotations.addAll(LISTENERS); //一个常量List,包括onClick,onItemClick,onLongClick等所有Android的Listenerreturn annotations; }@Override public boolean process(Set elements, RoundEnvironment env) { Map bindingMap = findAndParseTargets(env); for (Map.Entry entry : bindingMap.entrySet()) { TypeElement typeElement = entry.getKey(); BindingSet binding = entry.getValue(); JavaFile javaFile = binding.brewJava(sdk); try { javaFile.writeTo(filer); } catch (IOException e) { error(typeElement, "Unable to write binding for type %s: %s", typeElement, e.getMessage()); } }return false; }private Map findAndParseTargets(RoundEnvironment env) { Map builderMap = new LinkedHashMap<>(); Set erasedTargetNames = new LinkedHashSet<>(); scanForRClasses(env); // Process each @BindArray element. // Process each @BindBitmap element. // Process each @BindBool element. // Process each @BindColor element. // Process each @BindDimen element. // Process each @BindDrawable element. // Process each @BindFloat element. // Process each @BindInt element. // Process each @BindString element.// Process each @BindView element. for (Element element : env.getElementsAnnotatedWith(BindView.class)) { // we don't SuperficialValidation.validateElement(element) // so that an unresolved View type can be generated by later processing rounds try { parseBindView(element, builderMap, erasedTargetNames); } catch (Exception e) { logParsingError(element, BindView.class, e); } }// Process each @BindViews element. // Process each annotation that corresponds to a listener. for (Class listener : LISTENERS) { findAndParseListener(env, listener, builderMap, erasedTargetNames); }// Associate superclass binders with their subclass binders. This is a queue-based tree walk // which starts at the roots (superclasses) and walks to the leafs (subclasses). Deque> entries = new ArrayDeque<>(builderMap.entrySet()); Map bindingMap = new LinkedHashMap<>(); while (!entries.isEmpty()) { Map.Entry 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); } } }return bindingMap; }private void parseBindView(Element element, Map builderMap, Set erasedTargetNames) { TypeElement enclosingElement = (TypeElement) element.getEnclosingElement(); // Start by verifying common generated code restrictions. boolean hasError = isInaccessibleViaGeneratedCode(BindView.class, "fields", element) || isBindingInWrongPackage(BindView.class, element); // Verify that the target type extends from View. TypeMirror elementType = element.asType(); if (elementType.getKind() == TypeKind.TYPEVAR) { TypeVariable typeVariable = (TypeVariable) elementType; elementType = typeVariable.getUpperBound(); } Name qualifiedName = enclosingElement.getQualifiedName(); Name simpleName = element.getSimpleName(); if (!isSubtypeOfType(elementType, VIEW_TYPE) && !isInterface(elementType)) { if (elementType.getKind() == TypeKind.ERROR) { note(element, "@%s field with unresolved type (%s) " + "must elsewhere be generated as a View or interface. (%s.%s)", BindView.class.getSimpleName(), elementType, qualifiedName, simpleName); } else { error(element, "@%s fields must extend from View or be an interface. (%s.%s)", BindView.class.getSimpleName(), qualifiedName, simpleName); hasError = true; } }if (hasError) { return; }// Assemble information on the field. int id = element.getAnnotation(BindView.class).value(); BindingSet.Builder builder = builderMap.get(enclosingElement); QualifiedId qualifiedId = elementToQualifiedId(element, id); if (builder != null) { String existingBindingName = builder.findExistingBindingName(getId(qualifiedId)); if (existingBindingName != null) { error(element, "Attempt to use @%s for an already bound ID %d on '%s'. (%s.%s)", BindView.class.getSimpleName(), id, existingBindingName, enclosingElement.getQualifiedName(), element.getSimpleName()); return; } } else { builder = getOrCreateBindingBuilder(builderMap, enclosingElement); }String name = simpleName.toString(); TypeName type = TypeName.get(elementType); boolean required = isFieldRequired(element); builder.addField(getId(qualifiedId), new FieldViewBinding(name, type, required)); // Add the type-erased version to the valid binding targets set. erasedTargetNames.add(enclosingElement); }}






·类注解@AutoService是google公司开发的注解处理器,能方便实现自定义注解,ButterKnife使用了这给力的功能。

getSupportedAnnotationTypes()方法返回值Set,是确定这个Processor可以处理哪些注解的。可以看到下面的私有方法getSupportedAnnotations()里的class类型就是ButterKnife里面用到的注解。

process()在AbstractProcessor是唯一一个Abstract类型的方法,我们主要写的代码就是写这个方法体。方法很重要,分为以下几步
1.
第一行代码findAndParseTargets()就是扫描所有文件,找出类里面被ButterKnife里面注解的那些变量,可以看到有@BindBitmap、@BindInt、@BindDimen等一大堆。
里面有用到一个很重要的方法env.getElementsAnnotatedWith(BindView.class),意思是返回使用给定注释类型注释的元素。很拗口,在这里其实当前意思是返回用BindView注解的Field这个Element实体。
然后交由parseBindView方法处理,方法里有句代码int id = element.getAnnotation(BindView.class).value(); 是获取到写在BindView注解里面的一个资源int类型id。
之后就是用一个BindingSet来封装这个属性,资源id等重要信息。
2.
下一步看代码名字就知道了,用BindingSet的信息来生成一个JavaFile对象。人如其名,这个类主要是确定一个.java文件有哪些代码。这部分代码square公司写的很长很精妙,JavaFile这个类还不是属于ButterKnife项目的,是square公司的另一个项目javapoet,这个项目是专门用来生成.java源码文件的。
这里找出BindingSet的3个跟这个demo相关的方法。第一个方法,是生成一个属于Activity_ViewBinding的构造函数。第二个方法最重要,生成实现注解注入的构造函数。第三个方法,生成unbind函数。

private MethodSpec createBindingConstructorForActivity() { MethodSpec.Builder builder = MethodSpec.constructorBuilder() .addAnnotation(UI_THREAD) .addModifiers(PUBLIC) .addParameter(targetTypeName, "target"); if (constructorNeedsView()) { builder.addStatement("this(target, target.getWindow().getDecorView())"); } else { builder.addStatement("this(target, target)"); } return builder.build(); }private MethodSpec createBindingConstructor(int sdk) { MethodSpec.Builder constructor = MethodSpec.constructorBuilder() .addAnnotation(UI_THREAD) .addModifiers(PUBLIC); if (hasMethodBindings()) { constructor.addParameter(targetTypeName, "target", FINAL); } else { constructor.addParameter(targetTypeName, "target"); }if (constructorNeedsView()) { constructor.addParameter(VIEW, "source"); } else { constructor.addParameter(CONTEXT, "context"); }if (hasUnqualifiedResourceBindings()) { // Aapt can change IDs out from underneath us, just suppress since all will work at runtime. constructor.addAnnotation(AnnotationSpec.builder(SuppressWarnings.class) .addMember("value", "$S", "ResourceType") .build()); }if (hasOnTouchMethodBindings()) { constructor.addAnnotation(AnnotationSpec.builder(SUPPRESS_LINT) .addMember("value", "$S", "ClickableViewAccessibility") .build()); }if (parentBinding != null) { if (parentBinding.constructorNeedsView()) { constructor.addStatement("super(target, source)"); } else if (constructorNeedsView()) { constructor.addStatement("super(target, source.getContext())"); } else { constructor.addStatement("super(target, context)"); } constructor.addCode("\n"); } if (hasTargetField()) { constructor.addStatement("this.target = target"); constructor.addCode("\n"); }if (hasViewBindings()) { if (hasViewLocal()) { // Local variable in which all views will be temporarily stored. constructor.addStatement("$T view", VIEW); } for (ViewBinding binding : viewBindings) { addViewBinding(constructor, binding); } for (FieldCollectionViewBinding binding : collectionBindings) { constructor.addStatement("$L", binding.render()); }if (!resourceBindings.isEmpty()) { constructor.addCode("\n"); } }if (!resourceBindings.isEmpty()) { if (constructorNeedsView()) { constructor.addStatement("$T context = source.getContext()", CONTEXT); } if (hasResourceBindingsNeedingResource(sdk)) { constructor.addStatement("$T res = context.getResources()", RESOURCES); } for (ResourceBinding binding : resourceBindings) { constructor.addStatement("$L", binding.render(sdk)); } }return constructor.build(); }private MethodSpec createBindingUnbindMethod(TypeSpec.Builder bindingClass) { MethodSpec.Builder result = MethodSpec.methodBuilder("unbind") .addAnnotation(Override.class) .addModifiers(PUBLIC); if (!isFinal && parentBinding == null) { result.addAnnotation(CALL_SUPER); }if (hasTargetField()) { if (hasFieldBindings()) { result.addStatement("$T target = this.target", targetTypeName); } result.addStatement("if (target == null) throw new $T($S)", IllegalStateException.class, "Bindings already cleared."); result.addStatement("$N = null", hasFieldBindings() ? "this.target" : "target"); result.addCode("\n"); for (ViewBinding binding : viewBindings) { if (binding.getFieldBinding() != null) { result.addStatement("target.$L = null", binding.getFieldBinding().getName()); } } for (FieldCollectionViewBinding binding : collectionBindings) { result.addStatement("target.$L = null", binding.name); } }if (hasMethodBindings()) { result.addCode("\n"); for (ViewBinding binding : viewBindings) { addFieldAndUnbindStatement(bindingClass, result, binding); } }if (parentBinding != null) { result.addCode("\n"); result.addStatement("super.unbind()"); } return result.build(); }

3.然后调用javaFile.writeTo(filer); 最终生成出来一个.java文件。

这段代码最终会生成什么,项目打包后用jadx反编译来看看。

import android.support.annotation.CallSuper; import android.support.annotation.UiThread; import android.view.View; import android.widget.Button; import android.widget.TextView; import butterknife.Unbinder; import butterknife.internal.Utils; public class TestActivity_ViewBinding implements Unbinder { private TestActivity target; @UiThread public TestActivity_ViewBinding(TestActivity target) { this(target, target.getWindow().getDecorView()); }@UiThread public TestActivity_ViewBinding(TestActivity target, View source) { this.target = target; target.tvContent = (TextView) Utils.findRequiredViewAsType(source, R.id.tvContent, "field 'tvContent'", TextView.class); target.btnOk = (Button) Utils.findRequiredViewAsType(source, R.id.btnOk, "field 'btnOk'", Button.class); }@CallSuper public void unbind() { TestActivity target = this.target; if (target == null) { throw new IllegalStateException("Bindings already cleared."); } this.target = null; target.tvContent = null; target.btnOk = null; } }

可以看到ButterKnife给我们的TestActivity生成了一个TestActivity_ViewBinding类,实现Unbinder接口。里面的第二个构造函数就是我们BindView注解会生成的业务代码。里面两句Utils.findRequiredViewAsType赋值给控件的代码,我想你已经猜到啥意思了。不深追了。


那么这个TestActivity_ViewBinding是怎么用,什么时候用的?
回到我们TestActivity的onCreate()方法,我们看到了这句代码ButterKnife.bind(this); 所以我们进入ButterKnife.java看看

@NonNull @UiThread public static Unbinder bind(@NonNull Activity target) { View sourceView = target.getWindow().getDecorView(); return createBinding(target, sourceView); }private static Unbinder createBinding(@NonNull Object target, @NonNull View source) { Class targetClass = target.getClass(); if (debug) Log.d(TAG, "Looking up binding for " + targetClass.getName()); Constructor constructor = findBindingConstructorForClass(targetClass); if (constructor == null) { return Unbinder.EMPTY; }//noinspection TryWithIdenticalCatches Resolves to API 19+ only type. try { 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); } }@Nullable @CheckResult @UiThread private static Constructor findBindingConstructorForClass(Class cls) { Constructor bindingCtor = BINDINGS.get(cls); if (bindingCtor != null) { if (debug) Log.d(TAG, "HIT: Cached in binding map."); return bindingCtor; } 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 { Class bindingClass = Class.forName(clsName + "_ViewBinding"); //noinspection unchecked bindingCtor = (Constructor) bindingClass.getConstructor(cls, View.class); if (debug) Log.d(TAG, "HIT: Loaded binding class and constructor."); } 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; }


bind重载了很多方法,针对Activity、Dialog、Fragment、View的。都是调用了createBinding()方法。
createBinding()方法中通过findBindingConstructorForClass方法传入TestActivity.class获得一个构造函数。
什么构造函数呢?在findBindingConstructorForClass可以看到通过Class.forName(clsName + "_ViewBinding")字符串拼接的方法,最终能获得我们的TestActivity_ViewBinding类。
然后调用bindingClass.getConstructor(cls, View.class); 获取参数列表第一个是当前类,第二个是View类型的构造函数,也就是上面所说的核心业务构造函数啦。
最后在createBinding方法中,调用constructor.newInstance(target, source)调用起这个构造函数。
截止到这就完成我们所有findViewById代码的功能了。

小总结一下,ButterKnife里面的控件注解,会在程序编译(生成apk)时,生成XXX_ViewBinding类,里面的(构造)方法会有一些findViewById的代码。然后也会用反射调用到这个类的构造函数。



两个注入框架的对比结论
总结就是xUtils3是通过运行时注解利用反射,破解私有属性等手段实现控件的注入,ButterKnife是通过编译时注解,生成一个附属类代码,然后在这些代码插入到我们代码中执行。所以程序在运行时,ButterKnife会比xUtils更快。


//TODO
做个对比demo测试一下两个库的速度。



参考文章:
http://blog.csdn.net/yixiaogang109/article/details/7328466
【Android 注解的使用 xUtils3和ButterKnife控件的注解注入对比】http://www.trinea.cn/android/java-annotation-android-open-source-analysis/

    推荐阅读