相关文章:
周末两天早起看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