Android编译时注解初级之ButterKnife
本文的主要目的在于了解编译时注解,并能初步运用。代码在最后。
1.编译时注解 VS 运行时注解
1.1 运行时注解
这种注解在运行时,依赖反射,获得需要的信息,比如:
@SyntaxRun(R.id.id_text)
public Button mText;
...
private void processAnnotations() {
Field[] fields = this.getClass().getDeclaredFields();
for (Field f : fields) {
SyntaxRun sr = f.getAnnotation(SyntaxRun.class);
if (sr == null) {
continue;
}
int id = sr.value();
if (id == -1) {
continue;
}
try {
f.set(this, findViewById(id));
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
这样,就可以实现自动findview了。但是这样会在初始化的时侯,依赖反射去找到,并且设置,这一过程又会导致性能上的损失。
1.2 编译时注解
顾名思义,这个注解是在编译的时侯生效,那么唯一的做法就是帮我们生成那些findview的类。先看一个例子,后面再看完成代码,比如:
private void processGen(RoundEnvironment roundEnv) {
Set<? extends Element> elements =
roundEnv.getElementsAnnotatedWith(SyntaxGen.class);
String enclosingClass = null;
List<FieldDesc> views = new ArrayList<>();
for (Element each : elements) {
Element enclosingElement = each.getEnclosingElement();
JCTree tree = (JCTree) mTrees.getTree(enclosingElement);
//tree.accept(new DeprecatedTranslator(roundEnv, each, mTreeMaker, mMethodHelper, mElementUtils));
if (!(each instanceof Symbol.VarSymbol)) {
continue;
}
Symbol.VarSymbol symbol = (Symbol.VarSymbol) each;
String viewClass = symbol.asType().toString();
System.out.println("each : " + viewClass);
System.out.println("name : " + each.getSimpleName());
System.out.println("tree enclosing : " + enclosingElement);
//1.拿到资源id
int resId = each.getAnnotation(SyntaxGen.class).value();
if (enclosingClass == null) {
enclosingClass = enclosingElement.toString();
System.out.println("class : " + enclosingClass);
}
views.add(new FieldDesc(viewClass, each.getSimpleName().toString(), resId));
}
//2. 生成代码
if (enclosingClass != null) {
generateClass(enclosingClass, views);
}
}
先粗略看一下编译时注解,后面会解释。要做的就是通过注解拿到resid,然后生成代码帮我们绑定。
与运行时注解相比,只是实现方式不同罢了。
2.ButterKnife
早期版本的BK也是运行时注解,也是后来改成了编译时注解。
现在分析一下这个库的实现,分析开源库首先抓它的核心,不用太在意细枝末节。
依我们的用途,无非就是不再频繁的写findview,setonclick等,那么BK的BindView注解就是我们最常用的,先看一下,它时如何实现的。
看ButterKnifeProcessor.java这个类,以BindView为例,其他都类似的。
1.查找注解修饰的对象
从process方法开始分析
@Override public boolean process(Set<? extends TypeElement> elements, RoundEnvironment env) {
//1.获取注解的信息等
Map<TypeElement, BindingSet> bindingMap = findAndParseTargets(env);
for (Map.Entry<TypeElement, BindingSet> entry : bindingMap.entrySet()) {
TypeElement typeElement = entry.getKey();
BindingSet binding = entry.getValue();
//2.生成java类文件
JavaFile javaFile = binding.brewJava(sdk, debuggable, useAndroidX);
try {
javaFile.writeTo(filer);
} catch (IOException e) {
error(typeElement, "Unable to write binding for type %s: %s", typeElement, e.getMessage());
}
}
return false;
}
两步走:
一,找注解拿信息
二,生成java类文件。
private Map<TypeElement, BindingSet> findAndParseTargets(RoundEnvironment env) {
...
// 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 {
System.out.println("BindView Element : " + element);
parseBindView(element, builderMap, erasedTargetNames);
} catch (Exception e) {
logParsingError(element, BindView.class, e);
}
}
...
}
2.解析找到的element,element是编译时的对象,比如一个变量,一个类等。
主要看核心的点。
private void parseBindView(Element element, Map<TypeElement, BindingSet.Builder> builderMap,
Set<TypeElement> erasedTargetNames) {
TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();
// Start by verifying common generated code restrictions.
boolean hasError = isInaccessibleViaGeneratedCode(BindView.class, "fields", element)
|| isBindingInWrongPackage(BindView.class, element);
// Verify that the target type extends from View.
TypeMirror elementType = element.asType();
if (elementType.getKind() == TypeKind.TYPEVAR) {
TypeVariable typeVariable = (TypeVariable) elementType;
elementType = typeVariable.getUpperBound();
}
Name qualifiedName = enclosingElement.getQualifiedName();
Name simpleName = element.getSimpleName();
...错误判断
// Assemble information on the field.
//拿到id
int id = element.getAnnotation(BindView.class).value();
//1.得到一个Builder
BindingSet.Builder builder = builderMap.get(enclosingElement);
Id resourceId = elementToId(element, BindView.class, id);
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);
}
//2.拿到名字
String name = simpleName.toString();
//拿到变量的类型
TypeName type = TypeName.get(elementType);
boolean required = isFieldRequired(element);
//3.添加到Builder
builder.addField(resourceId, new FieldViewBinding(name, type, required));
// Add the type-erased version to the valid binding targets set.
erasedTargetNames.add(enclosingElement);
}
2.1 获取一个Builder,没有则创建。
参考如下创建builder,可以看出,平时用BK时,编译过后生成的类就是在这准备的。
比如:
ClassName bindingClassName = ClassName.get(packageName, className + "_ViewBinding");
如果在MainActivity中使用,那么就有 MainActivity_ViewBinding.java
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);
}
2.2 拿到名字和类型等信息。
2.3 添加到Builder中。
这样,我们的信息就准备好了。然后在ButterKnifeProcessor的process中看到
JavaFile javaFile = binding.brewJava(sdk, debuggable, useAndroidX);
会生成一个文件,写入到编译路径里面,那么写了什么呢?
按之前我们分析的BindView的路线,继续往下看。
最终,我们看到会进入如下的方法里面:
这里就比较熟悉了,最终生成了 target.view = source.findViewById(id)这样的语句。
!注意,用的是target.view这种直接调用的方式,这也是变量必须不能是private的原因。
现在这里出现了一些$L $T 等这些东西,是什么呢?这里用了生成java文件的一个库:JavaPoet , github自行搜索,后面也会用到。
private void addViewBinding(MethodSpec.Builder result, ViewBinding binding, boolean debuggable) {
if (binding.isSingleFieldBinding()) {
// Optimize the common case where there's a single binding directly to a field.
FieldViewBinding fieldBinding = binding.getFieldBinding();
CodeBlock.Builder builder = CodeBlock.builder()
.add("target.$L = ", fieldBinding.getName());
boolean requiresCast = requiresCast(fieldBinding.getType());
if (!debuggable || (!requiresCast && !fieldBinding.isRequired())) {
if (requiresCast) {
builder.add("($T) ", fieldBinding.getType());
}
builder.add("source.findViewById($L)", binding.getId().code);
} else {
builder.add("$T.find", UTILS);
builder.add(fieldBinding.isRequired() ? "RequiredView" : "OptionalView");
if (requiresCast) {
builder.add("AsType");
}
builder.add("(source, $L", binding.getId().code);
if (fieldBinding.isRequired() || requiresCast) {
builder.add(", $S", asHumanDescription(singletonList(fieldBinding)));
}
if (requiresCast) {
builder.add(", $T.class", fieldBinding.getRawType());
}
builder.add(")");
}
result.addStatement("$L", builder.build());
return;
}
List<MemberViewBinding> requiredBindings = binding.getRequiredBindings();
if (!debuggable || requiredBindings.isEmpty()) {
result.addStatement("view = source.findViewById($L)", binding.getId().code);
} else if (!binding.isBoundToRoot()) {
result.addStatement("view = $T.findRequiredView(source, $L, $S)", UTILS,
binding.getId().code, asHumanDescription(requiredBindings));
}
addFieldBinding(result, binding, debuggable);
addMethodBindings(result, binding, debuggable);
}
3.实现简单版的ButterKnife
3.1 呃呃呃,先去学习一下JavaPoet怎么用......
3.2 创建编译时注解(完整代码见后面)
1.创建类
public class SyntaxProcessor extends AbstractProcessor {
...
}
2.添加到路径
新建文件 resources/META-INF/services/javax.annotation.processing.Processor
并将SyntaxProcessor全路径名写在里面。
3.3 处理生成代码(按JavaPoet的形式生成)
private void generateClass(String originClass, List<FieldDesc> views) {
String pkg = originClass.substring(0, originClass.lastIndexOf("."));
String originClassName = originClass.substring(originClass.lastIndexOf(".") + 1);
String newClass = originClassName + "_Bind";
MethodSpec main = MethodSpec.methodBuilder("main")
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
.returns(void.class)
.addParameter(String[].class, "args")
.addStatement("$T.out.println($S)", System.class, "Hello, JavaPoet!")
.build();
ClassName activityClass = ClassName.get(pkg, originClassName);
FieldSpec activity = FieldSpec.builder(activityClass, "mA", Modifier.PRIVATE)
.addModifiers(Modifier.PRIVATE, Modifier.PRIVATE)
.build();
List<FieldSpec> specs = new ArrayList<>();
List<IdSt> sts = new ArrayList<>();
for (FieldDesc desc : views) {
String val = desc.viewClass;
String pre = getPre(val);
String suffix = getSuffix(val);
ClassName viewClass = ClassName.get(pre, suffix);
FieldSpec viewDef = FieldSpec.builder(viewClass, "mView_" + desc.id, Modifier.PUBLIC)
//.addModifiers(Modifier.PRIVATE, Modifier.PRIVATE)
.build();
specs.add(viewDef);
String st0 = "$N = $N.$N = this.$N.findViewById($L)";
String stPre = "mView_" + desc.id;
String st1 = "mA";
String st2 = desc.fieldName;
String st3 = "mA";
int st4 = desc.id;
sts.add(new IdSt(st0, stPre, st1, st2, st3, st4));
}
MethodSpec.Builder constructor = MethodSpec.constructorBuilder()
.addModifiers(Modifier.PUBLIC)
.addParameter(activityClass, "activity")
.addStatement("this.$N = $N", "mA", "activity");
for (IdSt st : sts) {
constructor.addStatement(st.st0, st.stPre, st.st1, st.st2, st.st3, st.st4);
}
TypeSpec.Builder builder = TypeSpec.classBuilder(newClass)
.addModifiers(Modifier.PUBLIC, Modifier.FINAL)
.addMethod(constructor.build())
.addMethod(main)
.addField(activity);
for (FieldSpec spec : specs) {
builder.addField(spec);
}
JavaFile javaFile = JavaFile.builder(pkg, builder.build())
.build();
try {
javaFile.writeTo(System.out);
javaFile.writeTo(processingEnv.getFiler());
} catch (IOException e) {
e.printStackTrace();
}
}
生成后的如下:
package com.syntax.javapoet;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView;
import java.lang.String;
import java.lang.System;
public final class MainActivity_Bind {
private MainActivity mA;
public Button mView_2131165252;
public TextView mView_2131165251;
public ImageView mView_2131165250;
public MainActivity_Bind(MainActivity activity) {
this.mA = activity;
mView_2131165252 = mA.mText = this.mA.findViewById(2131165252);
mView_2131165251 = mA.mTextView = this.mA.findViewById(2131165251);
mView_2131165250 = mA.mImage = this.mA.findViewById(2131165250);
}
public static void main(String[] args) {
System.out.println("Hello, JavaPoet!");
}
}
基本功能已经可以实现,想实现其他功能,再生成即可。
因一些原因,无法给出代码仓库,看这个吧。
代码见 https://pan.baidu.com/s/1oEjXjDiYehM-eIpg1D_TfQ