Android 注解的使用 xUtils3和ButterKnife控件的注解注入对比
来源:互联网 发布:中博软件学院 编辑:程序博客网 时间:2024/05/21 19:35
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.lactivity_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<Class<?>>,放置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<String> getSupportedAnnotationTypes() { Set<String> types = new LinkedHashSet<>(); for (Class<? extends Annotation> annotation : getSupportedAnnotations()) { types.add(annotation.getCanonicalName()); } return types; } private Set<Class<? extends Annotation>> getSupportedAnnotations() { Set<Class<? extends Annotation>> 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的Listener return annotations; } @Override public boolean process(Set<? extends TypeElement> elements, RoundEnvironment env) { Map<TypeElement, BindingSet> bindingMap = findAndParseTargets(env); for (Map.Entry<TypeElement, BindingSet> 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<TypeElement, BindingSet> findAndParseTargets(RoundEnvironment env) { Map<TypeElement, BindingSet.Builder> builderMap = new LinkedHashMap<>(); Set<TypeElement> 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<Map.Entry<TypeElement, BindingSet.Builder>> entries = new ArrayDeque<>(builderMap.entrySet()); Map<TypeElement, BindingSet> bindingMap = new LinkedHashMap<>(); while (!entries.isEmpty()) { Map.Entry<TypeElement, BindingSet.Builder> 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<TypeElement, BindingSet.Builder> builderMap, Set<TypeElement> 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<String>,是确定这个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
http://www.trinea.cn/android/java-annotation-android-open-source-analysis/
- Android 注解的使用 xUtils3和ButterKnife控件的注解注入对比
- android注解ButterKnife的使用
- Android ButterKnife 注解框架的使用详解和原理分析
- Android注解框架(ButterKnife的简单使用)
- android library中使用butterknife的注解
- Android Studio中butterknife注解的使用
- Android Butterknife 注解框架的使用
- 注解开源库 butterknife的使用
- ButterKnife 注解框架的使用
- android 超级简单方便的注解注入控件和方法
- 使用ButterKnife插件来生成对各个控件的注解
- ButterKnife的使用以及自定义注解实例化控件
- 关于Android Studio使用butterknife:8.5.1注解是,控件总是为空的问题
- [Android]_[注解库butterknife的基本使用]
- Butterknife 注解框架在 Android Studio 上的使用
- Android中的注解框架——butterknife的使用
- android注解框架:ButterKnife:8.1.0的使用
- Android中的注解式框架之ButterKnife的使用
- B. The Meeting Place Cannot Be Changed
- Tree Cutting POJ
- 零基础学习JAVA编程题—温度转换
- WC2017 && THUWC2017 游记
- 关于“任何软件工程遇到的问题都可以通过增加一个中间层来解决”的明悟
- Android 注解的使用 xUtils3和ButterKnife控件的注解注入对比
- 数组解简单多次递归
- 在LINUX下探测硬件信息的命令
- Rails控制台出现current transaction is aborted错误的解决
- [BZOJ1086][SCOI2005]王室联邦(树上分块)
- 生产者与消费者问题研究
- 深入浅出搜索架构引擎、方案与细节(上)
- 浅谈C++指针
- PHP学习杂记(一)