使用编译时注解实现简易的 ButterKnife 效果
一、前言
现在有太多了关于注解的三方框架供我们使用,比如 ButterKnife、Dagger2 等,我们不仅要会使用,还要知道其中的大致原理。接下来就通过一个小的实例来熟悉下编译时注解。
关于注解的基础知识就不介绍的,有兴趣的可以去看看这篇文章,写的挺好的。
另外本例子中的代码已经传到我的 Github,有兴趣的可以去看下。
二、准备
1、 新建项目 BindView
2、 新建 Module,命名为 annotation,类型选择为 Java Library。
3、 新建 Module,命名为 compile,类型选择为 Java Library。
在该 Module 的 build.gradle 中添加:
compile project(':annotation')
compile 'com.google.auto.service:auto-service:1.0-rc3'
4、 新建 Module,命名为 api,类型选择为 Android Library。
在该 Module 的 build.gradle 中添加 :
compile project(':annotation')
5、 在 app Module的 build.gradle 中添加:
compile project(':api')
annotationProcessor project(':compile')
三、编码实现
3.1 编写注解
在 annotation 模块中新建注解如下:
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.CLASS)
public @interface BindView {
int value();
}
3.2 编写注解处理器
在 compile 模块中新建类 BindViewProcessor 继承于 AbstractProcessor,并添加 @AutoService(Processor.class) 注解。
@AutoService(Processor.class)
public class BindViewProcessor extends AbstractProcessor {
/**
* 代码写入工具
*/
private Filer mFiler;
/**
* 日志打印
*/
private Messager mMessager;
/**
* 跟元素相关的辅助类,帮助我们去获取一些元素相关的信息。
*/
private Elements mElementUtils;
private Map<String, ProxyInfo> mProxyInfoCacheMap = new HashMap<>();
@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
super.init(processingEnvironment);
//获取日志打印工具
mMessager = processingEnv.getMessager();
//获取元素辅助类
mElementUtils = processingEnvironment.getElementUtils();
//获取代码写入工具
mFiler = processingEnvironment.getFiler();
}
@Override
public Set<String> getSupportedAnnotationTypes() {
// 新建 LinkedHashMap 用于存储有序的注解名称集合
Set<String> annotationTypes = new LinkedHashSet<>();
// 添加我们的BindView到集合中
annotationTypes.add(BindView.class.getCanonicalName());
return annotationTypes;
}
@Override
public SourceVersion getSupportedSourceVersion() {
// 指定Java版本,一般返回 latestSupported();
return SourceVersion.latestSupported();
}
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
//...
return false;
}
}
前面是准备工作,接下来就要编写最重要的 process 方法:
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
// 每次清除缓存
mProxyInfoCacheMap.clear();
// 返回被BindView注释的元素
Set<? extends Element> elementsWithBindView = roundEnvironment.getElementsAnnotatedWith(BindView.class);
//遍历被BindView注解的元素
for (Element element : elementsWithBindView) {
// 检查注解有效性
checkAnnotationValid(element, BindView.class);
// 获取成员变量元素
VariableElement variableElement = (VariableElement) element;
// 获取成员变量元素所在类元素
TypeElement classElement = (TypeElement) variableElement.getEnclosingElement();
// 获取类的全称
String fullClassName = classElement.getQualifiedName().toString();
// 从缓存中取出编译生成类信息
ProxyInfo proxyInfo = mProxyInfoCacheMap.get(fullClassName);
// 如果缓存中没有,就新建,然后添加到缓存中
if (proxyInfo == null){
proxyInfo = new ProxyInfo(mElementUtils,classElement);
mProxyInfoCacheMap.put(fullClassName,proxyInfo);
}
// 获取注解类对象
BindView bindViewAnnotation = variableElement.getAnnotation(BindView.class);
// 获取注解的中value的值
int id = bindViewAnnotation.value();
// 在编译生成类中存储元素
proxyInfo.injectVariables.put(id,variableElement);
}
// 遍历缓存中的所有元素生成编译类
for (String s : mProxyInfoCacheMap.keySet()) {
// 缓存中取出ProxyInfo
ProxyInfo proxyInfo = mProxyInfoCacheMap.get(s);
try {
// 生成代码
JavaFileObject jfo = mFiler.createSourceFile(proxyInfo.getProxyClassFullName(),proxyInfo.getTypeElement());
Writer writer = jfo.openWriter();
writer.write(proxyInfo.generatedJavaCode());
writer.flush();
writer.close();
} catch (IOException e) {
e.printStackTrace();
error(proxyInfo.getTypeElement(),"Unable to write injector for type %s: %s",
proxyInfo.getTypeElement(),e.getMessage());
}
}
return true;
}
/**
* 检查注解是否可用
*
* @param annotatedElement
* @param clazz
* @return
*/
private boolean checkAnnotationValid(Element annotatedElement, Class clazz) {
if (annotatedElement.getKind() != ElementKind.FIELD) {
error(annotatedElement, "%s must be declared on field.", clazz.getSimpleName());
return false;
}
if (ClassUtils.isPrivate(annotatedElement)) {
error(annotatedElement, "%s() must can not be private.", annotatedElement.getSimpleName());
return false;
}
return true;
}
/**
* 错误日志打印
*
* @param element
* @param message
* @param args
*/
private void error(Element element, String message, Object... args) {
if (args.length > 0) {
message = String.format(message, args);
}
mMessager.printMessage(Diagnostic.Kind.NOTE, message, element);
}
这里就不在说明了,注释已经很详细了。
上面使用到的类如下:
ProxyInfo :
public class ProxyInfo {
/**
* 包名
*/
private String mPackageName;
/**
* 类名
*/
private String mClassName;
/**
* 类或接口程序元素
*/
private TypeElement mTypeElement;
/**
* 用于存储id和元素的HashMap
*/
public Map<Integer, VariableElement> injectVariables = new HashMap<>();
/**
* 构造方法中初始化成员变量
* @param elements
* @param classElement
*/
public ProxyInfo(Elements elements, TypeElement classElement) {
this.mTypeElement = classElement;
//获取包名
PackageElement packageElement = elements.getPackageOf(classElement);
String packageName = packageElement.getQualifiedName().toString();
//获取类名
String className = ClassUtils.getClassName(classElement, packageName);
this.mClassName = className+"$$"+Constants.SUFFIX;
this.mPackageName = packageName;
}
/**
* 生成Java代码
* @return
*/
public String generatedJavaCode() {
StringBuilder builder = new StringBuilder();
builder.append("//Generated code,Do not modify!")
.append("\npackage ").append(mPackageName).append(";\n\n")
.append("import cn.smartsean.api.*;\n\n")
.append("public class ").append(mClassName).append(" implements " + Constants.SUFFIX + "<" + mTypeElement.getQualifiedName() + ">{\n\n");
generatedMethods(builder);
builder.append("\n}\n");
return builder.toString();
}
/**
* 生成方法
* @param builder
*/
private void generatedMethods(StringBuilder builder) {
builder.append("\t@Override\n")
.append("\tpublic void inject(" + mTypeElement.getQualifiedName() + " host,Object source) { \n ");
for (Integer integer : injectVariables.keySet()) {
VariableElement element = injectVariables.get(integer);
String name = element.getSimpleName().toString();
String type = element.asType().toString();
builder.append("\t\tif (source instanceof android.app.Activity){ \n")
.append("\t\t\thost." + name + " = (" + type + ")(((android.app.Activity)source).findViewById(" + integer + "));\n")
.append("\t\t}else {\n")
.append("\t\t\thost." + name + " = (" + type + ")(((android.view.View)source).findViewById(" + integer + "));\n")
.append("\t\t}");
}
builder.append("\n\t}");
}
/**
* 获取全称
* @return
*/
public String getProxyClassFullName() {
return this.mPackageName + "." + this.mClassName;
}
/**
* 获取类型元素
* @return
*/
public TypeElement getTypeElement() {
return mTypeElement;
}
}
ClassUtils :
public class ClassUtils {
/**
* 是否是私有的
* @param annotatedClass
* @return
*/
public static boolean isPrivate(Element annotatedClass) {
return annotatedClass.getModifiers().contains(PRIVATE);
}
/**
* 类名
* @param type
* @param packageName
* @return
*/
public static String getClassName(TypeElement type, String packageName) {
int packageLen = packageName.length() + 1;
return type.getQualifiedName().toString().substring(packageLen)
.replace('.', '$');
}
}
Constants :
public class Constants {
public static final String SUFFIX = "ViewInject";
}
至此,注解处理器已经完成了,接来下看看 api 模块
3.3 编写 api 模块
首先定义 注入接口:
public interface ViewInject<T> {
void inject(T t,Object o);
}
然后编写注入工具类:BindView
public class BindView {
public static final String SUFFIX = "$$ViewInject";
/**
* 在Activity中使用
* @param activity
*/
public static void bind(Activity activity) {
//根据传入的activity得到viewInject对象,并注入
ViewInject viewInject = findProxyActivity(activity);
viewInject.inject(activity, activity);
}
public static void bind(Object object, View view) {
ViewInject viewInject = findProxyActivity(object);
viewInject.inject(object, view);
}
/**
* 查找我们生成的类
* @param object
* @return
*/
private static ViewInject findProxyActivity(Object object) {
try {
Class clz = object.getClass();
Class bindViewClass = Class.forName(clz.getName() + SUFFIX);
return (ViewInject) bindViewClass.newInstance();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
}
throw new RuntimeException(String.format("can not find %s , something when compiler.", object.getClass().getSimpleName() + SUFFIX));
}
}
四、使用
在 MainActivity 中使用:
public class MainActivity extends AppCompatActivity {
@BindView(R.id.test)
TextView mTest;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
cn.smartsean.api.BindView.bind(this);
mTest.setText("This is my first annotation processor test");
}
}
先 build 下项目,在 app-build-generated-source-apt-cn.smartsean.bindview文件夹下有个 MainActivity$$ViewInject
文件。
这个文件就是我们通过编译时注解生成的,用来 findViewById 的。
MainActivity$$ViewInject:
//Generated code,Do not modify!
package cn.smartsean.bindview;
import cn.smartsean.api.*;
public class MainActivity$$ViewInject implements ViewInject<cn.smartsean.bindview.MainActivity>{
@Override
public void inject(cn.smartsean.bindview.MainActivity host,Object source) {
if (source instanceof android.app.Activity){
host.mTest = (android.widget.TextView)(((android.app.Activity)source).findViewById(2131165305));
}else {
host.mTest = (android.widget.TextView)(((android.view.View)source).findViewById(2131165305));
}
}
}
接下来运行下程序,效果图如下:
本例子中的代码已经传到我的 Github,有兴趣的可以去看下。