使用编译时注解简单实现类似 ButterKnife 的效果
这篇文章是学习鸿洋前辈的 Android 如何编写基于编译时注解的项目 的笔记,用于记录我的学习收获。
读完本文你将了解:
什么是编译时注解
上篇文章 什么是注解以及运行时注解的使用 中我们介绍了注解的几种使用场景,这里回顾一下:
- 编译前提示信息:注解可以被编译器用来发现错误,或者清除不必要的警告;
- 编译时生成代码:一些处理器可以在编译时根据注解信息生成代码,比如 Java 代码,xml 代码等;
- 运行时处理:我们可以在运行时根据注解,通过反射获取具体信息,然后做一些操作。
编译时注解就是只在编译时存在的注解,可以被注解处理器识别,用于生成一些代码。
APT
处理编译时注解需要使用 APT。
APT 即 Annotation Processing Tool,注解处理工具,它可以在编译时检测源代码文件,找到符合条件的注解修饰的内容,然后进行相应的处理。
我们在使用 ButterKnife 和 Dagger2 时,gradle 依赖中的 apt 就是指定在编译时调用它们的注解处理器:
compile "com.jakewharton:butterknife:$rootProject.butterknifeVersion"
apt "com.jakewharton:butterknife-compiler:$rootProject.butterknifeVersion"
compile "com.google.dagger:dagger:$rootProject.daggerVersion"
apt "com.google.dagger:dagger-compiler:$rootProject.daggerVersion"
编译时注解如何使用与编写
编译时注解的使用一般分为三步:
- 用注解修饰变量
- 编译时使用注解处理器生成代码
- 运行时调用生成的代码
那编写编译时注解项目的步骤就是这样:
- 先创建注解
- 创建注解处理器,在其中拿到注解修饰的变量信息,生成需要的代码
- 创建运行时,调用生成代码的调度器
举个例子
这里我们写一个类似 ButterKnife
使用注解实现 findViewById
的 demo。
思路
这个 demo 的目的减少编写 findViewById
的代码,使用一个注解就达到 View 对象的绑定效果。
羊毛出在猪身上,使用方便的背后一定有默默无闻的付出者,我们要做的就是根据注解实现对应 View 的绑定。
所以大概思路就是这样子:
- 先写一个注解,这个注解修饰一个成员变量,同时指定这个变量对应的 id
- 然后写个注解处理器,读取当前类的所有被注解修饰的成员对象和 id,生成对应的 findViewById 代码
- 最后写个运行时绑定的类,初始化当前类的成员
注意:
注解处理器所在的 module 必须是 Java Library,因为要用到特有的 javax;
注解处理器需要依赖 注解 module,所以注解所在的 module 也要是 Java Library;
运行时绑定的类要操作 Activity 或者 View,所以需要为 Android Library。
因此需要创建三个 module:
接下来将分别介绍每个 module 的内容。
1.创建注解
New 一个 Module,选择为 Java library,我们起名为 ioc-annotation。
在其中创建一个注解,这里叫 BindView:
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface BindView {
int value();
}
编译时注解的 Retention
为 RetentionPolicy.CLASS
,即只在编译时保留。
修饰目标为 ElementType.FIELD
,即成员变量。
这个注解有一个 value 属性,类型为 int,用于指明将来 findViewById
的 id。
现在我们可以使用这个注解来修饰 Activity 中的成员,指定它对应的 id:
@BindView(R.id.tv_content)
public TextView mTextView;
@BindView(R.id.tv_bottom_content)
public TextView mBottomTextView;
看起来和 ButterKnife
很相似吧,不过现在它只是有个样子,还得写点额外代码它才能起作用。
2.创建运行时绑定的类
类似 ButterKnife
,我们需要在 Activity 中调用一个绑定的方法,便于运行时初始化当前类中使用注解修饰的字段。就像这样:
@Override
protected void onCreate(@Nullable final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_annotation);
ViewBinder.bind(this);
}
New 一个 Module,选择为 Android library,我们起名为 ioc。
创建 ViewBinder
,它的作用是调用生成类,完成 bind()
方法传入对象的字段初始化。
**
* Description:
* <br> 从生成类中为当前 Activity/View 中的 View findViewById
* <p>
* <br> Created by shixinzhang on 17/6/22.
* <p>
* <br> Email: shixinzhang2016@gmail.com
* <p>
* <br> https://about.me/shixinzhang
*/
public class ViewBinder {
private static final String SUFFIX = "$$ViewInjector";
//Activity 中调用的方法
public static void bind(Activity activity) {
bind(activity, activity);
}
/**
* 1.寻找对应的代理类
* 2.调用接口提供的绑定方法
*
* @param host
* @param root
*/
@SuppressWarnings("unchecked")
private static void bind(final Object host, final Object root) {
if (host == null || root == null) {
return;
}
Class<?> aClass = host.getClass();
String proxyClassFullName = aClass.getName() + SUFFIX; //拼接生成类的名称
try {
Class<?> proxyClass = Class.forName(proxyClassFullName);
ViewInjector viewInjector = (ViewInjector) proxyClass.newInstance();
if (viewInjector != null) {
viewInjector.inject(host, root);
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
ViewBinder.bind(this)
的作用就是根据当前类名和约定好的类名,找到生成类,然后反射调用它的方法。为了调用指定的方法,让这个生成类实现一个接口。
所以我们还需要创建一个接口 ViewInjector
,这个接口的作用是便于反射调用。
public interface ViewInjector<T> {
void inject(T t, Object source);
}
其实也可以反射遍历调用对象的方法,但是效率不如直接实现一个接口来的好。
3.创建注解处理器
注解处理器的作用是读取注解、生成代码,先看下将来想要生成的代码:
我们要生成的类,名称是使用注解修饰的字段所在类名 拼接上 $$ViewInjector
,实现 ViewInjector
接口,在 inject()
方法中实现类中字段的 findViewById
过程。
这样直接传入对应的 activity,其中的 mTextView
或者 mBottomTextView
等使用 BindView
修饰的变量就可以初始化了。
OK,知道要生成啥样的类以后,就可以编写注解处理代码了。
New 一个 Module,选择为 Java library,我们起名为 ioc-processor。
①首先修改 build.gradle
文件,添加下面两行:
dependencies {
compile 'com.google.auto.service:auto-service:1.0-rc3'
compile project(path: ':ioc-annotation')
}
第一个依赖会帮我们生成 META-INF
元信息,指明注解处理器的完整路径。
如果不想使用这个依赖,要自己创建的话,也可以:
- 在 main 文件夹下创建一个 resources.META-INF.services 文件夹
- 在其中创建
javax.annotation.processing.Processor
文件
- 文件的内容是注解处理器的完整包名加类名
top.shixinzhang.BindViewProcessor
②我这里直接使用注解了,创建注解处理器,继承 AbstractProcessor
:
@AutoService(Processor.class) //帮我们生成 META-INF 信息
@SupportedAnnotationTypes("top.shixinzhang.BindView") 要处理的注解类型
@SupportedSourceVersion(SourceVersion.RELEASE_7) //支持的源码版本
public class BindViewProcessor extends AbstractProcessor {
//...
}
三个注解的作用如注释所示。
如果不使用后面两个注解,就需要重写 getSupportedAnnotationTypes()
和 getSupportedSourceVersion
方法:
// 有注解就不用重写这两个方法了
// @Override
// public Set<String> getSupportedAnnotationTypes() {
// Set<String> annotationTypes = new LinkedHashSet<>();
// annotationTypes.add(BindView.class.getCanonicalName());
// return annotationTypes;
// }
//
//
// /**
// * 支持的源码版本
// * @return
// */
// @Override
// public SourceVersion getSupportedSourceVersion() {
// return SourceVersion.latestSupported();
// }
③然后重写 init()
方法:
@AutoService(Processor.class)
@SupportedAnnotationTypes("top.shixinzhang.BindView")
@SupportedSourceVersion(SourceVersion.RELEASE_7) //支持的源码版本
public class BindViewProcessor extends AbstractProcessor {
private Elements mElementUtils; //基于元素进行操作的工具方法
private Filer mFileCreator; //代码创建者
private Messager mMessager; //日志,提示者,提示错误、警告
private Map<String, ProxyInfo> mProxyMap = new HashMap<>();
@Override
public synchronized void init(final ProcessingEnvironment processingEnv) {
super.init(processingEnv);
mElementUtils = processingEnv.getElementUtils();
mFileCreator = processingEnv.getFiler();
mMessager = processingEnv.getMessager();
}
//...
}
在这个方法中做初始化操作,参数 processingEnv
是注解处理环境,通过它可以获取很多功能类:
public interface ProcessingEnvironment {
//返回注解处理工具的一些配置选项
Map<String,String> getOptions();
//返回信息传递者,用来报告错误、警告灯信息
Messager getMessager();
//返回用于创建 Java 文件、class 文件或者其他辅助文件的文件创建者
Filer getFiler();
//返回用于基于元素进行操作的工具类
Elements getElementUtils();
//返回用于基于类型进行操作的工具类
Types getTypeUtils();
//返回生成文件的版本
SourceVersion getSourceVersion();
//返回当前区域,用于提示本地化的消息
Locale getLocale();
}
这么多功能,我们这里只使用 getElementUtils()
, getFiler()
和 getMessager()
,用于后续创建文件、获取元素信息,以及在编译时提示信息。
④重写 process()
方法
做好准备工作后,接下来在 process()
中做两件事:
- 收集信息
- 生成代码
首先收集信息,我们需要拿到的信息有如下几点:
- 注解修饰变量所在的类名,便于和后缀拼接生成代理类
- 类的完整包名
- 类中被注解修饰的字段,以及对应的布局 id
那我们编译时可以拿到什么呢?
@Override
public boolean process(final Set<? extends TypeElement> annotations, final RoundEnvironment roundEnv) {
//...
}
第一个参数暂且不表,第二个参数 RoundEnvironment
的作用是提供一个注解处理器,在编译时可以查询类的信息。其中有一个关键的方法 getElementsAnnotatedWith()
:
Set<? extends Element> getElementsAnnotatedWith(Class<? extends Annotation> a);
这个方法可以拿到指定注解修饰的元素集合,返回的是 Element
及其子类的对象集合。
Element
是一个接口,代表着一个包、类、方法或者元素,它的子接口有很多,比如:
VariableElement
:成员变量TypeElement
:类或者接口PackageElement
:包信息ExecutableElement
:方法
OK,了解了目的和条件,就可以编写代码了。
@Override
public boolean process(final Set<? extends TypeElement> annotations, final RoundEnvironment roundEnv) {
mMessager.printMessage(Diagnostic.Kind.NOTE, "process...");
//避免生成重复的代理类
mProxyMap.clear();
//拿到被 @BindView 注解修饰的元素,应该是 VariableElement
Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(BindView.class);
//1.收集信息
for (Element element : elements) {
if (!checkAnnotationValid(element, BindView.class)) { //去除不合格的元素
continue;
}
//类中的成员变量
VariableElement variableElement = (VariableElement) element;
//类或者接口
TypeElement typeElement = (TypeElement) variableElement.getEnclosingElement();
//完整的名称
String qualifiedName = typeElement.getQualifiedName().toString();
ProxyInfo proxyInfo = mProxyMap.get(qualifiedName);
if (proxyInfo == null) {
//将该类中被注解修饰的变量加入到 ProxyInfo 中
proxyInfo = new ProxyInfo(mElementUtils, typeElement);
mProxyMap.put(qualifiedName, proxyInfo);
}
BindView annotation = variableElement.getAnnotation(BindView.class);
if (annotation != null) {
int id = annotation.value();
proxyInfo.mInjectElements.put(id, variableElement);
}
}
//...
}
我们先输出了一个提示信息 “process…”,一会儿 build 项目的时候可以看到这个提示。
上面的代码主要做了这几件事:
- 先调用
roundEnv.getElementsAnnotatedWith(BindView.class)
拿到被 @BindView 注解修饰的元素集合,在前面的例子中,我们拿到的就是TextView mTextView
和TextView mBottomTextView
。 - 然后遍历这些元素,由于我们注解修饰的是变量,可以直接转换成
VariableElement
类型。 - 调用
variableElement.getEnclosingElement()
方法拿到变量所在类的对象信息,调用它的getQualifiedName().toString()
方法获得类的完整名称。
我们使用一个 map 保存类的信息:
private Map<String, ProxyInfo> mProxyMap = new HashMap<>();
你可以先创建一个空的 ProxyInfo
类,构造函数为:
public class ProxyInfo {
private static final String SUFFIX = "ViewInjector";
public Map<Integer, VariableElement> mInjectElements = new HashMap<>(); //被注解修饰的变量和 id 映射表
public ProxyInfo(final Elements elementUtils, final TypeElement typeElement) {
//...
}
}
}
它的具体内容后面介绍。
将该类中被注解修饰的变量加入到 mProxyMap
后,接下来就可以遍历这些信息,生成对应的代码了。
生成代码:
//...
//2.生成代理类
for (String key : mProxyMap.keySet()) {
ProxyInfo proxyInfo = mProxyMap.get(key);
try {
//创建文件对象
JavaFileObject sourceFile = mFileCreator.createSourceFile(
proxyInfo.getProxyClassFullName(), proxyInfo.getTypeElement());
Writer writer = sourceFile.openWriter();
writer.write(proxyInfo.generateJavaCode()); //写入文件
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;
}
遍历 mProxyMap
,里面的 ProxyInfo
列表创建文件。
创建文件对象只需要调用 mFileCreator.createSourceFile
拿到 JavaFileObject
对象,然后拿到 Writer,写入信息即可。
生成代码最终调用的是 proxyInfo.generateJavaCode()
,这时我们可以了解前面介绍的 ProxyInfo
了。
最终的代码生成类。
ProxyInfo
的作用就是拿代码拼出这个类:
所以它需要保存类的信息、包名、完整类名以及其中的变量列表,在构造函数中初始化:
public class ProxyInfo {
private static final String SUFFIX = "ViewInjector";
public Map<Integer, VariableElement> mInjectElements = new HashMap<>(); //变量列表
private TypeElement mTypeElement; //类信息
private String mPackageName; //包名
private String mProxyClassName; //代理类名
public ProxyInfo(final Elements elementUtils, final TypeElement typeElement) {
mTypeElement = typeElement;
PackageElement packageElement = elementUtils.getPackageOf(typeElement);
mPackageName = packageElement.getQualifiedName().toString();
String className = getClassName(typeElement, mPackageName);
mProxyClassName = className + "$$" + SUFFIX;
System.out.println("****** " + mProxyClassName + " \n" + mPackageName);
}
private String getClassName(final TypeElement typeElement, final String packageName) {
int packageLength = packageName.length() + 1; //
return typeElement.getQualifiedName().toString().substring(packageLength).replace('.', '$');
}
//...
}
然后就可以根据这些动态信息生成不同的类了。
{
public String generateJavaCode() {
StringBuilder stringBuilder = new StringBuilder();
//stringBuilder 中不要再使用 + 拼接字符串
stringBuilder.append("// Generate code. Do not modify it !\n")
.append("package ").append(mPackageName).append(";\n\n")
.append("import top.shixinzhang.ioc.*;\n\n")
.append("public class ").append(mProxyClassName).append(" implements ").append(SUFFIX).append("<").append(mTypeElement.getQualifiedName()).append(">").append("{\n");
generateMethod(stringBuilder);
stringBuilder.append("\n}\n");
return stringBuilder.toString();
}
private void generateMethod(final StringBuilder stringBuilder) {
if (stringBuilder == null) {
return;
}
stringBuilder.append("@Override\n")
.append("public void inject(").append(mTypeElement.getQualifiedName()).append(" host, Object object )").append("{\n");
for (Integer id : mInjectElements.keySet()) {
VariableElement variableElement = mInjectElements.get(id);
String name = variableElement.getSimpleName().toString();
String type = variableElement.asType().toString();
stringBuilder.append("if(object instanceof android.app.Activity)").append("{\n")
.append("host.").append(name).append(" = ")
.append("(").append(type).append(")((android.app.Activity)object).findViewById(").append(id).append(");")
.append("\n}\n")
.append("else").append("{\n")
.append("host.").append(name).append(" = ")
.append("(").append(type).append(")((android.view.View)object).findViewById(").append(id).append(");")
.append("\n}\n");
}
stringBuilder.append("\n}\n");
}
public String getProxyClassFullName() {
return mPackageName + "." + mProxyClassName;
}
public TypeElement getTypeElement() {
return mTypeElement;
}
}
拼的很简单粗暴,参考目标代码即可。
完成编写,使用一下
完成这三个 module 后,就可以直接使用了!
在 app module 的 gradle 文件中添加三个 module 的依赖:
compile project(':ioc')
compile project(':ioc-annotation')
apt project(':ioc-processor')
apt 指定注解处理器。
然后在类中使用注解修饰变量,同时调用 ViewBinder.bind(this)
绑定当前 Activity。
public class AnnotationTestActivity extends BaseActivity {
@BindView(R.id.tv_content)
public TextView mTextView;
@BindView(R.id.tv_bottom_content)
public TextView mBottomTextView;
@Override
protected void onCreate(@Nullable final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_annotation);
ViewBinder.bind(this);
}
//...
}
点击 Build
-> Rebuild Project
,可以在 Messages
-> Gradle Console
控制台中看到输出信息:
然后在 app -> build -> generated -> source -> apt -> flavor -> 使用注解的包名下,看到生成类:
有这个类表示生成代码成功了。
然后运行一下,运行时就会完成对应变量的初始化。
遇到的坑
1.无法引入javax包下的类库
javax 包属于java,Android核心库中没有。所以不能直接在app Module和Android Library中使用,必须要创建一个Java Library。然后由Java Library导出jar包使用。
2.不生成文件
检查你有没有使用注解。。。
2.几个 module 没有划分
注解没有单独在一个 module 中
3.感谢这个开源项目负责人认真的解答,让我也发现了问题所在!
https://github.com/realm/realm-java/issues/2695
代码地址
总结
这篇文章介绍了如何编写编译时注解,光看一边很难理解,希望各位可以亲手敲一遍,加深理解。
编译时注解的作用就是生成代码,对比在运行时反射进行类似的操作,性能影响可以忽略不计,它其实和直接运行手写代码没有任何区别,方便在帮我们省去编写一些重复的代码。
EventBus,ButterKnife,Dagger2 都使用了编译时注解,技术基础有了后,具体如何创造,就看你的想象力了!
Thanks
http://blog.csdn.net/lmj623565791/article/details/51931859
https://lizhaoxuan.github.io/2016/08/26/apt_usage_scenario/
http://blog.stablekernel.com/the-10-step-guide-to-annotation-processing-in-android-studio
https://medium.com/@aitorvs/annotation-processing-in-android-studio-7042ccb83024
https://bitbucket.org/hvisser/android-apt
https://joyrun.github.io/2016/07/19/AptHelloWorld/
使用编译时注解简单实现类似 ButterKnife 的效果的更多相关文章
- Kotlin编译时注解,简单实现ButterKnife
ButterKnife在之前的Android开发中还是比较热门的工具,帮助Android开发者减少代码编写,而且看起来更加的舒适,于是简单实现一下ButterKnife,相信把下面的代码都搞懂,看Bu ...
- Android 编译时注解解析框架
2.注解 说道注解,竟然还有各种分类,得,这记不住,我们从注解的作用来反推其分类,帮助大家记忆,然后举例强化大家的记忆,话说注解的作用: 1.标记一些信息,这么说可能太抽象,那么我说,你见过@Over ...
- Android 打造编译时注解解析框架 这只是一个开始
转载请标明出处:http://blog.csdn.net/lmj623565791/article/details/43452969 ,本文出自:[张鸿洋的博客] 1.概述 记得很久以前,写过几篇博客 ...
- 利用APT实现Android编译时注解
摘要: 一.APT概述 我们在前面的java注解详解一文中已经讲过,可以在运行时利用反射机制运行处理注解.其实,我们还可以在编译时处理注解,这就是不得不说官方为我们提供的注解处理工具APT (Anno ...
- java 编译时注解框架 lombok-ex
lombok-ex lombok-ex 是一款类似于 lombok 的编译时注解框架. 编译时注,拥有运行时注解的便利性,和无任何损失的性能. 主要补充一些 lombok 没有实现,且自己会用到的常见 ...
- WPF Label控件在数据绑定Content属性变化触发TargetUpdated事件简单实现类似TextChanged 事件效果
原文:WPF Label控件在数据绑定Content属性变化触发TargetUpdated事件简单实现类似TextChanged 事件效果 本以为Label也有TextChanged 事件,但在使 ...
- lombok编译时注解@Slf4j的使用及相关依赖包
slf4j是一个日志门面模式的框架,只对调用者开放少量接口用于记录日志 主要接口方法有 debug warn info error trace 在idea中可以引入lombok框架,使用@Slf4j注 ...
- Java 进阶巩固:什么是注解以及运行时注解的使用
这篇文章 2016年12月13日星期二 就写完了,当时想着等写完另外一篇关于自定义注解的一起发.结果没想到这一等就是半年多 - -. 有时候的确是这样啊,总想着等条件更好了再干,等准备完全了再开始,结 ...
- apt 根据注解,编译时生成代码
apt: @Retention后面的值,设置的为CLASS,说明就是编译时动态处理的.一般这类注解会在编译的时候,根据注解标识,动态生成一些类或者生成一些xml都可以,在运行时期,这类注解是没有的~~ ...
随机推荐
- 20145328 《Java程序设计》第6周学习总结
20145328 <Java程序设计>第6周学习总结 教材学习内容总结 输入/输出 InputStream与OutputStream 从应用程序角度来看,如果要将数据从来源取出,可以使用输 ...
- sublime text3 授权码
适用于 Sublime Text 3 Build3126 64位 官方版 -– BEGIN LICENSE -– Michael Barnes Single User License EA7E-821 ...
- JS时间和字符串的相互转换 Date+String
1.js字符串转换成时间 1.1方法一:输入的时间格式为yyyy-MM-dd function convertDateFromString(dateString) { if (dateString) ...
- Hive架构
Hive组织数据包含四种层次:DataBase --> Table --> Partition --> Bucket,对应在HDFS上都是文件夹形式. 数据库和数据仓库的区别: 1) ...
- 何为K-邻近算法
答:K-邻近算法,英文为K-nearest neighbor(KNN),就是计算要测试对象与k个样本对象之间的距离,通过距离的大小来对测试对象进行分类
- iOS开发中的地图开发
显示地图: 1.导入头文件 #import <MapKit/MapKit.h> 如果同时需要用户定位的话还需要 #import <CoreLocation/CoreLocation. ...
- 【ML数学知识】极大似然估计
它是建立在极大似然原理的基础上的一个统计方法,极大似然原理的直观想法是,一个随机试验如有若干个可能的结果A,B,C,... ,若在一次试验中,结果A出现了,那么可以认为实验条件对A的出现有利,也即出现 ...
- outline详解
outline这个属性平时用的不太多,最近被问及专门研究一下这个属性的作用. CSS2加进来的outline属性,中文翻译过来是外轮廓. 神马是轮廓? 轮廓,指边缘:物体的外周或图形的外框. 那这样的 ...
- .net知识点汇总
死锁的必要条件?怎么克服? 答:系统的资源不足,进程的推进的顺序不合适,资源分配不当,一个资源每次只能被一个进程使用,一个资源请求资源时,而此时这个资源已阻塞,对已获得资源不放,进程获得资源时,未使用 ...
- 使用ssm整合是项目启动tomcat报错java.lang.IndexOutOfBoundsException
解决办法:删除.m2文件夹下的全部仓库,然后重启myeclipse,对项目进行maven project.问题解决. 在没有这样做时,除了tomcat启动会失败,项目还有会报如下错误: ①cvc-co ...