您的位置:首页 > Web前端

浅析ButterKnife的实现 (二) —— BindResource

2016-08-15 09:33 155 查看
相关文章:

浅析ButterKnife的实现 (一) —— 搭建开发框架

周末两天早起看TI,看中国夺冠还是很激动的,周末时间一晃也就过去了。不说废话了,接着上一篇现在从最简单的Resource资源绑定来说明,大体了解整个开发基本流程。

@BindString

先定义个用来注入字符串资源的注解:

/**
* 字符串资源注解
*/
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface BindString {
@StringRes int value();
}


可以看到这是一个编译时注解(RetentionPolicy.CLASS),并且注解指定为Field注解(ElementType.FIELD),注解有个int型的属性值用来标注字符串资源ID(R.string.xxx)。注意这里对这个属性使用了 @StringRes 注解来限定取值范围只能是字符串资源,这个注解是在com.android.support:support-annotations 库里的,也就是之前为什么要加这个库。

再来看下注解处理器怎么处理这个注解:

@AutoService(Processor.class)
public class ButterKnifeProcessor extends AbstractProcessor {

private Types typeUtils;
private Elements elementUtils;
private Filer filer;
private Messager messager;

@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
typeUtils = processingEnv.getTypeUtils();
elementUtils = processingEnv.getElementUtils();
filer = processingEnv.getFiler();
messager = processingEnv.getMessager();
}

@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
// 保存包含注解元素的目标类,注意是使用注解的外围类,主要用来处理父类继承,例:MainActivity
Set<TypeElement> erasedTargetNames = new LinkedHashSet<>();
// TypeElement 使用注解的外围类,BindingClass 对应一个要生成的类
Map<TypeElement, BindingClass> targetClassMap = new LinkedHashMap<>();

// 处理BindString
for (Element element : roundEnv.getElementsAnnotatedWith(BindString.class)) {
if (VerifyHelper.verifyResString(element, messager)) {
ParseHelper.parseResString(element, targetClassMap, erasedTargetNames, elementUtils);
}
}
// 略...

for (Map.Entry<TypeElement, BindingClass> entry : targetClassMap.entrySet()) {
TypeElement typeElement = entry.getKey();
BindingClass bindingClass = entry.getValue();

// 查看是否父类也进行注解绑定,有则添加到BindingClass
TypeElement parentType = _findParentType(typeElement, erasedTargetNames);
if (parentType != null) {
BindingClass parentBinding = targetClassMap.get(parentType);
bindingClass.setParentBinding(parentBinding);
}

try {
// 生成Java文件
bindingClass.brewJava().writeTo(filer);
} catch (IOException e) {
_error(typeElement, "Unable to write view binder for type %s: %s", typeElement,
e.getMessage());
}
}
return true;
}

/**
* 查找父类型
* @param typeElement   类元素
* @param erasedTargetNames 存在的类元素
* @return
*/
private TypeElement _findParentType(TypeElement typeElement, Set<TypeElement> erasedTargetNames) {
TypeMirror typeMirror;
while (true) {
// 父类型要通过 TypeMirror 来获取
typeMirror = typeElement.getSuperclass();
if (typeMirror.getKind() == TypeKind.NONE) {
return null;
}
// 获取父类元素
typeElement = (TypeElement) ((DeclaredType)typeMirror).asElement();
if (erasedTargetNames.contains(typeElement)) {
// 如果父类元素存在则返回
return typeElement;
}
}
}
}
代码上的注释都大概说明了所做的事,现在只看关键的几个地方:

1、erasedTargetNames 保存的是使用注解的外围类元素,如 MainActivity 里有个变量 String mBindString 使用了 @BindString,那这里就会保存 MainActivity所对应的元素。这个主要在后面处理父类绑定继承的问题上需要用到,暂时不介绍这个可以先了解下即可;

2、targetClassMap
的键值对存储的是键就是上面的外围类元素,而值是一个 BindingClass,它保存了我们要生成的Java代码所有必要的数据信息,一个 BindingClass对应一个生成的Java类;

