07、Android进阶--ButterKnife原理解析

ButterKnife原理

ButterKnife自定义了很多我们常用的注解,比如@BindView和@OnClick。现在先来看@BindView的源码,如下所示:

@Retention(RUNTIME) @Target(FIELD)
public @interface BindView {
  /** View ID to which the field will be bound. */
  @IdRes int value();
}

Retention(CLASS)表明@BindView 注解是编译时注解,@Target(FIELD)则表明@BindView注解应用于成员变量。

接下来使用@BindView注解来绑定TextView 控件,后文会用到这些代码,如下所示:

public class MainActivity extends AppCompatActivity {
    @BindView(R.id.tv_text)
    TextView tvText;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ButterKnife.bind(this);
    }
}

ButterKnifeProcessor源码分析

要处理注解需要注解处理器,ButterKnife的注解处理器是ButterKnifeProcessor,它的主要处理逻辑都在process方法中,源码如下所示:

@AutoService(Processor.class)
public final class ButterKnifeProcessor extends AbstractProcessor {
	......
    @Override
    public boolean process(Set<? extends TypeElement> elements, RoundEnvironment env) {
        // findAndParseTargets方法主要用于 查找和解析注解。
        Map<TypeElement, BindingSet> bindingMap = findAndParseTargets(env);
        // 遍历findAndParseTargets方法返回的Map集合
        for (Map.Entry<TypeElement, BindingSet> entry : bindingMap.entrySet()) {
            TypeElement typeElement = entry.getKey();
            BindingSet binding = entry.getValue();
			// 得到BindingSet的值,并调用了它的brewJava方法
            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 true;
    }
	......
}

ButterKnifeProcessor继承自AbstractProcessor,我们再来看看findAndParseTargets方法:

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;    
}      

findAndParseTargets 方法会查找所有 ButterKnife 的注解来进行解析,这里只看处理@BindView注解的部分。然后查看parseBindView方法,如下所示

private void parseBindView(Element element, Map<TypeElement, BindingSet.Builder> builderMap,
                           Set<TypeElement> erasedTargetNames) {
    TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();
    /**
     * isInaccessibleViaGeneratedCode方法里面检查了3个点,它们分别是:
     * 1、方法修饰符不能为private和static;
     * 2、包含类型不能为非Class;
     * 3、包含类的修饰符不能是private。
     */
    */
    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);
    // 判断是否存在BindingSet.Builder的值,若没有则创建,有则复用。
    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);
    // 将注解修饰的类型的信息存储在 FieldViewBinding中,并将FieldViewBinding传入 BindingSet.Builder的addField方法中。
    builder.addField(resourceId, new FieldViewBinding(name, type, required));
    erasedTargetNames.add(enclosingElement);
}

可以看出,该方法主要用于检查类型和修饰符以及包名等,然后将注解修饰的类型的信息进行存储。

接下来我们回到process方法,brewJava方法将使用注解的类生成一个JavaFile,然后将该JavaFile输出成Java文件。(javaFile.writeTo(filer))

在build-generared-source-apt目录下可以找到生成的Java文件,这里生成的文件名为MainActivity_ViewBinding。

ButterKnife的bind方法

为了使用ButterKnife,我们需要用ButterKnife.bind()方法来绑定上下文。现在先来查看bind方法:

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

bind 方法有很多重载方法,上面的代码只是其中的一种,也就是传入 Activity的情况。得到Activity的DecorView,并将DecorView和Activity传入bind方法中:

@NonNull @UiThread
public static Unbinder bind(@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;
    }

    try {
        // MainActivity_ViewBinding实例,并将Activity和其DecorView作为参数传入构造函数
        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);
    }
}

调用了findBindingConstructorForClass方法获取Activity构造函数,方法如下所示:

@Nullable @CheckResult @UiThread
private static Constructor<? extends Unbinder> findBindingConstructorForClass(Class<?> cls) {
    // 先从BINDINGS中获取对应Class的Constructor实例,BINDINGS是一个Class为key、Constructor为value的Map。
    Constructor<? extends Unbinder> bindingCtor = BINDINGS.get(cls);
    if (bindingCtor != null || BINDINGS.containsKey(cls)) {
        if (debug) Log.d(TAG, "HIT: Cached in binding map.");
        return bindingCtor;
    }
    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类就是我们此前生成的MainActivity_ViewBinding。
        Class<?> bindingClass = cls.getClassLoader().loadClass(clsName + "_ViewBinding");
        // 调用getConstructor方法将Class转换为Constructor,
        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);
    }
    // 处将Constructor作为value,Class作为key存储在BINDINGS中,最后返回该Constructor。
    BINDINGS.put(cls, bindingCtor);
    return bindingCtor;
}

代码注释中使用到了反射的方式来创建MainActivity_ViewBinding,反射会影响性能,但是因为有 BINDERS的存在(一个类只会在第一次反射生成,以后会从BINDERS中去取),也可以解决一些性能问题。

生成的辅助类分析

MainActivity_ViewBinding.java,在app\build\generated\ap_generated_sources\debug\out\com\legend\butterknife文件夹下,代码如下所示:

public class MainActivity_ViewBinding implements Unbinder {
    private MainActivity target;
    // source值就是MainActivity的 DecorView,而target值为MainActivity 
    @UiThread
    public MainActivity_ViewBinding(MainActivity target) {
        this(target, target.getWindow().getDecorView());
    }

    @UiThread
    public MainActivity_ViewBinding(MainActivity target, View source) {
        this.target = target;
        // 调用了Utils的findRequiredViewAsType方法并将source值、R.id.tv_text等参数传入,findRequiredViewAsType方法
        target.tvText = Utils.findRequiredViewAsType(source, R.id.tv_text, "field 'tvText'", TextView.class);
    }

    @Override
    @CallSuper
    public void unbind() {
        MainActivity target = this.target;
        if (target == null) throw new IllegalStateException("Bindings already cleared.");
        this.target = null;

        target.tvText = null;
    }
}

在前面我们已经知道在 bind方法中调用 newInstance 方法生成 MainActivity_ViewBinding实例时传入的source值是MainActivity的DecorView。而target值为MainActivity。

注释处,findRequiredViewAsType传入source的值和R.id.tv_text等参数,让我们看看该方法:

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);
}

该方法调用findRequiredView方法,代码如下所示:

public static View findRequiredView(View source, @IdRes int id, String who) {
    View view = source.findViewById(id);
    if (view != null) {
        return view;
    }
	......
}

findRequiredView方法调用了DecorView的findViewById方法并将R.id.tv_text对应的View返回。再看看castView方法:

public static <T> T castView(View view, @IdRes int id, String who, Class<T> cls) {
    try {
        return cls.cast(view);
    } catch (ClassCastException e) {
        String name = getResourceEntryName(view, id);
		......
    }
}

castView方法会将View强制转换为TextView并返回。再次回到MainActivity_ViewBinding辅助类,这个返回的TextView会赋值给target,也就是MainActivity,这样我们在MainActivity中就可以使用这个TextView。

总结

ButterKnifeProcessor主要用于查找所有的注解,并生成相应的xxxx_ViewBingding类,然后在ButterKnife的bind方法来通过反射创建xxx_ViewBinding类的同时,将Activity的引用和DecorView的引用通过构造函数的方式传入,通过DecorView的findViewById方法拿到View的实例,然后在castView方法中进行强转并返回。

上一篇:APK:播放资源文件中的视频


下一篇:Android Studio像eclipse那样自动补全和修正代码