android 动画之漂移,Android从上车到漂移之ButterKnife完全解析

一、前言

ButterKnife——通过注解的方式生成View字段、资源绑定和方法绑定的样板代码,是一款老司机书写UI布局的必备神器!自从有了ButterKnife,妈妈再也不用担心我findViewbyid(),find到手抽筋。

本文基于最新的8.7.0版本进行分析,不同版本可能实现方式有所差异,请知悉。

二、上车

下载Android studio 插件Android ButterKnife Zelezny,一键生成模板代码。更多姿势,请参考官方文档。作为一个老司机,上车不是我们的重点,漂移才是我们的目标!

三、漂移

checkout下来源码,工程结构如图,核心实现模块是butterknife、butterknife-annotations、butterknife-compiler。

11d185deddcb

butterknife工程结构图.png

首先从入口开始,以activity的bind为例,代码如下:

@NonNull @UiThread

public static Unbinder bind(@NonNull Activity target) {

View sourceView = target.getWindow().getDecorView();

return createBinding(target, sourceView);

}

先获取activity的根布局DecorView,然后作为参数传递给createBinding()方法,该方法就是通过构造函数new出一个“clsName + "_ViewBinding”的class对象。看一下createBinding()方法的关键代码:

private static Unbinder createBinding(@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;

}

//noinspection TryWithIdenticalCatches Resolves to API 19+ only type.

try {

return constructor.newInstance(target, source);

} catch (IllegalAccessException e) {

throw new RuntimeException("Unable to invoke " + constructor, e);

}

继续追踪 findBindingConstructorForClass()的源码:

@Nullable @CheckResult @UiThread

private static Constructor extends Unbinder> findBindingConstructorForClass(Class> cls) {

Constructor extends Unbinder> bindingCtor = BINDINGS.get(cls);

if (bindingCtor != null) {

if (debug) Log.d(TAG, "HIT: Cached in binding map.");

return bindingCtor;

}

String clsName = cls.getName();

if (clsName.startsWith("android.") || clsName.startsWith("java.")) {

if (debug) Log.d(TAG, "MISS: Reached framework class. Abandoning search.");

return null;

}

try {

Class> bindingClass = cls.getClassLoader().loadClass(clsName + "_ViewBinding");

//noinspection unchecked

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

}

BINDINGS.put(cls, bindingCtor);

return bindingCtor;

}

该方法首先从BINDINGS这个LinkedHashMap中查找缓存,如果命中,直接返回bindingCtor,bindingCtor是实现了Unbinder接口的子类的构造函数;如果为null,则通过ClassLoade加载一个"clsName + "_ViewBinding""的类,然后返回其构造函数。其中clsName 就是我们上文调用bind()的activity,最后把它put到BINDINGS这个集合中。

那么"clsName + "_ViewBinding""的这个类在哪?里面实现了什么逻辑?又是怎么生成的?这些才是今天的重点!接下来我们一个个解答:

3.1 "clsName + "_ViewBinding""的这个类在哪?:

11d185deddcb

clsName + "_ViewBinding生成路径.jpg

大家根据截图一层一层追进去就可以找到相应代码。大致是:app->build->generated->source->apt->"打包渠道"->clsName类在工程中所在目录

3.2 "clsName + "_ViewBinding""的这个类实现了什么逻辑?

如上所述,该类实现了Unbinder接口,有两个重载的构造函数,和一个unbind()方法,unbind()方法很简单,顾名思义就是解除绑定,释放资源,没啥好说的。

public class BusinessmenListActivity_ViewBinding implements Unbinder {

private BusinessmenListActivity target;

@UiThread

public BusinessmenListActivity_ViewBinding(BusinessmenListActivity target) {

this(target, target.getWindow().getDecorView());

}

@UiThread

public BusinessmenListActivity_ViewBinding(BusinessmenListActivity target, View source) {

this.target = target;

target.mRecyclerView = Utils.findRequiredViewAsType(source, R.id.rv_bus, "field 'mRecyclerView'", RecyclerView.class);

target.mRefreshLayout = Utils.findRequiredViewAsType(source, R.id.refresh_layout, "field 'mRefreshLayout'", SwipeRefreshLayout.class);

}

@Override

@CallSuper

public void unbind() {

BusinessmenListActivity target = this.target;

if (target == null) throw new IllegalStateException("Bindings already cleared.");

this.target = null;

target.mRecyclerView = null;

target.mRefreshLayout = null;

}

}