3、VerifyHelper
和 ParseHelper是我另外封装的两个帮助类,主要是把注解处理的过程进行拆分,这样看逻辑思路的时候会清晰点,我们所做的处理工作大部分都在这里面进行,注意这样拆分不是必须的;

4、最后的代码就是生产Java文件的处理了,这个也放后面说明;

先来看下 VerifyHelper.verifyResString() 做了什么:

/**
* 检验元素有效性帮助类
*/
public final class VerifyHelper {

private static final String STRING_TYPE = "java.lang.String";

private VerifyHelper() {
throw new AssertionError("No instances.");
}

/**
* 验证 String Resource
*/
public static boolean verifyResString(Element element, Messager messager) {
return _verifyElement(element, BindString.class, messager);
}

/**
* 验证元素的有效性
* @param element   注解元素
* @param annotationClass   注解类
* @param messager  提供注解处理器用来报告错误消息、警告和其他通知
* @return  有效则返回true,否则false
*/
private static boolean _verifyElement(Element element, Class<? extends Annotation> annotationClass,
Messager messager) {
// 检测元素的有效性
if (!SuperficialValidation.validateElement(element)) {
return false;
}
// 获取最里层的外围元素
TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();

if (!_verifyElementType(element, annotationClass, messager)) {
return false;
}
// 使用该注解的字段访问权限不能为 private 和 static
Set<Modifier> modifiers = element.getModifiers();
if (modifiers.contains(Modifier.PRIVATE) || modifiers.contains(Modifier.STATIC)) {
_error(messager, element, "@%s %s must not be private or static. (%s.%s)",
annotationClass.getSimpleName(), "fields", enclosingElement.getQualifiedName(),
element.getSimpleName());
return false;
}
// 包含该注解的外围元素种类必须为 Class
if (enclosingElement.getKind() != ElementKind.CLASS) {
_error(messager, enclosingElement, "@%s %s may only be contained in classes. (%s.%s)",
annotationClass.getSimpleName(), "fields", enclosingElement.getQualifiedName(),
element.getSimpleName());
return false;
}
// 包含该注解的外围元素访问权限不能为 private
if (enclosingElement.getModifiers().contains(Modifier.PRIVATE)) {
_error(messager, enclosingElement, "@%s %s may not be contained in private classes. (%s.%s)",
annotationClass.getSimpleName(), "fields", enclosingElement.getQualifiedName(),
element.getSimpleName());
return false;
}
// 判断是否处于错误的包中
String qualifiedName = enclosingElement.getQualifiedName().toString();
if (qualifiedName.startsWith("android.")) {
_error(messager, element, "@%s-annotated class incorrectly in Android framework package. (%s)",
annotationClass.getSimpleName(), qualifiedName);
return false;
}
if (qualifiedName.startsWith("java.")) {
_error(messager, element, "@%s-annotated class incorrectly in Java framework package. (%s)",
annotationClass.getSimpleName(), qualifiedName);
return false;
}

return true;
}

/**
* 验证元素类型的有效性
* @param element   元素
* @param annotationClass   注解类
* @param messager  提供注解处理器用来报告错误消息、警告和其他通知
* @return  有效则返回true,否则false
*/
private static boolean _verifyElementType(Element element, Class<? extends Annotation> annotationClass,
Messager messager) {
// 获取最里层的外围元素
TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();

// 检测使用该注解的元素类型是否正确
if (annotationClass == BindString.class) {
if (!STRING_TYPE.equals(element.asType().toString())) {
_error(messager, element, "@%s field type must be 'String'. (%s.%s)",
annotationClass.getSimpleName(), enclosingElement.getQualifiedName(),
element.getSimpleName());
return false;
}
}
// 略...

return true;
}

/**
* 输出错误信息
* @param element
* @param message
* @param args
*/
private static void _error(Messager messager, Element element, String message, Object... args) {
if (args.length > 0) {
message = String.format(message, args);
}
messager.printMessage(Diagnostic.Kind.ERROR, message, element);
}
}


