ButterKnife原理

2020-11-18  本文已影响0人  小O机

参考: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 技术,使用时通过生成的辅助类完成操作,并不是在项目运行时通过注解加反射实现的,所以并不会影响项目运行时的性能,可能仅在项目编译时有略微的影响。

上一篇下一篇

猜你喜欢

热点阅读