ButterKnife介绍 功能说明 已经废弃,建议切换至View Binding
功能:使用源码注解+APT生成模版代码,进行Android视图变量和事件绑定。简化代码,提高可读性,编译时注解不会影响APP效率。
视图绑定:成员变量使用@BindView
注解避免调用findViewById
。
资源绑定:字段上使用@BindString、@BindColor、@BindDrawable
等注解,避免资源查找。
事件绑定:方法使用@OnClick、onTextChanged
等注解,避免绑定监听器、创建匿名内部类。
绑定视图数组或列表,批量执行Action行为。ViewCollections.run
运行时反射解析注解赋值,影响性能。(注解使用RUNTIME)
使用编译时注解+APT生成模版代码,运行时调用ButterKnife.bind(...)
注入字段。(注解使用CLASS)
由于编译时处理注解较耗时,调试效率低。因此提供了两种实现,调试阶段可以使用反射,发布阶段使用编译时注解。(注解使用RUNTIME)
项目结构如下:
基本使用 1 2 3 4 5 6 7 8 9 10 11 12 android { compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } } dependencies { implementation 'com.jakewharton:butterknife:10.2.3' annotationProcessor 'com.jakewharton:butterknife-compiler:10.2.3' }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class ButterknifeActivity extends AppCompatActivity { @BindView(R.id.btn1) public Button button1; @Override protected void onCreate (Bundle savedInstanceState) { super .onCreate(savedInstanceState); setContentView(R.layout.activity_butterknife); ButterKnife.bind(this ); button1.setText("I am a button " ); } }
注意事项
bind必须在setContentView之后
父类bind后,子类不需要再bind
Fragment中使用需要传入rootView、onDestroyView中需要unbind
高版本AGP需要使用R2引用资源id
View不能使用private或static修饰,否则编译会报错
工作流程
自定义注解和注解处理器
使用注解
编译时butterknife-gradle-plugin
根据R文件生成R2文件
执行butterknife-compile
APT,解析注解
使用JavaPoet生成XXX_ViewBinding类
运行时调用ButterKnife.bind(...)
。需要使用者手动调用代理类执行,或者通过门面对象,反射找到代理类并执行
反射实例化SimpleActivity_ViewBinding类
在构造方法中完成对Activity的View的绑定。
ButterKnife源码解析 只分析View绑定部分,以Activity为例。(只保留关键代码,先源码太长,可以直接看小结部分)。
注入原理
首先看下我们使用ButterKnife的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public class SimpleActivity extends Activity { @BindView(R.id.hello) Button hello; @OnClick(R.id.hello) void sayHello () { Toast.makeText(this , "Hello, views!" , LENGTH_SHORT).show(); } @Override protected void onCreate (Bundle savedInstanceState) { super .onCreate(savedInstanceState); setContentView(R.layout.simple_activity); ButterKnife.bind(this ); } }
查看ButterKnife.bind(this)
源码,此方法有很多重载方法,区分不同目标类,如Activity、Fragment等
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public final class ButterKnife { @NonNull @UiThread public static Unbinder bind (@NonNull Activity target) { View sourceView = target.getWindow().getDecorView(); return bind(target, sourceView); } @NonNull @UiThread public static Unbinder bind (@NonNull Object target, @NonNull View source) { Class<?> targetClass = target.getClass(); Constructor<? extends Unbinder> constructor = findBindingConstructorForClass(targetClass); return constructor.newInstance(target, source); } }
findBindingConstructorForClass
:查找对应的XXX_ViewBinding
类构造方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 public final class ButterKnife { @Nullable @CheckResult @UiThread private static Constructor<? extends Unbinder> findBindingConstructorForClass(Class<?> cls) { Constructor<? extends Unbinder> bindingCtor = BINDINGS.get(cls); if (bindingCtor != null || BINDINGS.containsKey(cls)) { return bindingCtor; } String clsName = cls.getName(); if (clsName.startsWith("android." ) || clsName.startsWith("java." ) || clsName.startsWith("androidx." )) { return null ; } try { Class<?> bindingClass = cls.getClassLoader().loadClass(clsName + "_ViewBinding" ); bindingCtor = (Constructor<? extends Unbinder>) bindingClass.getConstructor(cls, View.class); } catch (ClassNotFoundException e) { bindingCtor = findBindingConstructorForClass(cls.getSuperclass()); } BINDINGS.put(cls, bindingCtor); return bindingCtor; } }
查看XXX_ViewBinding
源码,位于build/generated/source/apt/
之下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 public class SimpleActivity_ViewBinding implements Unbinder { private SimpleActivity target; private View view7f08001e; @UiThread public SimpleActivity_ViewBinding (SimpleActivity target) { this (target, target.getWindow().getDecorView()); } @UiThread public SimpleActivity_ViewBinding (final SimpleActivity target, View source) { this .target = target; View view; view = Utils.findRequiredView(source, R.id.hello, "field 'hello' and method 'sayHello'" ); target.hello = Utils.castView(view, R.id.hello, "field 'hello'" , Button.class); view7f08001e = view; view.setOnClickListener(new DebouncingOnClickListener() { @Override public void doClick (View p0) { target.sayHello(); } }); } @Override @CallSuper public void unbind () { SimpleActivity target = this .target; if (target == null ) throw new IllegalStateException("Bindings already cleared." ); this .target = null ; target.hello = null ; view7f08001e.setOnClickListener(null ); view7f08001e = null ; } }
小结一下:
调用ButterKnife.bind
拼接类名,使用ClassLoader加载XXX_ViewBinding
类并缓存构造方法。(此处不是缓存ViewBinding实例,而是缓存构造方法,下次进入需要重新创建实例。由于ViewBinding会持有Activity对象,如果缓存实例,会导致无法释放 )
反射创建实例
在构造方法中访问rootView查找View,访问Activity对象给字段赋值,或者添加监听器。并对@OnClick事件绑定做了防抖
返回Unbinder对象,供调用方主动解绑
注解解析步骤 上面解释了依赖注入原理,下面看看XXX_ViewBinding
是如何生成的?
查看 butterknife-compiler
下的ButterKnifeProcessor
类
注解处理器均需要继承AbstractProcessor
,init
方法做初始化工作,getSupportedAnnotationTypes
筛选需要处理的注解,process
方法开始注解处理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 public final class ButterKnifeProcessor extends AbstractProcessor { @Override public synchronized void init (ProcessingEnvironment env) { } @Override public Set<String> getSupportedAnnotationTypes () { Set<String> types = new LinkedHashSet<>(); for (Class<? extends Annotation> annotation : getSupportedAnnotations()) { types.add(annotation.getCanonicalName()); } return types; } @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, debuggable); try { javaFile.writeTo(filer); } catch (IOException e) { error(typeElement, "Unable to write binding for type %s: %s" , typeElement, e.getMessage()); } } return false ; } }
findAndParseTargets
方法:查找并解析目标注解,构建需要生成的类信息,存入Map。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public final class ButterKnifeProcessor extends AbstractProcessor { private Map<TypeElement, BindingSet> findAndParseTargets (RoundEnvironment env) { Map<TypeElement, BindingSet.Builder> builderMap = new LinkedHashMap<>(); Set<TypeElement> erasedTargetNames = new LinkedHashSet<>(); for (Element element : env.getElementsAnnotatedWith(BindView.class)) { try { parseBindView(element, builderMap, erasedTargetNames); } catch (Exception e) { logParsingError(element, BindView.class, e); } } return bindingMap; } }
parseBindView
方法:解析@BindView
注解的元素,存入对应类的BindingSet.Builder
中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 public final class ButterKnifeProcessor extends AbstractProcessor { private void parseBindView (Element element, Map<TypeElement, BindingSet.Builder> builderMap, Set<TypeElement> erasedTargetNames) { TypeElement enclosingElement = (TypeElement) element.getEnclosingElement(); boolean hasError = isInaccessibleViaGeneratedCode(BindView.class, "fields" , element) || isBindingInWrongPackage(BindView.class, element); if (hasError) { return ; } int id = element.getAnnotation(BindView.class).value(); BindingSet.Builder builder = builderMap.get(enclosingElement); Id resourceId = elementToId(element, BindView.class, id); if (builder != null ) { String existingBindingName = builder.findExistingBindingName(resourceId); 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(resourceId, new FieldViewBinding(name, type, required)); erasedTargetNames.add(enclosingElement); } }
BindingSet.newBuilder
方法:构建类基本信息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 final class BindingSet implements BindingInformationProvider { static Builder newBuilder (TypeElement enclosingElement) { TypeMirror typeMirror = enclosingElement.asType(); boolean isView = isSubtypeOfType(typeMirror, VIEW_TYPE); boolean isActivity = isSubtypeOfType(typeMirror, ACTIVITY_TYPE); boolean isDialog = isSubtypeOfType(typeMirror, DIALOG_TYPE); TypeName targetType = TypeName.get(typeMirror); if (targetType instanceof ParameterizedTypeName) { targetType = ((ParameterizedTypeName) targetType).rawType; } ClassName bindingClassName = getBindingClassName(enclosingElement); boolean isFinal = enclosingElement.getModifiers().contains(Modifier.FINAL); return new Builder(targetType, bindingClassName, enclosingElement, isFinal, isView, isActivity, isDialog); } static ClassName getBindingClassName (TypeElement typeElement) { String packageName = getPackage(typeElement).getQualifiedName().toString(); String className = typeElement.getQualifiedName().toString().substring( packageName.length() + 1 ).replace('.' , '$' ); return ClassName.get(packageName, className + "_ViewBinding" ); } }
JavaPoet生成Java文件对象 这一部分简单了解一下即可,挑一部分讲,具体API使用可以看官方文档 。
binding.brewJava
方法:下面代码主要是使用JavaPoet
生成Java文件对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 final class BindingSet implements BindingInformationProvider { JavaFile brewJava (int sdk, boolean debuggable) { TypeSpec bindingConfiguration = createType(sdk, debuggable); return JavaFile.builder(bindingClassName.packageName(), bindingConfiguration) .addFileComment("Generated code from Butter Knife. Do not modify!" ) .build(); } private TypeSpec createType (int sdk, boolean debuggable) { TypeSpec.Builder result = TypeSpec.classBuilder(bindingClassName.simpleName()) .addModifiers(PUBLIC) .addOriginatingElement(enclosingElement); if (isFinal) { result.addModifiers(FINAL); } if (parentBinding != null ) { result.superclass(parentBinding.getBindingClassName()); } else { result.addSuperinterface(UNBINDER); } if (hasTargetField()) { result.addField(targetTypeName, "target" , PRIVATE); } if (isView) { result.addMethod(createBindingConstructorForView()); } else if (isActivity) { result.addMethod(createBindingConstructorForActivity()); } else if (isDialog) { result.addMethod(createBindingConstructorForDialog()); } if (!constructorNeedsView()) { result.addMethod(createBindingViewDelegateConstructor()); } result.addMethod(createBindingConstructor(sdk, debuggable)); if (hasViewBindings() || parentBinding == null ) { result.addMethod(createBindingUnbindMethod(result)); } return result.build(); } }
createBindingConstructor
方法:生成XXX_ViewBinding
类的构造函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 final class BindingSet implements BindingInformationProvider { private MethodSpec createBindingConstructor (int sdk, boolean debuggable) { 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 (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()) { constructor.addStatement("$T view" , VIEW); } for (ViewBinding binding : viewBindings) { addViewBinding(constructor, binding, debuggable); } for (FieldCollectionViewBinding binding : collectionBindings) { constructor.addStatement("$L" , binding.render(debuggable)); } 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(); } }
addViewBinding
方法:添加视图绑定代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 final class BindingSet implements BindingInformationProvider { private void addViewBinding (MethodSpec.Builder result, ViewBinding binding, boolean debuggable) { if (binding.isSingleFieldBinding()) { return ; } List<MemberViewBinding> requiredBindings = binding.getRequiredBindings(); if (!debuggable || requiredBindings.isEmpty()) { result.addStatement("view = source.findViewById($L)" , binding.getId().code); } else if (!binding.isBoundToRoot()) { result.addStatement("view = $T.findRequiredView(source, $L, $S)" , UTILS, binding.getId().code, asHumanDescription(requiredBindings)); } addFieldBinding(result, binding, debuggable); addMethodBindings(result, binding, debuggable); } }
小结一下:
通过继承AbstractProcessor
定义注解处理器,重写getSupportedAnnotationTypes()
方法筛选需要处理的注解,重写process
方法处理注解。
使用javax.lang.model
包下的类来解析Java代码。自定义解析规则,如
找到所有注解过的元素
解析元素:变量类型、变量名,变量修饰符,注解类型,注解值等
检查元素合法性:如是否为private或static、是否继承自View、是否是成员变量等
…
自定义BindingSet
类保存需要生成的ViewBinding
类的信息。
使用JavaPoet
库的API生成Java文件对象。
最后使用Filer
类写入文件。
APT如何找到自定义注解处理器? APT是如何找到自定义的ButterKnifeProcessor
注解处理器并执行的呢?
使用了JavaSPI(Service Provider Interface,服务发现接口)机制。关于JavaSPI机制可以阅读另一篇文章
原理:APT运行的时候加载Processor接口,通过ServiceLoader
读取services文件夹下的服务文件,找到Processor接口的实现类(可以有多个),遍历初始化和执行多个注解处理器。(类似于AndroidManifest
注册组件)
具体介绍和配置可以参考APT介绍和实践
增量注解处理器 注意到ButterKnife
中还依赖了一个incap
的库,并且使用了它的注解@IncrementalAnnotationProcessor(IncrementalAnnotationProcessorType.DYNAMIC)
。
Gradle支持配置增量注解处理器,通过在main
目录下新建resources/META-INF/gradle/incremental.annotation.processors
文件进行配置
这个库实际上就是通过注解+APT自动帮我们生成了配置文件
具体介绍和可以参考APT介绍和实践
Android视图绑定历程 findViewById 原始方式,需要在Activity、Fragment中编写大量重复代码
ButterKnife 已经被宣布废弃 。
通过源码注解+APT方式生成XXX_ViewBinding类,并在onCreate调用ButterKnife.bind(...)
注入字段。
ButterKnife存在问题:高版本AGP(Android Gradle Plugin)生成的R文件不再是常量(模块化中可能和三方库产生id冲突),而编译时注解要求在编译期就确定值。因此在Library模块中会编译失败。
可以添加ButterKnife提供的插件,生成R2资源id解决。
1 2 3 4 5 6 7 8 9 10 buildscript { dependencies { classpath 'com.jakewharton:butterknife-gradle-plugin:10.2.3' } } apply plugin: 'com.android.library' apply plugin: 'com.jakewharton.butterknife'
KAE(Kotlin Android Extensions) 已经被宣布废弃 。
使用方式:build.gradle
添加插件即可apply plugin: 'kotlin-android-extensions'
原理:通过Gradle插件生成findViewById代码,并使用HashMap缓存控件。
反编译成java代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public final class MainActivity extends AppCompatActivity { private HashMap _$_findViewCache; protected void onCreate (@Nullable Bundle savedInstanceState) { super .onCreate(savedInstanceState); this .setContentView(1300023 ); TextView var10000 = (TextView)this ._$_findCachedViewById(id.textView); var10000.setText((CharSequence)"Hello" ); } public View _$_findCachedViewById(int var1) { if (this ._$_findViewCache == null ) { this ._$_findViewCache = new HashMap(); } View var2 = (View)this ._$_findViewCache.get(var1); if (var2 == null ) { var2 = this .findViewById(var1); this ._$_findViewCache.put(var1, var2); } return var2; } }
存在问题:
类型安全:res下的任何id都可以被访问,有可能因访问了非当前Layout下的id而出错,难以利用lint等静态代码校验
空安全:运行时可能出现NPE
兼容性:只能在kotlin中使用,java不友好
局限性:不能跨module使用
RecyclerView.Adapter onBindiViewHolder
中直接使用,会生成findViewById代码,丧失ViewHolder复用优势
ViewBinding 官方文档 。内置Gradle插件,根据layout布局文件生成XXXBinding类。
与findViewById相比:ViewBinding能保证空安全、类型安全 。
与DataBinding相比:ViewBinding更轻量,但不支持布局变量和布局表达式,不支持数据绑定
使用方式如下:
启动ViewBinding功能
1 2 3 4 5 6 android { viewBinding { enabled = true } }
编写layout布局文件,build生成Binding类。如果想忽略布局文件,可以添加属性tools:viewBindingIgnore="true"
代码中使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public class MainFragment extends Fragment { private ResultProfileBinding binding; @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { binding = ResultProfileBinding.inflate(inflater, container, false ); View view = binding.getRoot(); return view; } @Override public void onDestroyView() { super .onDestroyView(); binding = null ; } }
未来? 响应式布局。
最好的视图绑定就是不需要findViewById