我们重点看一下Utils.findRequiredViewAsType()的代码,就是这个方法帮我们实现了绑定View相关逻辑。先调用findRequiredView()方法返回Viwe对象,然后再castView()成具体的子View。比较简单,直接看代码相信都能看懂:

public static T findRequiredViewAsType(View source, @IdRes int id, String who,

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

}

String name = getResourceEntryName(source, id);

throw new IllegalStateException("Required view '"

+ name

+ "' with ID "

+ id

+ " for "

+ who

+ " was not found. If this view is optional add '@Nullable' (fields) or '@Optional'"

+ " (methods) annotation.");

}

castView()代码:

public static T castView(View view, @IdRes int id, String who, Class cls) {

try {

return cls.cast(view);

} catch (ClassCastException e) {

String name = getResourceEntryName(view, id);

throw new IllegalStateException("View '"

+ name

+ "' with ID "

+ id

+ " for "

+ who

+ " was of the wrong type. See cause for more info.", e);

}

}

至此,就完成了View绑定的所有逻辑。

3.3 "clsName + "_ViewBinding"".java文件是如何生成的?

该类的生成主要依赖一个叫做APT的工具。APT(Annotation Processing Tool)是一种处理注解的工具,它对源代码文件进行检测找出其中的Annotation,使用Annotation进行额外的处理。

使用apt需要继承AbstractProcessor类,同时有几个核心方法需要实现,分别是:

init()主要做一些初始化操作;

getSupportedAnnotationTypes(),顾名思义,获取所有支持的注解类型;

process()处理注解相关逻辑,"clsName + "_ViewBinding"".java文件生成的核心逻辑就在这里!

在ButterKnife中,有一个ButterKnifeProcessor类,该类就是处理ButterKnife注解相关逻辑的类。

3.3.1 我们先从init()方法开始:初始化。

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

elementUtils = env.getElementUtils();

typeUtils = env.getTypeUtils();

filer = env.getFiler();

try {

trees = Trees.instance(processingEnv);

} catch (IllegalArgumentException ignored) {

}

}

init()方法中,没有过多逻辑,只是有几个变量需要说明一下,这几个主要是注解处理时用到的工具类。

private Elements elementUtils;

private Types typeUtils;

private Filer filer;

private Trees trees;

Elements:一个用来处理Element的工具类,源代码的每一个部分都是一个特定类型的Element。例如,包名、字段、方法等等。

Types:一个用来处理TypeMirror的工具类,比如判断该元素是class还是interface;

Filer:生成文件;

Trees :树,遍历文件用到。

3.3.2 接下来看一下getSupportedAnnotationTypes()方法:获取所有支持的注解类型。

@Override public Set getSupportedAnnotationTypes() {

Set types = new LinkedHashSet<>();

for (Class extends Annotation> annotation : getSupportedAnnotations()) {

types.add(annotation.getCanonicalName());

}

return types;

}

private Set> getSupportedAnnotations() {

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

}

该方法的大概意思就是,将所有ButterKnife用到的注解全部添加到支持的注解集合中。

我们来瞄一眼最熟悉的BindView.class

@Retention(CLASS) @Target(FIELD)

public @interface BindView {

/** View ID to which the field will be bound. */

@IdRes int value();

}

代码很简单,但是有几个注解相关的点需要说明一下,

元注解:元注解的作用就是负责注解其他注解。相关元注解:

1.@Target:

表示注解类型所适用的程序元素的种类。

2.@Retention:

表示该注解类型的注解保留的时长。

SOURCE 仅存在Java源文件,经过编译器后便丢弃相应的注解;

CLASS 存在Java源文件,以及经编译器后生成的Class字节码文件,但在运行时VM不再保留注释;

RUNTIME 存在源文件、编译生成的Class字节码文件,以及保留在运行时VM中,可通过反射性地读取注解。

对应到BindView这个注解中,我们可以知道,该注解适用的类型为字段,且会打包到Class字节码文件中,该注解接收的值类型为@IdRes int类型。

3.3.3 最后process()方法:遍历所有注解->根据注解生成相应的代码->生成java文件。