我把验证过程也分为几个方法处理,因为其它注解的处理过程也基本类似,只是在 _verifyElementType() 判断类型的时候会稍微有点不同,这个后面会看到。来整理下检测的流程,我也分步骤来说吧:

1、首先调用了 SuperficialValidation.validateElement(element)来检测使用注解的元素的有效性,这个是 auto-common 提供的方法,可以看下官方说明;

2、_verifyElementType() 检测元素类型是否符合要求,因为我们在处理字符串资源的绑定,所以元素类型必须为 STRING_TYPE("java.lang.String");

3、然后就是判断外围元素种类必须为 Class 和一些访问权限的判断,因为我们后面肯定要对这些字段的值进行注入,所以需要有访问的权限;

4、最后就是判断外围元素不能处在 android. 和 java. 开头的系统包中,getQualifiedName() 取得是它完全限定名,即包含包路径的完整名称;

检测大体就这样,下面来看怎么解析注解:

/**
* 注解解析绑定帮助类
*/
public final class ParseHelper {

private ParseHelper() {
throw new AssertionError("No instances.");
}

/**
* 解析 String 资源
*
* @param element        使用注解的元素
* @param targetClassMap 映射表
* @param elementUtils   元素工具类
*/
public static void parseResString(Element element, Map<TypeElement, BindingClass> targetClassMap,
Set<TypeElement> erasedTargetNames, Elements elementUtils) {
// 获取字段名和注解的资源ID
String name = element.getSimpleName().toString();
int resId = element.getAnnotation(BindString.class).value();

BindingClass bindingClass = _getOrCreateTargetClass(element, targetClassMap, elementUtils);
// 生成资源信息
FieldResourceBinding binding = new FieldResourceBinding(resId, name, "getString");
// 给BindingClass添加资源信息
bindingClass.addResourceBinding(binding);

erasedTargetNames.add((TypeElement) element.getEnclosingElement());
}

/**
* 获取存在的 BindingClass,没有则重新生成
*
* @param element        使用注解的元素
* @param targetClassMap 映射表
* @param elementUtils   元素工具类
* @return BindingClass
*/
private static BindingClass _getOrCreateTargetClass(Element element, Map<TypeElement, BindingClass> targetClassMap,
Elements elementUtils) {
TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();
BindingClass bindingClass = targetClassMap.get(enclosingElement);
// 以下以 com.butterknife.MainActivity 这个类为例
if (bindingClass == null) {
// 获取元素的完全限定名称:com.butterknife.MainActivity
String targetType = enclosingElement.getQualifiedName().toString();
// 获取元素所在包名:com.butterknife
String classPackage = elementUtils.getPackageOf(enclosingElement).getQualifiedName().toString();
// 获取要生成的Class的名称:MainActivity$$ViewBinder
int packageLen = classPackage.length() + 1;
String className = targetType.substring(packageLen).replace('.', '$') + BINDING_CLASS_SUFFIX;
// 生成Class的完全限定名称:com.butterknife.MainActivity$$ViewBinder
String classFqcn = classPackage + "." + className;

/* 不要用下面这个来生成Class名称,内部类会出错,比如ViewHolder */
//            String className = enclosingElement.getSimpleName() + BINDING_CLASS_SUFFIX;

bindingClass = new BindingClass(classPackage, className, targetType, classFqcn);
targetClassMap.put(enclosingElement, bindingClass);
}
return bindingClass;
}
}


可以看到这里主要就是获取对应的注解字段和它的注解属性值,然后生成一个 FieldResourceBinding 对象并添加到BindingClass中。我们前面介绍了一个外围类对应一个 BindingClass,而外围类可能同时包含多个注解的,所以 BindingClass可能是存在并保存在 targetClassMap中,这时我们直接去获取就行了。现在只要了解 FieldResourceBinding和 BindingClass的用法整个流程就通了,先来看下 FieldResourceBinding:

