ButterKnife 是一个常用的第三方库,它在编译期间,使用注解处理器解析注解,并生成样板代码,从而达到给 Android views 绑定的效果,从而简化了我们写的一些样板代码。
为了了解 ButterKnife 的原理,我自己模仿写了一个库,几乎可以以假乱真,当然,这个库只有学习参考的价值。
既然是模仿,先看下 ButterKnife 使用 @BindString 注解的例子。
首先,参照 github 上 ButterKnife 的 README,添加依赖
compile 'com.jakewharton:butterknife:8.8.1'
annotationProcessor 'com.jakewharton:butterknife-compiler:8.8.1'
有没有想过,为何要添加2个库,并且添加库的方式还不一样?这个问题后面会给出解释。
在 MainActivity 中使用 @BindString
public class MainActivity extends AppCompatActivity {
@BindString(R.string.app_name)
String mAppName;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 用 ButterKnife 绑定 Activity
ButterKnife.bind(this);
}
}
代码中,@BindString(R.string.app_name)
其实只是为了在编译期间生成样板代码,而真正的为 mAppName 赋值的其实是 ButterKnife.bind(this)
完成的,所以这两步缺一不可。
那么,现在不要急着运行项目,先编译一下,也就是 Android Studio 中的 Build -> Rebuild Project,编译完成后会生成一个 MainActivity_ViewBinding 的类,路径为 app\build\generated\source\apt\debug\ ,如下图
// Generated code from Butter Knife. Do not modify!
package com.ckt.customandroidannotation;
import android.content.Context;
import android.content.res.Resources;
import android.support.annotation.CallSuper;
import android.support.annotation.UiThread;
import android.view.View;
import butterknife.Unbinder;
import java.lang.Deprecated;
import java.lang.Override;
public class MainActivity_ViewBinding implements Unbinder {
@UiThread
public MainActivity_ViewBinding(MainActivity target) {
this(target, target);
}
@UiThread
public MainActivity_ViewBinding(MainActivity target, Context context) {
Resources res = context.getResources();
target.mAppName = res.getString(R.string.app_name);
}
}
这个代码我精简过,而且这个类简直毫无PS痕迹,这明明就是 Android Project 中的创建的一个 Java 类。
Now,你是否按耐不住内中的冲动,想要搞清楚这他妈的代码是怎么生成的,FOLLOW ME ~
注解处理器(Annotation Processor),是在编译期间处理注解的,所以它并不影响程序的性能。在普通的 Java Project 中可以很轻松的自定义一个注解处理器,但是在 Android Project,并不能为 JavaCompiler 定义一个注解处理器,但是 Android Project 可以引用一个 Java Project 作为 library,从而使用整个 library 中的自定义注解处理器来编译代码。
图中,android-annotation 和 android-compiler 为 Java Library ,mybutterknife 为 Android Library,app 为 Android Project.
首先创建一个 android-annotation 的 Java Library,用来自定义注解,可能有人不知道怎么创建 Java Library,所以我用2个图来表示创建过程,后面创建 android-compiler 的 Java Library 也是一样。
现在,在 android-annotation 中创建一个自定义的注解,搞直接点,就叫做 BindString
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.CLASS)
public @interface BindString {
int value();
}
如代码所示,BindString 这个注解的目标是 FIELD,也就是成员变量,这个注解可以在 CLASS 文件中保留,当然也可以改为在只在源码中保留,因为这并不影响在编译时期获取注解。
现在,再来创建一个名为 android-compiler 的 Java Library,并在这个库中添加一个注解处理器的实现类,由于注解处理器要指定处理哪个注解,因此,android-compiler 要把 android-annotation 作为库引入进来。
那么,在 android-compiler 的 build.gradle 中引入
dependencies {
compile fileTree(include: ['*.jar'], dir: 'libs')
compile project(':android-annotation')
}
当然,你也可以在 android-compiler 这个项目上,按 F4,然后通过添加 Module Dependency 把 android-annotation 添加进来。
现在,通过继承 AbstractProcessor 来自定义一个注解处理器
public class BindViewProcessor extends AbstractProcessor {
@Override
public boolean process(Set extends TypeElement> set, RoundEnvironment roundEnvironment) {
return false;
}
}
process() 方法,是必须要重写的方法,另外一般还要重写三个方法
而 SupportedAnnotationTypes() 和 SupportedSourceVersion() 方法在 jdk 1.7 后,可以通过注解的方式来替代。
@SupportedAnnotationTypes("com.example.BindString")
@SupportedSourceVersion(SourceVersion.RELEASE_7)
public class BindViewProcessor extends AbstractProcessor {
// 用于解析 Element
private Elements mElementUtils;
// 用于创建文件
private Filer mFiler;
@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
mElementUtils = processingEnvironment.getElementUtils();
mFiler = processingEnvironment.getFiler();
}
@Override
public boolean process(Set extends TypeElement> set, RoundEnvironment roundEnvironment) {
for (Element element : roundEnvironment.getElementsAnnotatedWith(BindString.class)) {
// 如果注解不是应用在成员上,就过滤
if (element.getKind() != ElementKind.FIELD) {
continue;
}
// 获取注解所在类的 Element
Element enclosingElement = element.getEnclosingElement();
// 获取注解所在类的 Name
Name enclosingElementName = enclosingElement.getSimpleName();
// 生成类的名字
String clsName = enclosingElementName.toString() + "_BindString";
// 获取成员变量的名字
String fieldName = element.getSimpleName().toString();
int value = element.getAnnotation(BindString.class).value();
// 获取包的 Element
PackageElement packageName = mElementUtils.getPackageOf(element);
// 创建要生成的代码的字符串
String content = getContentFromJava(value);
try {
// 创建文件
JavaFileObject javaFileObject = mFiler.createSourceFile(packageName + "." + clsName);
Writer writer = javaFileObject.openWriter();
// 把内容写入到文件中
writer.write(content);
writer.flush();
writer.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return true;
}
private String getContentFromJava(int id){
StringBuilder sb = new StringBuilder();
sb.append("package com.ckt.custombutterknife;\n\n");
sb.append("import android.content.res.Resources;\n\n");
sb.append("public class MainActivity_BindString {\n\n");
sb.append("public MainActivity_BindString(MainActivity target) {\n");
sb.append(" Resources res = target.getResources();\n");
sb.append(" target.mAppName = res.getString(").append(id).append(");\n");
sb.append("}\n");
sb.append("}");
return sb.toString();
}
}
getContentFromJava() 方法是用 StringBuilder 来创建生成的代码字符串,然而,这种方式实在是违背了程序员的“懒惰”精神,那么可以使用 Square 公司开源的 JavaPoet 库,在 build.gradle 中引入
dependencies {
compile fileTree(include: ['*.jar'], dir: 'libs')
compile project(':android-annotation')
compile 'com.squareup:javapoet:1.9.0'
}
那么现在用 JavaPoet 库来创建样板代码
private String getContentFromJavaPoet(Name enclosingElementName, String clsName, String fieldName, int value, PackageElement packageName) {
ClassName resource = ClassName.get("android.content.res", "Resources");
ClassName mainActivity = ClassName.get(packageName.toString(), enclosingElementName.toString());
MethodSpec constructor = MethodSpec.constructorBuilder()
.addModifiers(Modifier.PUBLIC)
.addParameter(mainActivity, "target")
.addStatement("$T res = target.getResources()", resource)
.addStatement("target.$L = res.getString($L)", fieldName, value)
.build();
TypeSpec typeSpec = TypeSpec.classBuilder(clsName)
.addModifiers(Modifier.PUBLIC)
.addMethod(constructor)
.build();
return JavaFile.builder(packageName.toString(), typeSpec).build().toString();
}
这个代码就不多做解释了,有兴趣的朋友可以到 github 上看看 JavaPoet 的 wiki,里面有详细的使用教程。
Ok,到这了,一个自定义的注解处理器的类就完成了,但是 android-compiler 这个库还没有完成,因为 JavaComipler 还不知道怎么找到这个注解处理器呢,按照 Oracle 官方的做法,是需要手动创建包和文件,来指定注解处理器的路径,然而,这种机械化的操作可以由 Google 开源的 auto-service 库解决,因此在 build.gradle 中再次引入。
dependencies {
compile fileTree(include: ['*.jar'], dir: 'libs')
compile project(':android-annotation')
compile 'com.squareup:javapoet:1.9.0'
compile 'com.google.auto.service:auto-service:1.0-rc3'
}
使用这个库就更简单了,直接给注解处理器的类加个注解就完了
@AutoService(Processor.class)
@SupportedAnnotationTypes("com.example.BindString")
@SupportedSourceVersion(SourceVersion.RELEASE_7)
public class HelloProcessor extends AbstractProcessor {}
现在,JavaCompiler 就可以找到这个处理 BindString 注解的注解处理器了。
那么,现在 android-compiler 这个库就完成了,不过,你应该也想知道这个生成的类到底是什么样子吧? 很简单,把 app 这个 Android Project 引入这2个 Java Library,然后使用 @BindString 注解(与例子中使用方法一样),然后 rebuild 即可看到下面的代码
package com.ckt.custombutterknife;
import android.content.res.Resources;
public class MainActivity_BindString {
public MainActivity_BindString(MainActivity target) {
Resources res = target.getResources();
target.mAppName = res.getString(2131099681);
}
}
生成的代码很简单,就一个 Android Project 中普通的 Java 类以及一个构造方法,从构造方法中也可以看出,只要传递一个 MainActivity 对象,就可以给 MainActivity 的 mAppName 赋值了,那么在 rebuild 后,就可以直接在 MainActivity 中调用 new MainActivity_BindString(this);
就可以为 mAppName 赋值了。
然而,这有个很明显的缺陷,那就是要先编译,再自己手动创建对象,也就等于手动赋值了。而 ButterKnife 却不是这样做的,ButterKnife 创建一个 Android Library(build.gradle 中 compile ‘com.jakewharton:butterknife:8.8.1’),通过传入的 Activity 对象,加载生成的类,通过反射获取生成类的构造函数,并创建了这个类的对象。 于是乎,Android Project 引用这个 Android Library,就可以在不用编译的情况下,也可以使用 ButterKnife.bind(this);
,只是没有任何作用,一旦编译甚至运行后,这个代码就起作用了。
在前面使用 ButterKnife 的例子中提到过, ButterKnife.bind(this)
为 mAppName 赋值,它的原理是加载生成的类,通过反射获取生成类的构造方法,然后创建这个类的对象。那么,我也来模仿下这个操作,创建一个名为 mybutterknife 的 Android Library,注意,不是 Java Library, 也不是 Android Project。具体步骤,如下2个图所示
然后,在 mybutterknife 这个类中,创建一个 MyButterKnife 的 Java 类
public class MyButterKnife {
public static void bind(Activity target) {
// 获取 activity 的 Class 对象
Class extends Activity> targetCls = target.getClass();
// 获取 activity 的名字
String clsName = targetCls.getName();
try {
// 通过 activity 的 ClassLoader 加载生成的类
Class> generatedCls = targetCls.getClassLoader().loadClass(clsName + "BindString");
// 找到构造方法
Constructor> constructor = generatedCls.getConstructor(targetCls);
if (constructor != null) {
// 创建对象
constructor.newInstance(target);
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
}
通过 Activiy 的 ClassLoader 加载了生成的类,通过反射获取了构造方法,然后创建了生成类的对象,从而达到了为 mAppName 赋值的目的。
在使用 ButterKnife 的例子中,加载了两个 ButterKnife 的库
compile 'com.jakewharton:butterknife:8.8.1'
annotationProcessor 'com.jakewharton:butterknife-compiler:8.8.1'
butterknife,这个已经说过,是一个 Android Library,而 butterknife-compiler,我想你也猜到了吧,是注解处理器的 Java Library,而 annotationProcessor 的意思是,app 项目编译时候,使用 butterknife-compiler ,然而并不会合并到最终的 apk 中,这是一种优化处理。
ok,到现在为止,模仿的项目中已经创建了两个 Java Library,android-annotation 和 android-compiler,一个 Android Library,mybutterknife,那么按照 ButterKnife 添加依赖库的方式,添加我们自己创建的依赖库
compile project(':mybutterknife')
annotationProcessor project(':android-compiler')
很明显,如果想要在 Activity 中使用自定义注解 @BindString,还需要把 android-annotation 加进来,这个可以在 mybutterknife 的 build.gradle 中添加进来
dependencies {
compile fileTree(include: ['*.jar'], dir: 'libs')
androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
exclude group: 'com.android.support', module: 'support-annotations'
})
compile 'com.android.support:appcompat-v7:26.+'
testCompile 'junit:junit:4.12'
// 添加自定义注解项目为依赖库
compile project(':android-annotation')
}
好的,到这里,app 项目就可以正常使用 @BindString 注解了
public class MainActivity extends AppCompatActivity {
@BindString(R.string.app_name)
String mAppName;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
MyButterKnife.bind(this);
System.out.println(mAppName);
}
}
与 ButterKnife 的使用方式完成一样,并且不需要预编译,也就是不需要先 rebuild,直接运行,mAppName 就可以被赋值。
本篇文章,抱着学习 ButterKnife 原理的心态,主要探究了注解处理的过程,代码生成的过程,并模仿 ButterKnife 做了一些优化。 当然,这篇文章只是抛砖引玉,想要了解更多,还是要深入研究下 ButterKnife 这个库。
https://docs.oracle.com/javase/7/docs/api/javax/annotation/processing/Processor.html
https://docs.oracle.com/javase/7/docs/api/java/util/ServiceLoader.html
https://medium.com/@iammert/annotation-processing-dont-repeat-yourself-generate-your-code-8425e60c6657
https://stablekernel.com/the-10-step-guide-to-annotation-processing-in-android-studio/
http://www.jianshu.com/p/d7567258ae85
https://github.com/square/javapoet