@Override public boolean process(Set extends TypeElement> elements, RoundEnvironment env) {

Map bindingMap = findAndParseTargets(env);

for (Map.Entry 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()方法核心代码:

private Map findAndParseTargets(RoundEnvironment env) {

Map builderMap = new LinkedHashMap<>();

Set erasedTargetNames = new LinkedHashSet<>();

// ...... 此处省略若干行代码

// Process each @BindView element.

for (Element element : env.getElementsAnnotatedWith(BindView.class)) {

// we don't SuperficialValidation.validateElement(element)

// so that an unresolved View type can be generated by later processing rounds

try {

parseBindView(element, builderMap, erasedTargetNames);

} catch (Exception e) {

logParsingError(element, BindView.class, e);

}

}

}

// 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> entries =

new ArrayDeque<>(builderMap.entrySet());

Map bindingMap = new LinkedHashMap<>();

while (!entries.isEmpty()) {

Map.Entry entry = entries.removeFirst();

TypeElement type = entry.getKey();

BindingSet.Builder builder = entry.getValue();

TypeElement parentType = findParentType(type, erasedTargetNames);

if (parentType == null) {

bindingMap.put(type, builder.build());

} else {

BindingSet parentBinding = bindingMap.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 为例,其他的类似,不再一一赘述。关键代码:

// Process each @BindView element.

for (Element element : env.getElementsAnnotatedWith(BindView.class)) {

// we don't SuperficialValidation.validateElement(element)

// so that an unresolved View type can be generated by later processing rounds

try {

parseBindView(element, builderMap, erasedTargetNames);

} catch (Exception e) {

logParsingError(element, BindView.class, e);

}

}

}

遍历所有使用@BindView注解的Element,继续看parseBindView()代码:

private void parseBindView(Element element, Map builderMap,

Set erasedTargetNames) {

TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();

// Assemble information on the field.

//获取绑定的View的id,即:R.id.xx.

int id = element.getAnnotation(BindView.class).value();

//判断该元素是否已经绑定过,如果绑定过,返回错误,否则,调用getOrCreateBindingBuilder()方法

BindingSet.Builder builder = builderMap.get(enclosingElement);

QualifiedId qualifiedId = elementToQualifiedId(element, id);

if (builder != null) {

String existingBindingName = builder.findExistingBindingName(getId(qualifiedId));

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(getId(qualifiedId), new FieldViewBinding(name, type, required));

// Add the type-erased version to the valid binding targets set.

erasedTargetNames.add(enclosingElement);

}

首先,通过 int id = element.getAnnotation(BindView.class).value();获取绑定的View的id,即:R.id.xx.;

再通过elementToQualifiedId()方法,生成一个合格标识:

private QualifiedId elementToQualifiedId(Element element, int id) {

return new QualifiedId(elementUtils.getPackageOf(element).getQualifiedName().toString(), id);

}

第三步,从已经绑定的元素中查找该元素是否存在,如果存在,返回错误,不允许重复绑定,否则调用getOrCreateBindingBuilder()方法生成一个绑定对象:

private BindingSet.Builder getOrCreateBindingBuilder(

Map builderMap, TypeElement enclosingElement) {

BindingSet.Builder builder = builderMap.get(enclosingElement);

if (builder == null) {

builder = BindingSet.newBuilder(enclosingElement);

builderMap.put(enclosingElement, builder);

}

return builder;

}

进一步看看 BindingSet.newBuilder(enclosingElement)方法

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;

}

String packageName = getPackage(enclosingElement).getQualifiedName().toString();

String className = enclosingElement.getQualifiedName().toString().substring(

packageName.length() + 1).replace('.', '$');

ClassName bindingClassName = ClassName.get(packageName, className + "_ViewBinding");

boolean isFinal = enclosingElement.getModifiers().contains(Modifier.FINAL);

return new Builder(targetType, bindingClassName, isFinal, isView, isActivity, isDialog);

}

就是这里将生成的java文件类名定义为“className + "_ViewBinding"”

第四步,回到parseBindView()方法,为绑定对象添加绑定的View字段:

builder.addField(getId(qualifiedId), new FieldViewBinding(name, type, required));

第五步,关联父类绑定的资源(view、string、listener等),并把她们添加到 Map bindingMap = new LinkedHashMap<>()这个集合当中,这个集合存储了所有的BuildSet对象。

BuildSet对象就是单个绑定类型(activity、fragment、view、dialog)的所有绑定请求(BindView、BindString等)的集合。每一个使用ButterKnife的类对应一个BuildSet。

// 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> entries =

new ArrayDeque<>(builderMap.entrySet());

Map bindingMap = new LinkedHashMap<>();

while (!entries.isEmpty()) {

Map.Entry entry = entries.removeFirst();

TypeElement type = entry.getKey();

BindingSet.Builder builder = entry.getValue();

TypeElement parentType = findParentType(type, erasedTargetNames);

if (parentType == null) {

bindingMap.put(type, builder.build());

} else {

BindingSet parentBinding = bindingMap.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);

}

}

主要看一下关键代码 BindingSet build()代码,以及BindingSet的构造函数:

BindingSet build() {

ImmutableList.Builder viewBindings = ImmutableList.builder();

for (ViewBinding.Builder builder : viewIdMap.values()) {

viewBindings.add(builder.build());

}

return new BindingSet(targetTypeName, bindingClassName, isFinal, isView, isActivity, isDialog,

viewBindings.build(), collectionBindings.build(), resourceBindings.build(),

parentBinding);

}

private BindingSet(TypeName targetTypeName, ClassName bindingClassName, boolean isFinal,

boolean isView, boolean isActivity, boolean isDialog, ImmutableList viewBindings,

ImmutableList collectionBindings,

ImmutableList resourceBindings, BindingSet parentBinding) {

this.isFinal = isFinal;

this.targetTypeName = targetTypeName;

this.bindingClassName = bindingClassName;

this.isView = isView;

this.isActivity = isActivity;

this.isDialog = isDialog;

this.viewBindings = viewBindings;

this.collectionBindings = collectionBindings;

this.resourceBindings = resourceBindings;

this.parentBinding = parentBinding;

}

至此,所有需要绑定的资源已经添加到集合当中,只差生成代码,可谓万事俱备只欠东风!

回到process()代码:

@Override public boolean process(Set extends TypeElement> elements, RoundEnvironment env) {

Map bindingMap = findAndParseTargets(env);

for (Map.Entry 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;

}

通过第一行代码,我们几经周折终于掌握了她的来龙去脉,还剩一个for循环。for循环无非就是遍历生成相应的"clsName + "_ViewBinding"".java文件,具体怎么生成的,我们跟进去瞄一眼:

JavaFile brewJava(int sdk, boolean debuggable) {

return JavaFile.builder(bindingClassName.packageName(), createType(sdk, debuggable))

.addFileComment("Generated code from Butter Knife. Do not modify!")

.build();

}

这里涉及一个重要的知识点:JavaPoet。此为何物? 套用官方的简介就是:“JavaPoet is a Java API for generating .java source files.” 简直精辟得不能再精辟!

简单理解——就是用来生成java文件的,这不正是我们所要的东风吗?具体的使用姿势,不是本文的重点,可以查看官方文档,文档写得相当详细,在此不再一一赘述,本文只对用到的地方做一些解析。

看一下createType()方法:

private TypeSpec createType(int sdk, boolean debuggable) {

TypeSpec.Builder result = TypeSpec.classBuilder(bindingClassName.simpleName())

.addModifiers(PUBLIC);

if (isFinal) {

result.addModifiers(FINAL);

}

if (parentBinding != null) {

result.superclass(parentBinding.bindingClassName);

} 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()) {

// Add a delegating constructor with a target type + view signature for reflective use.

result.addMethod(createBindingViewDelegateConstructor());

}

result.addMethod(createBindingConstructor(sdk, debuggable));

if (hasViewBindings() || parentBinding == null) {

result.addMethod(createBindingUnbindMethod(result));

}

return result.build();

}

如果你已经了解了JavaPoet之后再来看这个代码,其实可以一目了然,这里不做过多解释,仅仅验证一下,我们的设想与生成的"clsName + "_ViewBinding"".java文件内容是否一致即可。

主要看一下createBindingConstructor()方法,该方法是生成"clsName + "_ViewBinding"".java代码的核心:

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 (hasUnqualifiedResourceBindings()) {

// Aapt can change IDs out from underneath us, just suppress since all will work at runtime.

constructor.addAnnotation(AnnotationSpec.builder(SuppressWarnings.class)

.addMember("value", "$S", "ResourceType")

.build());

}

if (hasOnTouchMethodBindings()) {

constructor.addAnnotation(AnnotationSpec.builder(SUPPRESS_LINT)

.addMember("value", "$S", "ClickableViewAccessibility")

.build());

}

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()) {

// Local variable in which all views will be temporarily stored.

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

}

大功告成!所有的逻辑执行完毕之后,就生成是我们3.2节对应的代码。

4、总结

祝各位司机漂移成功!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值