上篇博客写的是源码分析,但是并没有涉及到apt;这次就来看看使用apt如何写一个ButterKnife;
Part1 注解基础
如果没用过注解或者用的少,推荐下看下http://blog.csdn.net/briblue/article/details/73824058
Part2 apt实现ButterKnife
使用Annotation Processing Tool实现的好处就是没有性能损耗,因为没有用反射,只是在编译时解析注解生成需要的class文件。
步骤:
1.新建名为annotation的Java Library,
annotation的build.gradle:
apply plugin: 'java'
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
}
sourceCompatibility = "1.7"
targetCompatibility = "1.7"
定义两个注解:
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Bind {
int id() default -1;
}
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface OnClick {
/** View IDs to which the method will be bound. */
int[] value() default {-1};
}
2.新建名为compiler的Java Library
compiler的build.gradle:
apply plugin: 'java'
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
compile 'com.google.auto.service:auto-service:1.0-rc2'
compile 'com.squareup:javapoet:1.7.0'
compile project(':annotation')
}
sourceCompatibility = "1.7"
targetCompatibility = "1.7"
配置项目根目录的build.gradle
dependencies {
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
}
定义一个ButterKnifeProcessor 继承AbstractProcessor;这里参考了最新版本的ButterKnife源码,不过里面的逻辑很复杂容易把人绕晕。所以我根据自己的理解,写了个更简单易懂的。
重写process方法,依据bindingMap 结果遍历生成多个以_ViewBinding为结尾的类文件:
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment env) {
Map<ClassName, BindingSet> bindingMap = findAndParseTargets(env);
for (Map.Entry<ClassName, BindingSet> entry : bindingMap.entrySet()) {//根据ClassName取得BindingSet,生成多个文件;
ClassName className = entry.getKey();
BindingSet binding = entry.getValue();
TypeSpec.Builder
result = TypeSpec.classBuilder(binding.bindingClassName.simpleName() + "_ViewBinding")
.addModifiers(PUBLIC)
.addSuperinterface(UNBINDER)
.addField(binding.targetTypeName, "target", PRIVATE)
.addMethod(createBindingConstructorForActivity(binding))
.addMethod(createBindingConstructorForActivity2(binding))
.addMethod(createBindingUnbindMethod(binding));
for (Element element : binding.elementClicks) {//添加点击view的成员变量;
int[] ss = element.getAnnotation(OnClick.class).value();
for (int s : ss) {
result.addField(VIEW, "view" + s, PRIVATE);
}
}
JavaFile javaFile = JavaFile.builder(binding.bindingClassName.packageName(), result.build())
.addFileComment("Generated code from Butter Knife. Do not modify!")
.build();
try {
javaFile.writeTo(processingEnv.getFiler());
} catch (IOException e) {
e.printStackTrace();
}
}
return false;
}
BindingSet :依据类名,保存需要绑定的view和click集合。
public class BindingSet {
public TypeName targetTypeName;
public ClassName bindingClassName;
public Set<Element> elements = new HashSet<>();
public Set<Element> elementClicks = new HashSet<>();
}
取出所有的注解,依据所在的类名保存到bindingMap :
private Map<ClassName, BindingSet> findAndParseTargets(RoundEnvironment env) {//以ClassName为key,BindingSet为value保存结果
LinkedHashMap<ClassName, BindingSet> map = new LinkedHashMap<ClassName, BindingSet>();
Set<? extends Element> elements = env.getElementsAnnotatedWith(Bind.class);
Set<? extends Element> elementClicks = env.getElementsAnnotatedWith(OnClick.class);
for (Element element : elements) {
TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();
TypeMirror typeMirror = enclosingElement.asType();
TypeName targetTypeName = TypeName.get(typeMirror);
if (targetTypeName instanceof ParameterizedTypeName) {
targetTypeName = ((ParameterizedTypeName) targetTypeName).rawType;
}
String packageName = getPackage(enclosingElement).getQualifiedName().toString();
String className = enclosingElement.getQualifiedName().toString().substring(packageName.length() + 1).replace('.', '$');
ClassName bindingClassName = ClassName.get(packageName, className);
if (map.keySet().contains(bindingClassName)) {
BindingSet bindingSet = map.get(bindingClassName);
bindingSet.elements.add(element);
bindingSet.targetTypeName = targetTypeName;
bindingSet.bindingClassName = bindingClassName;
} else {
BindingSet bindingSet = new BindingSet();
bindingSet.elements.add(element);
bindingSet.targetTypeName = targetTypeName;
bindingSet.bindingClassName = bindingClassName;
map.put(bindingClassName, bindingSet);
}
}
for (Element click : elementClicks) {
TypeElement enclosingElement = (TypeElement) click.getEnclosingElement();
TypeMirror typeMirror = enclosingElement.asType();
TypeName targetTypeName = TypeName.get(typeMirror);
if (targetTypeName instanceof ParameterizedTypeName) {
targetTypeName = ((ParameterizedTypeName) targetTypeName).rawType;
}
String packageName = getPackage(enclosingElement).getQualifiedName().toString();
String className = enclosingElement.getQualifiedName().toString().substring(packageName.length() + 1).replace('.', '$');
ClassName bindingClassName = ClassName.get(packageName, className);
if (map.keySet().contains(bindingClassName)) {
BindingSet bindingSet = map.get(bindingClassName);
bindingSet.elementClicks.add(click);
bindingSet.targetTypeName = targetTypeName;
bindingSet.bindingClassName = bindingClassName;
} else {
BindingSet bindingSet = new BindingSet();
bindingSet.elementClicks.add(click);
bindingSet.targetTypeName = targetTypeName;
bindingSet.bindingClassName = bindingClassName;
map.put(bindingClassName, bindingSet);
}
}
return map;
}
使用javapoet生成代码:findView和设置onclick等;
取值替换需要用到以下字符,有点类似于String.format:
$L for Literals
$S for Strings
$T for Types 类型,用了会自动import;
$N for Names(我们自己生成的方法名或者变量名等等)
private MethodSpec createBindingConstructorForActivity2(BindingSet binding) {
MethodSpec.Builder builderConstructor2 = MethodSpec.constructorBuilder()
.addModifiers(PUBLIC)
.addParameter(binding.targetTypeName, "target", Modifier.FINAL)
.addParameter(VIEW, "source")
.addStatement("this.target = target")
.addStatement("View view");
for (Element element : binding.elements) {
builderConstructor2.addStatement("target.$L = $T.findRequiredViewAsType(source, R.id.$L, \"field '$L'\", $T.class)", element.getSimpleName(), UTILS, element.getSimpleName(), element.getSimpleName(), getRawType(element));
}
for (Element element : binding.elementClicks) {
int[] ss = element.getAnnotation(OnClick.class).value();
for (int s : ss) {
builderConstructor2
.addStatement("view = Utils.findRequiredView(source, $L)", s)
.addStatement("view$L = view", s)
.addStatement("view.setOnClickListener(new View.OnClickListener() {\n" +
" @Override\n" +
" public void onClick(View v) {\n" +
" target.onClick(v);\n" +
" }\n" +
" })");
}
}
return builderConstructor2.build();
}
主要的apt相关的代码就是上面这些,完成这些后再project build后就会自动生成绑定用到的java代码;
public class ButterKnife {
public static final String TAG = "ButterKnife";
public static Unbinder bind(Activity target) {
View sourceView = target.getWindow().getDecorView();
return createBinding(target, sourceView);
}
private static Unbinder createBinding(Object target, View source) {
Class<?> targetClass = target.getClass();
Constructor<? extends Unbinder> constructor = findViewBinder(targetClass);
try {
return constructor.newInstance(target, source);//这里调用了2个参数的构造方法。生成实例并且绑定了对象;
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
return null;
}
private static Constructor<? extends Unbinder> findViewBinder(Class<?> cls) {
String clsName = cls.getName();
try {
Class<?> bindingClass = cls.getClassLoader().loadClass(clsName + "_ViewBinding");//查找到apt生成的java文件,加载;
return (Constructor<? extends Unbinder>) bindingClass.getConstructor(cls, View.class);
} catch (ClassNotFoundException e) {
Log.e(TAG, "ClassNotFoundException Not found.");
} catch (NoSuchMethodException e) {
throw new RuntimeException("Unable to find binding constructor for " + clsName, e);
}
return null;
}
}