美文网首页
ButterKnife原理

ButterKnife原理

作者: 小O机 | 来源:发表于2020-11-18 16:09 被阅读0次

    参考:https://www.jianshu.com/p/39fc66aa3297
    github:https://github.com/JakeWharton/butterknife

    一、使用方法

    首先在项目build.gradle中添加如下依赖

    android {
        ...
        compileOptions {
            sourceCompatibility JavaVersion.VERSION_1_8
            targetCompatibility JavaVersion.VERSION_1_8
        }
        ...
    }
    
    dependencies {
        ...
    
        // ButterKnife
        implementation 'com.jakewharton:butterknife:10.2.3'
        // 下载butterknife-compiler jar包
        implementation 'com.jakewharton:butterknife-compiler:10.2.3'
        annotationProcessor 'com.jakewharton:butterknife-compiler:10.2.3'
    
        ...
    }
    

    其中implementation 'com.jakewharton:butterknife-compiler:10.2.3'是为了下载butterknife-compiler.jar包,方便看ButterKnifeProcessor.java源码。sync后项目中应该出现如下文件:

    image.png

    接下来新建一个Demo

    public class ButterKnifeActivity extends AppCompatActivity {
        ...
        @BindView(R.id.tv_loading)
        TextView mTvLoading;
    
        @BindView(R.id.rc_images)
        RecyclerView mRecyclerView;
    
        @OnClick(R.id.btn_scroll)
        void scroll() {
            startAnimator();
        }
    
        private Unbinder mUnBinder;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_butter_knife);
            mUnBinder = ButterKnife.bind(this);
            ...
        }
    
        @Override
        protected void onDestroy() {
            super.onDestroy();
            Log.e(TAG, "onDestroy");
            mUnBinder.unbind();
            ...
        }
    

    注意: ButterKnife标签注解的变量都不能用privatestatic修饰,原因后面解释。

    二、原理分析

    直觉告诉我们应该从ButterKnife.bind(this)开始分析,那我们先看这部分源码:

    @NonNull
    @UiThread
    public static Unbinder bind(@NonNull Activity target) {
        View sourceView = target.getWindow().getDecorView();
        return bind(target, sourceView);
    }
    

    getDecorView()返回的是当前Activity的顶层View,是一个FrameLayout,接着往下看:

    @NonNull
    @UiThread
    public static Unbinder bind(@NonNull Object target, @NonNull View source) {
        // 获取要绑定target对应的Class
        Class<?> targetClass = target.getClass();
        // 根据Class获取target对象构造器,该构造器继承了Unbinder
        Constructor<? extends Unbinder> constructor = findBindingConstructorForClass(targetClass);
    
        if (constructor == null) {
            return Unbinder.EMPTY;
        }
    
        //noinspection TryWithIdenticalCatches Resolves to API 19+ only type.
        try {
            // 根据构造器反射生成Unbinder子类实例
            return constructor.newInstance(target, source);
        } catch (IllegalAccessException e) {
            throw new RuntimeException("Unable to invoke " + constructor, e);
        }
        ...
    }
    

    先通过target.getClass()获取target对应的Class,再通过findBindingConstructorForClass(targetClass)生成一个继承了Unbinder的构造器Constructor,最后通过反射生成这个Unbinder的一个实例,也就是后面说到的xxx_ViewBinding.java,至此ButterKnife.bind(this)操作结束。我们重点看下findBindingConstructorForClass(targetClass)是怎么生成Unbinder的:

    static final Map<Class<?>, Constructor<? extends Unbinder>> BINDINGS = new LinkedHashMap<>();
    
    @Nullable
    @CheckResult
    @UiThread
    private static Constructor<? extends Unbinder> findBindingConstructorForClass(Class<?> cls) {
        // 现在缓存中查找是否已经存在该class,如果存在,则直接返回Constructor
        Constructor<? extends Unbinder> bindingCtor = BINDINGS.get(cls);
        if (bindingCtor != null || BINDINGS.containsKey(cls)) {
            return bindingCtor;
        }
        
        // 缓存中不存在,如果clsName是系统相关的,那么直接返回null
        String clsName = cls.getName();
        if (clsName.startsWith("android.") || clsName.startsWith("java.")
                || clsName.startsWith("androidx.")) {
            return null;
        }
        try {
            // 通过ClassLoader加载类,此处涉及类加载模型(双亲委派模型)
            Class<?> bindingClass = cls.getClassLoader().loadClass(clsName + "_ViewBinding");
            //noinspection unchecked
            bindingCtor = (Constructor<? extends Unbinder>) bindingClass.getConstructor(cls, View.class);
        } catch (ClassNotFoundException e) {
            bindingCtor = findBindingConstructorForClass(cls.getSuperclass());
        } catch (NoSuchMethodException e) {
            throw new RuntimeException("Unable to find binding constructor for " + clsName, e);
        }
        BINDINGS.put(cls, bindingCtor);
        return bindingCtor;
    }
    

    在生成Constructor<? extends Unbinder>实例的过程中,先在LinkedHashMap中查找是否已经有缓存,如果有则直接返回,否则通过类加载器加载,通过拼接类名“clsName_ViewBinding”加载,然后用bindingClass.getConstructor(cls, View.class)通过xxx_ViewBinding.java的构造方法生成xxx_ViewBinding.java实例。
    看到这里你是否有个疑问:

    项目里我明明没有手动创建xxx_ViewBinding.java类,ClassLoader怎么能加载到这个类呢?

    相信大家在用ButterKnife的时候,肯定注意到生成了很多xxx_ViewBinding.java类,他们都在这里:

    image.png
    我们看看xxx_ViewBinding.java里面都做了啥:
    public class ButterKnifeActivity_ViewBinding implements Unbinder {
        private ButterKnifeActivity target;
    
        private View view7f090071;
    
        @UiThread
        public ButterKnifeActivity_ViewBinding(ButterKnifeActivity target) {
            this(target, target.getWindow().getDecorView());
        }
    
        @UiThread
        public ButterKnifeActivity_ViewBinding(final ButterKnifeActivity target, View source) {
            this.target = target;
    
            View view;
            target.mTvLoading = Utils.findRequiredViewAsType(source, R.id.tv_loading, "field 'mTvLoading'", TextView.class);
            target.mRecyclerView = Utils.findRequiredViewAsType(source, R.id.rc_images, "field 'mRecyclerView'", RecyclerView.class);
            view = Utils.findRequiredView(source, R.id.btn_scroll, "method 'scroll'");
            view7f090071 = view;
            view.setOnClickListener(new DebouncingOnClickListener() {
                @Override
                public void doClick(View p0) {
                    target.scroll();
                }
            });
        }
    
        @Override
        @CallSuper
        public void unbind() {
            ButterKnifeActivity target = this.target;
            if (target == null) throw new IllegalStateException("Bindings already cleared.");
            this.target = null;
    
            target.mTvLoading = null;
            target.mRecyclerView = null;
    
            view7f090071.setOnClickListener(null);
            view7f090071 = null;
        }
    }
    
    >>Utils.java
    public static <T> T findRequiredViewAsType(View source, @IdRes int id, String who, Class<T> cls) {
        View view = findRequiredView(source, id, who);
        return castView(view, id, who, cls);
    }
    
    public static View findRequiredView(View source, @IdRes int id, String who) {
        View view = source.findViewById(id);
        if (view != null) {
            return view;
        }
    }
    
    public static <T> T castView(View view, @IdRes int id, String who, Class<T> cls) {
        try {
            return cls.cast(view);
        } catch (ClassCastException e) {
            ...
        }
    }
    

    这个生成类中持有了我们自己Activity的引用,View的binding都转化成了findViewById(id)。接下来我们看看这些xxx_ViewBinding.java是怎么生成的。

    三、注解处理器

    首先,要生成这个类就要先得到这个类必须的基础信息,这就涉及到了annotationProcessor技术,和 APT(Annotation Processing Tool)技术类似,它是一种注解处理器,项目编译时对源代码进行扫描检测找出存活时间为RetentionPolicy.CLASS的指定注解,然后对注解进行解析处理,进而得到要生成的类的必要信息,然后根据这些信息动态生成对应的 java 类,至于如何生成 java 类就涉及到了后边要说的 JavaPoet技术。
    摘自:https://www.jianshu.com/p/39fc66aa3297

    ButterKnife注解处理器的核心逻辑在ButterKnifeProcessor.java中,该类有1500多行,以下截取基本框架部分:

    // 该注解实现注解处理器的注册,注册到 javac 后,在项目编译时就能执行注解处理器了
    @AutoService(Processor.class)
    @IncrementalAnnotationProcessor(IncrementalAnnotationProcessorType.DYNAMIC)
    @SuppressWarnings("NullAway") // TODO fix all these...
    public final class ButterKnifeProcessor extends AbstractProcessor {
        @Override
        public synchronized void init(ProcessingEnvironment env) {
            super.init(env);
    
            String sdk = env.getOptions().get(OPTION_SDK_INT);
            if (sdk != null) {
                try {
                    this.sdk = Integer.parseInt(sdk);
                } catch (NumberFormatException e) {
                    env.getMessager()
                            .printMessage(Kind.WARNING, "Unable to parse supplied minSdk option '"
                                    + sdk
                                    + "'. Falling back to API 1 support.");
                }
            }
    
            debuggable = !"false".equals(env.getOptions().get(OPTION_DEBUGGABLE));
    
            typeUtils = env.getTypeUtils();
            filer = env.getFiler();
            try {
                trees = Trees.instance(processingEnv);
            } catch (IllegalArgumentException ignored) {
                try {
                    // Get original ProcessingEnvironment from Gradle-wrapped one or KAPT-wrapped one.
                    for (Field field : processingEnv.getClass().getDeclaredFields()) {
                        if (field.getName().equals("delegate") || field.getName().equals("processingEnv")) {
                            field.setAccessible(true);
                            ProcessingEnvironment javacEnv = (ProcessingEnvironment) field.get(processingEnv);
                            trees = Trees.instance(javacEnv);
                            break;
                        }
                    }
                } catch (Throwable ignored2) {
                }
            }
        }
    
        @Override
        public Set<String> getSupportedOptions() {
            ImmutableSet.Builder<String> builder = ImmutableSet.builder();
            builder.add(OPTION_SDK_INT, OPTION_DEBUGGABLE);
            if (trees != null) {
                builder.add(IncrementalAnnotationProcessorType.ISOLATING.getProcessorOption());
            }
            return builder.build();
        }
    
        @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(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 SourceVersion getSupportedSourceVersion() {
            return SourceVersion.latestSupported();
        }
    
        @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;
        }
    }
    

    ButterKnifeProcessor继承AbstractProcessor,并重写以下五个方法:

    1. public synchronized void init(ProcessingEnvironment env);
      完成minSdk的判断,初始化需要用到的辅助类:
      Types: typeUtils:用来对类型进行操作的工具类。
      具体用法参考http://itmyhome.com/java-api/javax/lang/model/util/Types.html
      Filer: filer:用来生成java类文件。
      具体用法参考http://itmyhome.com/java-api/javax/annotation/processing/Filer.html
      Trees: trees:
    2. public Set<String> getSupportedOptions();
      获取注解处理器可处理的注解操作。
    3. public Set<String> getSupportedAnnotationTypes();
      该方法返回一个Set<String>,代表ButterKnifeProcessor要处理的注解类的名称集合,即 ButterKnife支持的注解。
    4. public SourceVersion getSupportedSourceVersion();
      获取当前系统支持的 java 版本。
    5. public boolean process(Set<? extends TypeElement> elements, RoundEnvironment env);
      处理target类信息收集和生成对应java类流程。重点看下这个方法:
      第一步:收集注解信息
    private Map<TypeElement, BindingSet> findAndParseTargets(RoundEnvironment env) {
        Map<TypeElement, BindingSet.Builder> builderMap = new LinkedHashMap<>();
        Set<TypeElement> erasedTargetNames = new LinkedHashSet<>();   
        ...
        
        // env.getElementsAnnotatedWith(BindView.class)获取所有使用BindView注解的元素
        for (Element element : env.getElementsAnnotatedWith(BindView.class)) {
            try {
                parseBindView(element, builderMap, erasedTargetNames);
            } catch (Exception e) {
                logParsingError(element, BindView.class, e);
            }
        }
    
        ...
        // 根据上面找到的所有TypeElement和BindingSet返回所有父类元素
        Map<TypeElement, ClasspathBindingSet> classpathBindings =
            findAllSupertypeBindings(builderMap, erasedTargetNames);
        // 将builderMap中的数据添加到队列中
        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();
            // 拿到第一个Entry对象
            TypeElement type = entry.getKey();
            BindingSet.Builder builder = entry.getValue();
            // 根据TypeElement查找父类元素
            TypeElement parentType = findParentType(type, erasedTargetNames, classpathBindings.keySet());
            // 父类为null,则直接将当前TypeElement放入bindingMap中
            if (parentType == null) {
                bindingMap.put(type, builder.build());
            } else {
                // 父类不为null,则根据父类TypeElemet在bindingMap查找(BindingSet implements BindingInformationProvider)
                BindingInformationProvider parentBinding = bindingMap.get(parentType);
                // 如果从bindingMap中拿到的BindingInformationProvider为null,则从classpathBindings中查找(ClasspathBindingSet implements BindingInformationProvider)
                if (parentBinding == null) {
                    parentBinding = classpathBindings.get(parentType);
                }
                // 最终找到父类BindingInformationProvider
                if (parentBinding != null) {
                    // 为当前BindingSet设置父Binding,并更新bindingMap对应位置元素
                    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为例,其他逻辑类似。先解释一下上面代码用到的几个类:

    1. TypeElement:代表使用了 ButterKnife 的类,即 Activity、Fragment等。
    2. BindingInformationProvider:BindingSet内部接口,提供获取绑定类类名的方法和是否需要一个View用来绑定的判断:
    interface BindingInformationProvider {
          boolean constructorNeedsView();
          ClassName getBindingClassName();
    }
    
    1. BindingSet:实现BindingInformationProvider,用来存储要生成类的基本信息以及注解元素的相关信息。
    2. ClasspathBindingSet:实现BindingInformationProvider,并在构造方法中获取绑定类类名。

    总结:整个findAndParseTargets()方法流程是先将解析得到的所有注解和绑定信息存放在builderMaperasedTargetNames中,中间通过子TypeElement找到其父类绑定关系,并更新到子类中,最终将整合的绑定关系以TypeElement为key,BindingSet为value存到bindingMap中并返回。

    下面看看parseBindView()是怎么解析注解的:

    private void parseBindView(Element element, Map<TypeElement, BindingSet.Builder> builderMap,
                               Set<TypeElement> erasedTargetNames) {
        // 首先要注意,此时element是VariableElement类型的,即成员变量,比如前面例子中用到的mTvLoading
        // enclosingElement是当前元素的父类元素,一般就是我们使用ButteKnife时定义的View类型成员变量所在的类,可以理解为之前例子中的ButterKnifeActivity
        TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();
    
        // 进行相关校验
        // 1、isInaccessibleViaGeneratedCode(),先判断当前元素的是否是private或static类型,
        // 再判断其父元素是否是一个类以及是否是private类型。
        // 2、isBindingInWrongPackage(),是否在系统相关的类中使用了ButteKnife注解
        boolean hasError = isInaccessibleViaGeneratedCode(BindView.class, "fields", element)
                || isBindingInWrongPackage(BindView.class, element);
    
        // TypeMirror表示Java编程语言中的一种类型。类型包括基元类型,声明的类型(类和接口类型),数组类型,类型变量和空类型。 
        // 还表示了通配符类型参数,可执行文件的签名和返回类型,以及与包和关键字void相对应的伪类型。
        TypeMirror elementType = element.asType();
        // 如果当前元素是类的成员变量
        if (elementType.getKind() == TypeKind.TYPEVAR) {
            TypeVariable typeVariable = (TypeVariable) elementType;
            elementType = typeVariable.getUpperBound();
        }
        Name qualifiedName = enclosingElement.getQualifiedName();
        Name simpleName = element.getSimpleName();
        // 判断当前元素是否是 View 的子类,或者是接口,不是的话抛出异常
        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;
        }
    
        // 获得元素使用BindView注解时设置的属性值,即 View 对应的xml中的id
        int id = element.getAnnotation(BindView.class).value();
        // 尝试获取父元素对应的BindingSet.Builder
        BindingSet.Builder builder = builderMap.get(enclosingElement);
        // resourceId,比如上面例子里的R.id.tv_loading
        Id resourceId = elementToId(element, BindView.class, id);
        if (builder != null) {
            // 返回绑定元素的名称,比如mTvLoading
            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 {
            // 创建一个新的BindingSet.Builder并返回,并且以enclosingElement 为key添加到builderMap中
            builder = getOrCreateBindingBuilder(builderMap, enclosingElement);
        }
    
        String name = simpleName.toString();
        TypeName type = TypeName.get(elementType);
        // 判断当前元素是否使用了Nullable注解
        boolean required = isFieldRequired(element);
    
        builder.addField(resourceId, new FieldViewBinding(name, type, required));
    
        // Add the type-erased version to the valid binding targets set.
        erasedTargetNames.add(enclosingElement);
    }
    

    enclosingElement是当前元素的父类元素,一般就是我们使用ButteKnife时定义的View类型成员变量所在的类,可以理解为之前例子中的ButterKnifeActivity

    第一步即校验绑定信息是否合法,包括绑定元素修饰符是否合法(非private,非static)和绑定类是否是系统相关类,另外对绑定元素类型(View,Interface)进行校验。

    现在可以回到开头提到的问题,为什么绑定元素不能用private和static修饰:用private修饰的元素在生成的xxx_ViewBinding类中无法通过target类直接访问(当然可以用反射,但那样的话性能就会打折扣);static修饰的元素存在内存泄漏风险。

    第二步是通过传入的参数进解析出元素id,类型等信息并更新到当前的BindingSet.Builder中。下面看看getOrCreateBindingBuilder()是怎么更新BindingSet.Builder的:

    private BindingSet.Builder getOrCreateBindingBuilder(
            Map<TypeElement, BindingSet.Builder> builderMap, TypeElement enclosingElement) {
        BindingSet.Builder builder = builderMap.get(enclosingElement);
        if (builder == null) {
            builder = BindingSet.newBuilder(enclosingElement);
            builderMap.put(enclosingElement, builder);
        }
        return builder;
    }
    

    这段比较简单,接着看newBuilder()里面做了什么:

    static BindingSet.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);
    
        // 判断父类元素是不是final类型
        boolean isFinal = enclosingElement.getModifiers().contains(Modifier.FINAL);
        return new BindingSet.Builder(targetType, bindingClassName, enclosingElement, isFinal, isView, isActivity,
                isDialog);
    }
    

    综上,所以BindingSet主要保存了要生成的目标类的基本特征信息,以及类中使用了 ButterKnife 注解的元素(成员变量)的信息,这样一个BindingSet就和一个使用了ButterKnife 的类对应了起来。至此,要生成的目标类基本信息就收集就完成了。下一步就是生成java文件了。

    四、JavaPoet

    JavaPoet是一个生成.java文件的开源框架,ButterKnife就是JavaPoet来生成java文件的。回到process()方法:

    @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 javaFile = binding.brewJava(sdk, debuggable);
            try {
                // 生成java文件,写入磁盘
                javaFile.writeTo(filer);
            } catch (IOException e) {
                error(typeElement, "Unable to write binding for type %s: %s", typeElement, e.getMessage());
            }
        }
    
        return false;
    }
    

    JavaPoet如何生成java文件的过程这里就不分析了,感兴趣的朋友可以去研究一下。

    五、总结

    可以看出 ButterKnife 整个过程是在项目编译阶段完成的,主要用到了 annotationProcessor 和 JavaPoet 技术,使用时通过生成的辅助类完成操作,并不是在项目运行时通过注解加反射实现的,所以并不会影响项目运行时的性能,可能仅在项目编译时有略微的影响。

    相关文章

      网友评论

          本文标题:ButterKnife原理

          本文链接:https://www.haomeiwen.com/subject/baiqiktx.html