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 extends Annotation> 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 extends TypeElement> 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 extends Annotation> 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 extends Unbinder> 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 extends Unbinder> findBindingConstructorForClass(Class> cls) {
Constructor extends Unbinder> 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 extends Unbinder>) 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/