/**
* 资源信息
*/
public final class FieldResourceBinding {

// 资源ID
private final int id;
// 字段变量名称
private final String name;
// 获取资源数据的方法
private final String method;

public FieldResourceBinding(int id, String name, String method) {
this.id = id;
this.name = name;
this.method = method;
}

public int getId() {
return id;
}

public String getName() {
return name;
}

public String getMethod() {
return method;
}
}


这个还是好理解,id 和 name都好理解,前面我们也通过注解获取了,至于 method我们现在取的是String资源所以传入"getString",这个等下在 BindingClass中就会用到。下面来看 BindingClass的实现:

/**
* 绑定处理类,一个 BindingClass 对应一个要生成的类
*/
public final class BindingClass {

private static final ClassName FINDER = ClassName.get("com.dl7.butterknifelib", "Finder");
private static final ClassName VIEW_BINDER = ClassName.get("com.dl7.butterknifelib", "ViewBinder");
private static final ClassName CONTEXT = ClassName.get("android.content", "Context");
private static final ClassName RESOURCES = ClassName.get("android.content.res", "Resources");

private final List<FieldResourceBinding> resourceBindings = new ArrayList<>();
private final String classPackage;
private final String className;
private final String targetClass;
private final String classFqcn;

/**
* 绑定处理类
*
* @param classPackage 包名:com.butterknife
* @param className    生成的类:MainActivity$$ViewBinder
* @param targetClass  目标类:com.butterknife.MainActivity
* @param classFqcn    生成Class的完全限定名称:com.butterknife.MainActivity$$ViewBinder
*/
public BindingClass(String classPackage, String className, String targetClass, String classFqcn) {
this.classPackage = classPackage;
this.className = className;
this.targetClass = targetClass;
this.classFqcn = classFqcn;
}

/**
* 生成Java类
*
* @return JavaFile
*/
public JavaFile brewJava() {
// 构建一个类
TypeSpec.Builder result = TypeSpec.classBuilder(className)
.addModifiers(Modifier.PUBLIC)
.addTypeVariable(TypeVariableName.get("T", ClassName.bestGuess(targetClass)));

if (_hasParentBinding()) {
// 有父类则继承父类
result.superclass(ParameterizedTypeName.get(ClassName.bestGuess(parentBinding.classFqcn),
TypeVariableName.get("T")));
} else {
// 实现 ViewBinder 接口
result.addSuperinterface(ParameterizedTypeName.get(VIEW_BINDER, TypeVariableName.get("T")));
}

// 添加方法
result.addMethod(_createBindMethod());
// 构建Java文件
return JavaFile.builder(classPackage, result.build())
.addFileComment("Generated code from Butter Knife. Do not modify!")
.build();
}

/**
* 创建方法
*
* @return MethodSpec
*/
private MethodSpec _createBindMethod() {
// 定义一个方法,其实就是实现 ViewBinder 的 bind 方法
MethodSpec.Builder result = MethodSpec.methodBuilder("bind")
.addAnnotation(Override.class)
.addModifiers(Modifier.PUBLIC)
.addParameter(FINDER, "finder", Modifier.FINAL)
.addParameter(TypeVariableName.get("T"), "target", Modifier.FINAL)
.addParameter(Object.class, "source");

if (_hasParentBinding()) {
// 调用父类的bind()方法
result.addStatement("super.bind(finder, target, source)");
}

if (_hasResourceBinding()) {
// 过滤警告
result.addAnnotation(AnnotationSpec.builder(SuppressWarnings.class)
.addMember("value", "$S", "ResourceType")
.build());

result.addStatement("$T context = finder.getContext(source)", CONTEXT);
result.addStatement("$T res = context.getResources()", RESOURCES);
// Resource
for (FieldResourceBinding binding : resourceBindings) {
result.addStatement("target.$L = res.$L($L)", binding.getName(), binding.getMethod(),
binding.getId());
}
}

return result.build();
}

/**
* 添加资源
*
* @param binding 资源信息
*/
public void addResourceBinding(FieldResourceBinding binding) {
resourceBindings.add(binding);
}

private boolean _hasResourceBinding() {
return !(resourceBindings.isEmpty() && colorBindings.isEmpty());
}
}
这里只列了必要的步骤,其实整个过程就是用 javapoet 生成Java类,和我们注解相关的最主要是这句话:

result.addStatement("target.$L = res.$L($L)", binding.getName(), binding.getMethod(), binding.getId());

我们调用对应的方法(getMethod)并传入对应的ID(getId)来设置对应的字段(getName),和我们平常写句子的逻辑是一样的。关于javapoet的详细用法可以参考官方示例。

在处理的过程中我们还进行了父类继承的处理,为什么要进行这个处理呢?举个简单的例子,我们有一个类B,它有直接父类A,它们都有使用我们定义的注解,正常我们类B是可以调用类A中的非私有域,所以我们在执行类B的bind()方法时要先执行父类的bind()方法,即super.bind()。

到这里整个流程就完成了,你再回头看看注解处理器最后生成Java文件的代码应该能理解是怎么回事了,现在在代码中应该也能正常使用了,使用代码我就不贴了,看下文章最后给的例子源码就行了。

@BindColor

接下来看下 @BindColor注解怎么处理,其实大体流程都是一样的,先定义个注解:

/**
* 绑定颜色资源
*/
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface BindColor {
@ColorRes int value();
}


同样的,这边用了 @ColorRes 来限定注解的属性值只能为 R.color.xxx。

再看下注解处理器:

@AutoService(Processor.class)
public class ButterKnifeProcessor extends AbstractProcessor {

@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
// 略...

// 处理BindColor
for (Element element : roundEnv.getElementsAnnotatedWith(BindColor.class)) {
if (VerifyHelper.verifyResColor(element, messager)) {
ParseHelper.parseResColor(element, targetClassMap, erasedTargetNames, elementUtils);
}
}
// 略...
}

@Override
public Set<String> getSupportedAnnotationTypes() {
Set<String> annotations = new LinkedHashSet<>();
annotations.add(BindString.class.getCanonicalName());
annotations.add(BindColor.class.getCanonicalName());
return annotations;
}
}


和刚才的注解处理基本一致,主要来看下怎么检查和解析注解,注意要在 getSupportedAnnotationTypes() 中指明要处理这个注解。检测如下:

public final class VerifyHelper {

private static final String COLOR_STATE_LIST_TYPE = "android.content.res.ColorStateList";

/**
* 验证 Color Resource
*/
public static boolean verifyResColor(Element element, Messager messager) {
return _verifyElement(element, BindColor.class, messager);
}

private static boolean _verifyElement(Element element, Class<? extends Annotation> annotationClass,
Messager messager) {
// 和 @BindString 一致
}

private static boolean _verifyElementType(Element element, Class<? extends Annotation> annotationClass,
Messager messager) {
// 获取最里层的外围元素
TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();
if (annotationClass == BindColor.class) {
if (COLOR_STATE_LIST_TYPE.equals(element.asType().toString())) {
return true;
} else if (element.asType().getKind() != TypeKind.INT) {
_error(messager, element, "@%s field type must be 'int' or 'ColorStateList'. (%s.%s)",
BindColor.class.getSimpleName(), enclosingElement.getQualifiedName(),
element.getSimpleName());
return false;
}
}

return true;
}
}


检测主要看元素类型的检测,其它的和上一个注解一致,在这里我们定义的注解可能会返回两种情况:一种是我们正常使用的颜色资源,返回 int 类型;还有一种是根据状态变化的颜色选择器,返回 COLOR_STATE_LIST_TYPE = "android.content.res.ColorStateList",由 <selector> 标签定义的一组颜色值。

下面看对注解的解析:

public final class ParseHelper {

/**
* 解析 String 资源
*
* @param element        使用注解的元素
* @param targetClassMap 映射表
* @param elementUtils   元素工具类
*/
public static void parseResColor(Element element, Map<TypeElement, BindingClass> targetClassMap,
Set<TypeElement> erasedTargetNames, Elements elementUtils) {
// 获取字段名和注解的资源ID
String name = element.getSimpleName().toString();
int resId = element.getAnnotation(BindColor.class).value();

BindingClass bindingClass = _getOrCreateTargetClass(element, targetClassMap, elementUtils);
// 生成资源信息
FieldColorBinding binding;
if (COLOR_STATE_LIST_TYPE.equals(element.asType().toString())) {
binding = new FieldColorBinding(resId, name, "getColorStateList");
} else {
binding = new FieldColorBinding(resId, name, "getColor");
}
// 给BindingClass添加资源信息
bindingClass.addColorBinding(binding);

erasedTargetNames.add((TypeElement) element.getEnclosingElement());
}
}


还是和 @BindString 基本一致,主要多了对 COLOR_STATE_LIST_TYPE
的处理,这里分别设置不同的 Method 来获取数据。这边的 FieldColorBinding 和 FieldResourceBinding 的字段其实都是一样的,我把它们分开是后面生成Java文件的时候好判断,定义如下:

/**
* 资源 Color 绑定信息
*/
public final class FieldColorBinding {

private final int id;
private final String name;
private final String method;

public FieldColorBinding(int id, String name, String method) {
this.id = id;
this.name = name;
this.method = method;
}

public int getId() {
return id;
}

public String getName() {
return name;
}

public String getMethod() {
return method;
}
}


最后看下 BindingClass 的处理,只给出变化的部分:

public final class BindingClass {

private static final ClassName CONTEXT = ClassName.get("android.content", "Context");
private static final ClassName RESOURCES = ClassName.get("android.content.res", "Resources");
private static final ClassName CONTEXT_COMPAT = ClassName.get("android.support.v4.content", "ContextCompat");

private final List<FieldColorBinding> colorBindings = new ArrayList<>();

private MethodSpec _createBindMethod() {
// 略...
if (_hasResourceBinding()) {
// 略...

// ClassResource
for (FieldColorBinding binding : colorBindings) {
result.addStatement("target.$L = $T.$L(context, $L)", binding.getName(), CONTEXT_COMPAT,
binding.getMethod(), binding.getId());
}
}
}

public void addColorBinding(FieldColorBinding binding) {
colorBindings.add(binding);
}
}
可以看到在获取颜色的时候用了 v4 包中的 ContextCompat 类来处理,这是一个兼容类,因为在SDK≥23 的时候获取颜色会需要传入一个 Resources.Theme,用兼容包的话会统一帮我们处理,这个和源码处理不一样,但效果是相似的。

到这里 @BindColor 也可以使用了,最后在代码中使用如下:

public class MainActivity extends AppCompatActivity {

@BindString(R.string.activity_string)
String mBindString;
@BindColor(R.color.colorAccent)
int mBindColor;
@BindColor(R.color.sel_btn_text)
ColorStateList mBtnTextColor;

// 略...
}


看下注解处理器帮我们生成的代码:

public class MainActivity$$ViewBinder<T extends MainActivity> implements ViewBinder<T> {
@Override
@SuppressWarnings("ResourceType")
public void bind(final Finder finder, final T target, Object source) {
Context context = finder.getContext(source);
Resources res = context.getResources();
target.mBindString = res.getString(2131099669);
target.mBindColor = ContextCompat.getColor(context, 2131427346);
target.mBtnTextColor = ContextCompat.getColorStateList(context, 2131427399);
}
}


可以看到应该是正常的,根据这些代码你再回想一下关于检测的权限、代码的生成过程应该有一个更好的认识。

关于其它资源绑定注解其实实现都和这两个类似,这方面我就不再说明了,下一个就到了最常用的 @Bind 绑定 View 视图的处理。

例子代码:ButterKnifeStudy
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息