butterknife-源码解析

    技术2022-07-11  82


    title: butterKnife源码解析 date: 2020-06-30 09:43:23 tags: [源码笔记] typora-copy-images-to: ./imgs typora-root-url: ./imgs


    使用

    使用就很简单了.导包.然后来个demo

    private static final String TAG = "MainActivity"; private ViewGroup.LayoutParams layoutParams; @BindView(R.id.tip) TextView view; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); ButterKnife.bind(this); } @OnClick(R.id.tip) void onclick(View view ){ Toast.makeText(this, "uytut", Toast.LENGTH_SHORT).show(); }

    原理

    它主要是通过注解主动生成了一个辅助类,在辅助类中把Id和控件进行绑定。辅助类的后缀是_ViewBinding, 同时,可以看到.辅助类和我们的类是相同路径的.

    辅助类代码

    所有的辅助类都是继承自unbinder. 他的unbind解绑操作必须要我们调用.否则会内存泄漏

    public class MainActivity_ViewBinding implements Unbinder { private MainActivity target; private View view7f0700b7; @UiThread 构造函数.执行绑定过程 public MainActivity_ViewBinding(MainActivity target) { //可以看到.这里拿到了 target的顶层view this(target, target.getWindow().getDecorView()); } @UiThread public MainActivity_ViewBinding(final MainActivity target, View source) { this.target = target; View view; //这里是的decorview 的findviewById.找到 id对应的view. 这个R.id.tip 就是我们之前@BindView(R.id.tip)的参数. view = Utils.findRequiredView(source, R.id.tip, "field 'view' and method 'onclick'"); //把这个view 转换成TextView. 然后赋值给targetActivity 的view target.view = Utils.castView(view, R.id.tip, "field 'view'", TextView.class); view7f0700b7 = view; 设置点击事件.绑定 view的点击和 activity里的 onclick方法.这个方法可以随便命名. 同时这里巧妙的设置了放置瞬间多次点击的问题. view.setOnClickListener(new DebouncingOnClickListener() { @Override public void doClick(View p0) { target.onclick(p0); } }); } @Override @CallSuper 这个类里边因为绑定了activity和view. 需要主动释放.否则造成内存泄露 public void unbind() { MainActivity target = this.target; if (target == null) throw new IllegalStateException("Bindings already cleared."); this.target = null; target.view = null; view7f0700b7.setOnClickListener(null); view7f0700b7 = null; } }

    总结

    这个辅助类.完成了activity和里边view 的绑定方法.并实现了点击的处理. 其实就是省略的我们手写的findviewById. 而butterknife 里主要的就是如何生成这个辅助类.和对不同view的类似的处理.所以接下来我们主要看如何生成辅助类

    ButterKinfe.bind过程

    源码追踪,.有删减

    找到docerview, decorview是每个activity的顶层view public static Unbinder bind(@NonNull Activity target) { View sourceView = target.getWindow().getDecorView(); return bind(target, sourceView); } public static Unbinder bind(@NonNull Object target, @NonNull View source) { 拿到target的class,因为这时候已经生成了辅助类.而辅助类和target类是在同样的路径下.只是比target类名称多了_ViewBinding 所以这里利用target的class来找到辅助类.然后调用构造函数初始化 Class<?> targetClass = target.getClass(); Constructor<? extends Unbinder> constructor = findBindingConstructorForClass(targetClass); try { return constructor.newInstance(target, source); } catch (IllegalAccessException e) { throw new RuntimeException("Unable to invoke " + constructor, e); } }

    继续看,如何找到辅助类

    private static Constructor<? extends Unbinder> findBindingConstructorForClass(Class<?> cls) { 先从LinkedHashMap缓存中查找 Constructor<? extends Unbinder> bindingCtor = BINDINGS.get(cls); String clsName = cls.getName(); 略过系统类. if (clsName.startsWith("android.") || clsName.startsWith("java.") || clsName.startsWith("androidx.")) { if (debug) Log.d(TAG, "MISS: Reached framework class. Abandoning search."); return null; } try { 可以看到.字符串拼接后.通过class名称找到class .然后就是得到构造函数.加入map缓存. 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; }

    总结

    这里也很明了. 通过target的class.找到生成的辅助类.然后反射创建辅助类.执行构造函数. 在构造函数中执行绑定.

    生成辅助类

    这里是在编译的时候通过编译时期-注解处理器.生成的辅助类. 这个代码在butterknife的源码里.

    先来个注解处理器的文章

    https://www.cnblogs.com/ganchuanpu/p/9020478.html

    https://blog.csdn.net/xiayong1/article/details/85854725

    主要是根据定义的注解. 拿到注解对应的参数.然后再把这些参数进行处理. 对于butterkinfe就是生成辅助类.

    public class MyProcessor extends AbstractProcessor { //用来指定你使用的 java 版本。通常你应该返回 SourceVersion.latestSupported() @Override public SourceVersion getSupportedSourceVersion() { return SourceVersion.latestSupported(); } //会被处理器调用,可以在这里获取Filer,Elements,Messager等辅助类,后面会解释 @Override public synchronized void init(ProcessingEnvironment processingEnv) { super.init(processingEnv); } //这个方法返回stirng类型的set集合,集合里包含了你需要处理的注解 @Override public Set<String> getSupportedAnnotationTypes() { Set<String> annotataions = new LinkedHashSet<String>(); annotataions.add("com.example.MyAnnotation"); return annotataions; } //核心方法,这个一般的流程就是先扫描查找注解,再生成 java 文件 //这2个步骤设计的知识点细节很多。 @Override public boolean process(Set<? extends TypeElement> annoations, RoundEnvironment env) { return false; } }

    注解的原理.找一张 网上的图

    这里 我们注意.主要工作就是用注解处理器.根据注解.生成辅助类.且辅助类和target在同样的路径中.这里有手动生成java文件的操作.

    主要代码在ButterKnifeProcessor 类中.在 butterkinfe-compiler 模块中.

    Annotation processing是在编译阶段执行的,它的原理就是读入Java源代码,解析注解,然后生成新的Java代码。新生成的Java代码最后被编译成Java字节码,注解解析器(Annotation Processor)不能改变读入的Java 类,比如不能加入或删除Java方法。注解处理器运行在它自己的 JVM 中

    下面看源码

    支持的注解

    这是他支持的所有注解类型.其实一目了然.

    private Set<Class<? extends Annotation>> getSupportedAnnotations() { Set<Class<? extends Annotation>> annotations = new LinkedHashSet<>(); annotations.add(BindAnim.class); 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(BindFont.class); annotations.add(BindInt.class); annotations.add(BindString.class); annotations.add(BindView.class); annotations.add(BindViews.class); annotations.addAll(LISTENERS); return annotations; }

    处理过程

    @Override public boolean process(Set<? extends TypeElement> elements, RoundEnvironment env) { 得到 BindingSet 集合.这里是对各种注解的处理. bindingSet 可以理解为是一个java 文件的抽象.他里边的各种属性最后可以生成一个完整的java文件.这是butterknife自己定义的. Map<TypeElement, BindingSet> bindingMap = findAndParseTargets(env); for (Map.Entry<TypeElement, BindingSet> entry : bindingMap.entrySet()) { TypeElement typeElement = entry.getKey(); BindingSet binding = entry.getValue(); //这里已经生成了java文件,这个java文件就是上文的辅助类. JavaFile javaFile = binding.brewJava(sdk, debuggable); try { 把java文件由内存中写到磁盘上 javaFile.writeTo(filer); } catch (IOException e) { } } return false; }

    接下来看如何把有注解得到的数据封装成辅助类.他这里处理了各种注解.我们先只看BindView

    private Map<TypeElement, BindingSet> findAndParseTargets(RoundEnvironment env) { Map<TypeElement, BindingSet.Builder> builderMap = new LinkedHashMap<>(); Set<TypeElement> erasedTargetNames = new LinkedHashSet<>(); 这里是拿到所有 bindView 的注解.然后遍历处理. for (Element element : env.getElementsAnnotatedWith(BindView.class)) { try { parseBindView(element, builderMap, erasedTargetNames); } catch (Exception e) { logParsingError(element, BindView.class, e); } } 对lister的处理. 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, classpathBindings.keySet()); if (parentType == null) { bindingMap.put(type, builder.build()); } else { BindingInformationProvider parentBinding = bindingMap.get(parentType); if (parentBinding == null) { parentBinding = classpathBindings.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; }

    这里总共看三步. 处理bindview的注解.生成对应bindingset. 处理listener. 处理父类绑定.

    解析bindView注解

    他遍历的Element 代表了源文件中的每一部分特定类型.介绍如下

    public class ClassA { // TypeElement private int var_0; // VariableElement public ClassA() {} // ExecuteableElement public void setA( // ExecuteableElement int newA // TypeElement ) { } }

    入口处在 ButterKnifeProcessor.parseBindView

    private void parseBindView(Element element, Map<TypeElement, BindingSet.Builder> builderMap, Set<TypeElement> erasedTargetNames) { 这里就是注解类型 TypeElement enclosingElement = (TypeElement) element.getEnclosingElement(); TypeMirror elementType = element.asType(); 注解的名称和全限定名 Name qualifiedName = enclosingElement.getQualifiedName(); Name simpleName = element.getSimpleName(); // 拿到注解对应的id值. 就是我们写的BindView(R.id.tip) 中的R.id.tip int id = element.getAnnotation(BindView.class).value(); BindingSet.Builder builder = builderMap.get(enclosingElement); 封装id并缓存 Id resourceId = elementToId(element, BindView.class, id); 这里创建了新的 BindingSet .为了封装 bindview 的相关参数. if (builder != null) { String existingBindingName = builder.findExistingBindingName(resourceId); } else { builder = getOrCreateBindingBuilder(builderMap, enclosingElement); } 到这.就给辅助类生成了这个 bindview注解先关的属性.名称.类型.然后添加到builder中.最后统一创建. String name = simpleName.toString(); TypeName type = TypeName.get(elementType); boolean required = isFieldRequired(element); 我猜其实就是生成 private View view7f0700b7; 这种类型的格式.当然还绑定了对应的id. builder.addField(resourceId, new FieldViewBinding(name, type, required)); // Add the type-erased version to the valid binding targets set. erasedTargetNames.add(enclosingElement); }

    所以这方法大概就是,通过annotation的类型.然后取出注解的值. 最后经过封装.创建这个属性的builder.与id进行绑定.这里主要是根据注解创建类结构.看不太懂也没事.

    listener的处理

    继续回到ButterKnifeProcessor 的findAndParseTargets中对点击事件的处理

    // Process each annotation that corresponds to a listener. for (Class<? extends Annotation> listener : LISTENERS) { findAndParseListener(env, listener, builderMap, erasedTargetNames); }

    接着看 findAndParseListener 又跳到parseListenerAnnotation.

    private void parseListenerAnnotation(Class<? extends Annotation> annotationClass, Element element, Map<TypeElement, BindingSet.Builder> builderMap, Set<TypeElement> erasedTargetNames) { 先拿到注解的类型. ExecutableElement executableElement = (ExecutableElement) element; TypeElement enclosingElement = (TypeElement) element.getEnclosingElement(); 在拿到注解中的值.也就是绑定点击的id数组. @OnClick({R.id.hello,R.id.hello2}) 这个 int[] ids = (int[]) annotationValue.invoke(annotation); String name = executableElement.getSimpleName().toString(); boolean required = isListenerRequired(executableElement); 这里我也看不太懂. 应该是监听的注解. 这里我们要知道.butterknife会默认创建一个listener.并在click中执行view的回调 ListenerClass listener = annotationClass.getAnnotation(ListenerClass.class); ListenerMethod method; ListenerMethod[] methods = listener.method(); callback 的注解. 没有listener的就会走callback 的回调 Method annotationCallback = annotationClass.getDeclaredMethod("callback"); Enum<?> callback = (Enum<?>) annotationCallback.invoke(annotation); Field callbackField = callback.getDeclaringClass().getField(callback.name()); method = callbackField.getAnnotation(ListenerMethod.class); 拿到所有参数 List<? extends VariableElement> methodParameters = executableElement.getParameters(); 返回值类型 TypeMirror returnType = executableElement.getReturnType(); 最后,把上边的参数进行封装.等着以后生成listener的辅助类. MethodViewBinding binding = new MethodViewBinding(name, Arrays.asList(parameters), required, hasReturnValue); BindingSet.Builder builder = getOrCreateBindingBuilder(builderMap, enclosingElement); Map<Integer, Id> resourceIds = elementToIds(element, annotationClass, ids);

    看一下生成listener相关的注解,应该就是构建这个类似的结构.

    @ListenerClass( targetType = "android.widget.AdapterView<?>", setter = "setOnItemClickListener", type = "android.widget.AdapterView.OnItemClickListener", method = @ListenerMethod( name = "onItemClick", parameters = { "android.widget.AdapterView<?>", "android.view.View", "int", "long" } ) )

    最后就是那这些封装数据.生成一个java类. 那个不看了.不好理解.

    本文由博客群发一文多发等运营工具平台 OpenWrite 发布

    Processed: 0.010, SQL: 9