Android注解使用之通过annotationProcessor注解生成代码实现自己的ButterKnife框架
前言:
Annotation注解在Android的开发中的使用越来越普遍,例如EventBus、ButterKnife、Dagger2等,之前使用注解的时候需要利用反射机制势必影响到运行效率及性能,直到后来android-apt的出现通过注解根据反射机制动态编译生成代码的方式来解决在运行时不再使用发射机制,不过随着android-apt的退出不再维护,我们今天利用Android studio的官方插件annotationProcessor来实现一下自己的ButterKnife UI注解框架。
需要了解的知识:
自动成代码:
1.)先看下整个项目结构
整个项目分一个app、app.api、app.annotation、app.complier
app:整个项目的入口 用于测试注解框架
app.annotation:主要用于申明app所有使用的UI注解
app.api:用于申明UI注解框架的api
app.complier:用于在编译期间通过反射机制自动生成代码
2.)app.annotation 声明注解框架中要使用的注解
这里我声明了一个BindView注解,声明周期为Class,作用域为成员变量
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface BindView {
int value();
}
因为这里仅仅想要实现绑定View控件,这里就声明了一个BindView注解
注意:
app.annotation module为java library,build.gradle配置如下
apply plugin: 'java'
sourceCompatibility =JavaVersion.VERSION_1_7
targetCompatibility =JavaVersion.VERSION_1_7
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
}
3.)app.api 声明UI注解框架中使用的api,比如绑定解绑,查找View控件等
面向接口编程,定义一个绑定注解的接口
/**
* UI绑定解绑接口
*
* @param <T>
*/
public interface ViewBinder<T> { void bindView(T host, Object object, ViewFinder finder); void unBindView(T host);
}
定义一个被绑定者查找view的接口
/**
* ui提供者接口
*/
public interface ViewFinder { View findView(Object object, int id);
}
这里声明一个Activity 默认的View查找者
/**
* Activity UI查找提供者
*/ public class ActivityViewFinder implements ViewFinder {
@Override
public View findView(Object object, int id) {
return ((Activity) object).findViewById(id);
}
}
注解框架向外提供绑定方法,这里使用静态类来管理
public class LCJViewBinder {
private static final ActivityViewFinder activityFinder = new ActivityViewFinder();//默认声明一个Activity View查找器
private static final Map<String, ViewBinder> binderMap = new LinkedHashMap<>();//管理保持管理者Map集合 /**
* Activity注解绑定 ActivityViewFinder
*
* @param activity
*/
public static void bind(Activity activity) {
bind(activity, activity, activityFinder);
} /**
* '注解绑定
*
* @param host 表示注解 View 变量所在的类,也就是注解类
* @param object 表示查找 View 的地方,Activity & View 自身就可以查找,Fragment 需要在自己的 itemView 中查找
* @param finder ui绑定提供者接口
*/
private static void bind(Object host, Object object, ViewFinder finder) {
String className = host.getClass().getName();
try {
ViewBinder binder = binderMap.get(className);
if (binder == null) {
Class<?> aClass = Class.forName(className + "$$ViewBinder");
binder = (ViewBinder) aClass.newInstance();
binderMap.put(className, binder);
}
if (binder != null) {
binder.bindView(host, object, finder);
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
} /**
* 解除注解绑定 ActivityViewFinder
*
* @param host
*/
public static void unBind(Object host) {
String className = host.getClass().getName();
ViewBinder binder = binderMap.get(className);
if (binder != null) {
binder.unBindView(host);
}
binderMap.remove(className);
}
}
4.)app.complier根据注解在编译期间自动生成java代码
优先需要自定义一个AbstractProcessor,然后Annotation生成代码,完整的AbstractProcessor
public class LCJViewBinderProcessor extends AbstractProcessor { @Override
public synchronized void init(ProcessingEnvironment env){ } @Override
public boolean process(Set<? extends TypeElement> annoations, RoundEnvironment env) { } @Override
public Set<String> getSupportedAnnotationTypes() { } @Override
public SourceVersion getSupportedSourceVersion() { } }
重要函数解说
- init(ProcessingEnvironment env): 每一个注解处理器类都必须有一个空的构造函数。然而,这里有一个特殊的init()方法,它会被注解处理工具调用,并输入ProcessingEnviroment参数。ProcessingEnviroment提供很多有用的工具类Elements,Types和Filer。
public boolean process(Set<? extends TypeElement> annoations, RoundEnvironment env)这相当于每个处理器的主函数main()。 在这里写扫描、评估和处理注解的代码,以及生成Java文件。输入参数RoundEnviroment,可以让查询出包含特定注解的被注解元素。
- getSupportedAnnotationTypes();这里必须指定,这个注解处理器是注册给哪个注解的。注意,它的返回值是一个字符串的集合,包含本处理器想要处理的注解类型的合法全称。换句话说,在这里定义你的注解处理器注册到哪些注解上。
- getSupportedSourceVersion();用来指定你使用的Java版本。
该module同样是Java Library,build.gradle配置如下
apply plugin: 'java'
//apply plugin: 'com.github.dcendents.android-maven'
sourceCompatibility =JavaVersion.VERSION_1_7
targetCompatibility =JavaVersion.VERSION_1_7 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(':app.annotation')
}
com.google.auto.service:auto-service:1.0-rc2 谷歌提供的Java 生成源代码库
com.squareup:javapoet:1.7.0 提供了各种 API 让你用各种姿势去生成 Java 代码文件
定义一个被注解类对象AnnotedClass,用于保存哪些被注解的对象
class AnnotatedClass {
private static class TypeUtil {
static final ClassName BINDER = ClassName.get("com.whoislcj.appapi", "ViewBinder");
static final ClassName PROVIDER = ClassName.get("com.whoislcj.appapi", "ViewFinder");
} private TypeElement mTypeElement;
private ArrayList<BindViewField> mFields;
private Elements mElements; AnnotatedClass(TypeElement typeElement, Elements elements) {
mTypeElement = typeElement;
mElements = elements;
mFields = new ArrayList<>();
} void addField(BindViewField field) {
mFields.add(field);
} JavaFile generateFile() {
//generateMethod
MethodSpec.Builder bindViewMethod = MethodSpec.methodBuilder("bindView")
.addModifiers(Modifier.PUBLIC)
.addAnnotation(Override.class)
.addParameter(TypeName.get(mTypeElement.asType()), "host")
.addParameter(TypeName.OBJECT, "source")
.addParameter(TypeUtil.PROVIDER, "finder"); for (BindViewField field : mFields) {
// find views
bindViewMethod.addStatement("host.$N = ($T)(finder.findView(source, $L))", field.getFieldName(), ClassName.get(field.getFieldType()), field.getResId());
} MethodSpec.Builder unBindViewMethod = MethodSpec.methodBuilder("unBindView")
.addModifiers(Modifier.PUBLIC)
.addParameter(TypeName.get(mTypeElement.asType()), "host")
.addAnnotation(Override.class);
for (BindViewField field : mFields) {
unBindViewMethod.addStatement("host.$N = null", field.getFieldName());
} //generaClass
TypeSpec injectClass = TypeSpec.classBuilder(mTypeElement.getSimpleName() + "$$ViewBinder")
.addModifiers(Modifier.PUBLIC)
.addSuperinterface(ParameterizedTypeName.get(TypeUtil.BINDER, TypeName.get(mTypeElement.asType())))
.addMethod(bindViewMethod.build())
.addMethod(unBindViewMethod.build())
.build(); String packageName = mElements.getPackageOf(mTypeElement).getQualifiedName().toString(); return JavaFile.builder(packageName, injectClass).build();
}
}
然后再定义一个BindViewField对象用于被注解的成员变量
class BindViewField {
private VariableElement mVariableElement;
private int mResId; BindViewField(Element element) throws IllegalArgumentException {
if (element.getKind() != ElementKind.FIELD) {
throw new IllegalArgumentException(String.format("Only fields can be annotated with @%s",
BindView.class.getSimpleName()));
}
mVariableElement = (VariableElement) element; BindView bindView = mVariableElement.getAnnotation(BindView.class);
mResId = bindView.value();
if (mResId < 0) {
throw new IllegalArgumentException(
String.format("value() in %s for field %s is not valid !", BindView.class.getSimpleName(),
mVariableElement.getSimpleName()));
}
} /**
* 获取变量名称
*
* @return
*/
Name getFieldName() {
return mVariableElement.getSimpleName();
} /**
* 获取变量id
*
* @return
*/
int getResId() {
return mResId;
} /**
* 获取变量类型
*
* @return
*/
TypeMirror getFieldType() {
return mVariableElement.asType();
}
}
上面两个对象定义好了之后,就下来实现一下根据注解生成代码过程
@AutoService(Processor.class)
public class LCJViewBinderProcessor extends AbstractProcessor {
private Filer mFiler; //文件相关的辅助类
private Elements mElementUtils; //元素相关的辅助类
private Messager mMessager; //日志相关的辅助类
private Map<String, AnnotatedClass> mAnnotatedClassMap;
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
mFiler = processingEnv.getFiler();
mElementUtils = processingEnv.getElementUtils();
mMessager = processingEnv.getMessager();
mAnnotatedClassMap = new TreeMap<>();
} @Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
mAnnotatedClassMap.clear();
try {
processBindView(roundEnv);
} catch (IllegalArgumentException e) {
e.printStackTrace();
error(e.getMessage());
} for (AnnotatedClass annotatedClass : mAnnotatedClassMap.values()) {
try {
annotatedClass.generateFile().writeTo(mFiler);
} catch (IOException e) {
error("Generate file failed, reason: %s", e.getMessage());
}
}
return true;
} private void processBindView(RoundEnvironment roundEnv) throws IllegalArgumentException { for (Element element : roundEnv.getElementsAnnotatedWith(BindView.class)) {
AnnotatedClass annotatedClass = getAnnotatedClass(element);
BindViewField bindViewField = new BindViewField(element);
annotatedClass.addField(bindViewField);
}
} private AnnotatedClass getAnnotatedClass(Element element) {
TypeElement typeElement = (TypeElement) element.getEnclosingElement();
String fullName = typeElement.getQualifiedName().toString();
AnnotatedClass annotatedClass = mAnnotatedClassMap.get(fullName);
if (annotatedClass == null) {
annotatedClass = new AnnotatedClass(typeElement, mElementUtils);
mAnnotatedClassMap.put(fullName, annotatedClass);
}
return annotatedClass;
} private void error(String msg, Object... args) {
mMessager.printMessage(Diagnostic.Kind.ERROR, String.format(msg, args));
} @Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
} @Override
public Set<String> getSupportedAnnotationTypes() {
Set<String> types = new LinkedHashSet<>();
types.add(BindView.class.getCanonicalName());
return types;
}
}
原理是现解析保存被注解的类,然后再根据注解解析被注解的成员变量,进行保存,最后根据生成java类进行写文件
5.)app使用注解框架的应用
在build.gradle中添加如下配置
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
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:24.2.1'
testCompile 'junit:junit:4.12'
compile project(':app.api')
compile project(':app.annotation')
annotationProcessor project(':app.compiler')
}
Activity中使用
public class MainActivity extends AppCompatActivity {
@BindView(R.id.test)
Button mButton; @Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
LCJViewBinder.bind(this);
mButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(MainActivity.this, "success", Toast.LENGTH_SHORT).show();
}
});
} @Override
protected void onDestroy() {
super.onDestroy();
LCJViewBinder.unBind(this);
}
}
然后把项目重新build一下就会自动生成MainActivity$$ViewBinder类
public class MainActivity$$ViewBinder implements ViewBinder<MainActivity> {
@Override
public void bindView(MainActivity host, Object source, ViewFinder finder) {
host.mButton = (Button)(finder.findView(source, 2131427413));
} @Override
public void unBindView(MainActivity host) {
host.mButton = null;
}
}
总结:
通过注解生成代码在平时的开发过程中可能很少接触,因为目前很多开源框架帮我们处理了这部分,如果我们需要自己做一个使用注解的框架就需要这方面知识了,这个例子仅仅是我自己查找资源然后模仿做出来的,其实我们项目中业务组件化之间可以通过注解来声明路由scheme地址,后期有时间实现一下。
Android注解使用之通过annotationProcessor注解生成代码实现自己的ButterKnife框架的更多相关文章
- apt 根据注解,编译时生成代码
apt: @Retention后面的值,设置的为CLASS,说明就是编译时动态处理的.一般这类注解会在编译的时候,根据注解标识,动态生成一些类或者生成一些xml都可以,在运行时期,这类注解是没有的~~ ...
- Android高效率编码-findViewById()的蜕变-注解,泛型,反射
Android高效率编码-findViewById()的蜕变-注解,泛型,反射 Android的老朋友findViewById()篇! 先看看他每天是在干什么 //好吧,很多重复的,只不过想表达项目里 ...
- Android注解神器 ButterKnife框架
前言: 本人是一个只有几个月工作经验的码小渣.这是我写的第一篇博客,如有不足之处还请大家不要介意,还请大佬可以指出问题. 在这几个月的实战开发中自己也遇到了很多问题,真的是举步艰难啊!!! 在实战开发 ...
- Android开发学习之路-让注解帮你简化代码,彻底抛弃findViewById
本文主要是记录注解的使用的学习笔记,如有错误请提出. 在通常的情况下,我们在Activity中有一个View,我们要获得这个View的实例是要通过findViewById这个方法,然后这个方法返回的是 ...
- Android开发学习之路--Annotation注解简化view控件之初体验
一般我们在写android Activity的时候总是会在onCreate方法中加上setContentView方法来加载layout,通过findViewById来实现控件的绑定,每次写这么多代码总 ...
- android 中的一些资源注解,让编译器帮你检查代码
android 中的一些资源注解,让编译器帮你检查代码 写方便的时候可以用注解来声明一些参数,以明确的指示参数的类型,让代码更安全.我们看到,在android源代码里大量使用了注解.我整理了一些注解如 ...
- android Observable api请求参数设置注解问题
android Observable api请求参数设置注解问题 2018-10-29 20:05:24.919 11786-11786/xxx E/wxh: getQuote=USD getBase ...
- Atitit.注解and属性解析(2)---------语法分析 生成AST attilax总结 java .net
Atitit.注解and属性解析(2)---------语法分析 生成AST attilax总结 java .net 1. 应用场景:::因为要使用ui化的注解 1 2. 使用解释器方式来实现生成 ...
- 2016.7.14 generator基于注解和基于xml自动生成代码的区别
1.generator配置文件generatorConfig.xml的区别 2.生成代码的区别 注:二者的实体类都一样. (1)基于XML 生成的文件有: 后面省略. 也就是说,基于xml的方式,是要 ...
随机推荐
- web全栈开发之网站开发二(弹出式登录注册框前端实现-类腾讯)
这次给大家分享的是目前很多网站中流行的弹出式登录框,如下面的腾讯网登录界面,采用弹出式登录的好处是大大提升了网站的用户体验和交互性,用户不用重新跳转到指定的页面就能登录,非常方便 先来个演示地址 要实 ...
- $.extend()的实现源码 --(源码学习1)
目标: $.extend({ add:function(a,b){ return a + b; } }) console.log($.a ...
- webapi - 模型验证
本次要和大家分享的是webapi的模型验证,讲解的内容可能不单单是做验证,但都是围绕模型来说明的:首先来吐槽下,今天下午老板为自己买了套新办公家具,看起来挺好说明老板有钱,不好的是我们干技术的又成了搬 ...
- 一个技术汪的开源梦 —— 公共组件缓存之分布式缓存 Redis 实现篇
Redis 安装 & 配置 本测试环境将在 CentOS 7 x64 上安装最新版本的 Redis. 1. 运行以下命令安装 Redis $ wget http://download.redi ...
- C#中如何调整图像大小
在本篇文章中,我将介绍如何在C#中来调整你想要的图像大小.要实现这一目标,我们可以采取以下几个步骤: 1.首先要获取你想要调整大小的图像: string path = Server.MapPath(& ...
- fir.im Weekly - 关于 iOS10 适配、开发、推送的一切
"小程序"来了,微信变成名副其实的 Web OS,新一轮的Web App 与Native App争论四起.程序员对新技术永远保持灵敏的嗅觉和旺盛的好奇心,@李锦发整理了微信小程序资 ...
- jQuery可拖拽3D万花筒旋转特效
这是一个使用了CSS3立体效果的强大特效,本特效使用jQuery跟CSS3 transform来实现在用户鼠标按下拖动时,环形图片墙可以跟随鼠标进行3D旋转动画. 效果体验:http://hovert ...
- HTML5 标签 details 展开 搜索
details有一个新增加的子标签--summary,当鼠标点击summary标签中的内容文字时,details标签中的其他所有元素将会展开或收缩. 默认状态为 收缩状态 设置为展开状态为 <d ...
- js中的null 和undefined
参考链接:http://blog.csdn.net/qq_26676207/article/details/53100912 http://www.ruanyifeng.com/blog/2014/0 ...
- eclipse,myeclipse 误删文件,回滚历史文件操作
昨天因为误操作把一个写了一上午的代码给删了,找到的这个,以前竟然还没发现有这个功能- -! 具体操作: 1.建立同路径同名的文件 2.文件上右键 --> Compare With --> ...