一、项目工程介绍

  • lib-annotation 是一个 Java Library 模块,主要用于自定义注解;
  • lib-compiler 是一个 Java Library 模块,需要依赖 lib-annotation 模块,主要用于解析自定义注解与生成源文件。lib-compiler 还需要依赖 3 个开源库来帮助开发;
    • auto-common/auto-service:为注解处理器自动生成 metadata 文件并将注解处理器 jar 文件加入构建路径,不再需要我们手动创建并更新 META-INF/services/javax.annotation.processing.Processor 文件;
    • javapoet:一款 Java 代码生成框架,可以令我们省去繁琐冗杂的拼接代码的重复工作。
  • lib-inject 是一个 Android Library 模块,需要依赖 lib-annotation 模块,主要用于提供 Api 给 app 模块调用;
  • app 为应用模块,依赖 lib-compiler 与 lib-inject;

二、lib-annotation-自定义注解模块

创建一个自定义注解类BindView

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.CLASS)
public @interface BindView {int value();
}
复制代码
  • @Target(ElementType.FIELD)表示该注解修饰的是成员变量;
  • @Retention(RetentionPolicy.CLASS)表示该注解只会在编译时使用;
  • int value()为注解的值,这里应该传入的是一个控件 id;

三、lib-compiler-注解处理器模块

首先在build.gradle里添加依赖

dependencies {api project(':lib-annotation')implementation 'com.google.auto:auto-common:0.8'implementation 'com.google.auto.service:auto-service:1.0-rc3'implementation 'com.squareup:javapoet:1.9.0'
}
复制代码

然后创建一个类 BindViewProcessor,通过继承 AbstractProcessor 来自定义注解处理器,继承 AbstractProcessor 要实现一个抽象方法process()

public class BindViewProcessor extends AbstractProcessor {@Overridepublic boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {return false;}
}
复制代码

这里我们先不理会这个方法,先做一些准备工作

第一步,我们需要注册 BindViewProcessor,之前我们已经添加了 auto-service 这个库,那么注册就是一个注解的事,使用@AutoService(Processor.class)

@AutoService(Processor.class)
public class BindViewProcessor extends AbstractProcessor { }
复制代码

第二步,我们需要声明支持的 Java 版本,这里有两种方式,一种是重写getSupportedSourceVersion(),一种是使用注解@SupportedSourceVersion()

// 重写方法
@Override
public SourceVersion getSupportedSourceVersion() {return SourceVersion.latestSupported();
}// 使用注解
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class BindViewProcessor extends AbstractProcessor { }
复制代码

SourceVersion 是一个枚举类,可以使用SourceVersion.RELEASE_0SourceVersion.RELEASE_8表示各个 Java 版本,也可以直接使用SourceVersion.latestSupported()表示最新的版本

第三步,我们需要声明自定义注解处理器要处理哪些注解,同样的,这里也有两种方式,一种是重写getSupportedAnnotationTypes(),一种是使用注解@SupportedAnnotationTypes()

// 重写方法
@Override
public Set<String> getSupportedAnnotationTypes() {Set<String> set = new LinkedHashSet<>();set.add(BindView.class.getCanonicalName());return set;
}// 使用注解-传入注解的全类名
@SupportedAnnotationTypes({"com.fancyluo.lib_annotation.BindView"})
public class BindViewProcessor extends AbstractProcessor { }
复制代码

第四步,我们需要重写init()方法来获取一些辅助类

// 解析 Elementm 的工具类,主要用于获取包名
private Elements mElementUtils;
// 主要用于输出 Java 源文件
private Filer mFiler;@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {super.init(processingEnvironment);mElementUtils = processingEnvironment.getElementUtils();mFiler = processingEnvironment.getFiler();
}
复制代码

第五步,这里要重新拿起之前忽略的process()方法,这个方法是重中之重,我们要在这里面解析自定义注解和生成 Java 源文件。

先来看看我们要生成什么样的代码

public class MainActivity$$ViewBinder<T extends MainActivity> implements ViewBinder<MainActivity> {@Overridepublic void bind(final MainActivity target) {target.btnAction=(Button)target.findViewById(2131165218);}
}
复制代码

当我们使用BindView修饰程序元素的时候,我们的自定义注解处理器就可以拿到相应的程序元素的节点,通过解析节点,拿到相应的数据,然后自动的为这个程序元素所在的类生成一个辅助类,在里面为程序元素赋值。

也可以这么理解,我们会为使用BindView修饰的控件所在的 Activity 自动的生成一个辅助类,在里面进行控件的findViewById

接下来的代码都是在process()方法里,只是我将其分拆出来讲解

@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {...//代码下面讲解return false;
}
复制代码

首先,我们通过 roundEnvironment 拿到所有的被BindView修饰的节点

Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(BindView.class);
复制代码

这里可以理解为一个控件,只是被封装转换成了 Element

@BindView(R.id.btnAction)
Button btnAction;转换成 -> Element
复制代码

然后遍历 elements 集合,解析数据,将我们需要的数据封装成一个类,并按照 TypeElement 来进行分组。TypeElement 可以理解为类节点,而 Element 是成员节点,再具体来说,TypeElement 就是 MainActivity,而 Element 就是其中的 btnAction;那么,按照 TypeElement 分组也就是将控件按照其所在的 Activity 进行分组。

首先创建我们需要的数据封装类BindViewInfo

public class FieldBinding {// 可以理解为:Button 这个类型private TypeMirror typeMirror;// 可以理解为:成员变量名-btnActionprivate String name;// 可以理解为:Button 的 id-R.id.btnActionprivate int resId;...}
复制代码

开始遍历集合,并且将节点数据封装到 BindViewInfo,并将其分组保存到 Map 集合

// Key 为类型节点,可以理解为 MainActivity
// Value 可以理解为 MainActivity 里面所有被 BindView 注解的成员变量信息
Map<TypeElement, List<BindViewInfo>> cacheMap = new HashMap<>();// 遍历所有被 BindView 注解的成员变量,按照 Activity 进行分组
for (Element element : elements) {// 得到类型节点,可以理解为得到MainActivityTypeElement enclosingElement = (TypeElement) element.getEnclosingElement();// 从缓存中获取数据,如果没有,则新建并添加到缓存List<BindViewInfo> fieldList = cacheMap.get(enclosingElement);if (fieldList == null) {fieldList = new ArrayList<>();cacheMap.put(enclosingElement, fieldList);}// 封装被 BindView 注解的成员变量的信息// 成员变量的类类型,例如 ButtonTypeMirror typeMirror = element.asType();// 成员变量名 例如 btnActionString fieldName = element.getSimpleName().toString();// 控件资源Id 例如 R.id.btnint resId = element.getAnnotation(BindView.class).value();BindViewInfo bindViewInfo = new BindViewInfo(typeMirror, fieldName, resId);fieldList.add(fieldBinding);
}
复制代码

将数据分好组缓存后,我们就可以来构建我们需要的 Java 源文件的代码了,前面说过,TypeElement 代表着一个Activity,而 List<BindViewInfo>就代表着里面使用 BindView 注解修饰的控件,我们要为 Activity 生成一个辅助类,在里面为这些控件生成 findViewById 代码

首先,我们遍历 cacheMap,并解析我们需要的数据

for (Map.Entry<TypeElement, List<FieldBinding>> entry : cacheMap.entrySet()) {List<FieldBinding> bindingList = entry.getValue();// 如果该Activity没有被BindView注解的成员变量,则执行下一个if (bindingList == null || bindingList.size() == 0) {continue;}// 获取类型节点 例如 MainActivityTypeElement typeElement = entry.getKey();// 获取包名 例如 com.fancyluo.k_butterknifeString packageName = getPackageName(typeElement);// 获取类名 例如 MainActivityString classNameStr = getClassName(packageName, typeElement);ClassName classNamePackage = ClassName.bestGuess(classNameStr);// 获取ViewBinderClassName viewBinder = ClassName.get("com.fancyluo.lib_inject", "ViewBinder");...//代码下面讲解
}
复制代码

getPackageName(typeElement)

private String getPackageName(TypeElement enClosingElement) {// 获取包节点PackageElement packageElement = mElementUtils.getPackageOf(typeElement);//返回的是 com.fancyluo.k_butterknifereturn packageElement.getQualifiedName().toString();
}
复制代码

getClassName(packageName, typeElement)

// 例如 com.fancyluo.k_butterknife.MainActivity
String qualifiedName = typeElement.getQualifiedName().toString();
// 例如 com.fancyluo.k_butterknife.
int length = packageName.length() + 1;
// 如果当前的TypeElement是内部类的话,裁剪掉包名和后面的点号,并将之后的点号替换为$
return qualifiedName.substring(length).replace(".", "$");
复制代码

ViewBinder是在lib_inject模块里定义的一个接口,我们生成的辅助类需要实现这个接口并且实现接口的bind()方法进行控件的findViewById

拿到我们需要的数据以后,就可以开始使用 javapoet 提供的 api 来构建 Java 源代码,下面,我们再来贴一下我们要生成的代码,然后我们会一步一步来构建这些代码。

public class MainActivity$$ViewBinder<T extends MainActivity> implements ViewBinder<MainActivity> {@Overridepublic void bind(final MainActivity target) {target.btnAction=(Button)target.findViewById(2131165218);}
}
复制代码

首先,我们要构建

TypeSpec.Builder typeBuilder = TypeSpec.classBuilder(classNameStr + "$$ViewBinder").addModifiers(Modifier.PUBLIC).addTypeVariable(TypeVariableName.get("T", classNamePackage)).addSuperinterface(ParameterizedTypeName.get(viewBinder, classNamePackage));
复制代码
  • classBuilder 里传入的是类名
  • addModifiers 是设置类的访问属性
  • addTypeVariable 是设置类的泛型参数,传入一个 TypeVariableName,TypeVariableName 第一个参数为泛型参数名,第二个参数为 ClassName,例如 T extends MainActivity
  • addSuperinterface 是设置当前类实现的接口,传入一个 ParameterizedTypeName,ParameterizedTypeName 第一个参数为父接口的 ClassName,第二个参数 ClassName,例如 ViewBinder<MainActivity>

这里就相当于构建了

public class MainActivity$$ViewBinder<T extends MainActivity> implements ViewBinder<MainActivity> {
}
复制代码

第二,我们要构建方法

MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder("bind")//方法名.addAnnotation(Override.class)//添加注解.addModifiers(Modifier.PUBLIC)//访问属性.returns(TypeName.VOID)// 返回值// 添加参数:1-ClassName 2-参数名  3-参数的访问权限.addParameter(classNamePackage, "target", Modifier.FINAL);
复制代码

构建完方法的基本元素后,现在的代码结构为

public class MainActivity$$ViewBinder<T extends MainActivity> implements ViewBinder<MainActivity> {@Overridepublic void bind(final MainActivity target) {...}
}
复制代码

最后我们来构建方法里面的具体代码,也就是相应控件的 findViewById

for (BindViewInfo bindViewInfo : bindingList) {// 获取类型名称,例如 ButtonString packageNameStr = fieldBinding.getTypeMirror().toString();ClassName className = ClassName.bestGuess(packageNameStr);// $L/$T代表占位符,$L为基本类型  $T为类类型// 这里相当于生成了 target.btnAction=(Button)target.findViewById(2131165218);methodBuilder.addStatement("target.$L=($T)target.findViewById($L)",fieldBinding.getName(),className,fieldBinding.getResId());
}
复制代码

方法完全构建完成后,我们将其添加到类里面

typeBuilder.addMethod(methodBuilder.build());
复制代码

最后,我们通过 Filer 类来生成 Java 源文件

try {//生成Java文件,最终写是通过filer类写出的JavaFile.builder(packageName,result.build()).addFileComment("auto create make").build().writeTo(filer);
} catch (IOException e) {e.printStackTrace();
}
复制代码

四、lib-inject-核心 Api 模块

定义一个ViewBinder接口,之前说过,这个接口是给注解处理器自动生成的类来实现的,然后在其bind()方法里面实现 findViewById 代码

public interface ViewBinder<T> {void bind(T target);
}
复制代码

接下来,定义一个核心类,其中的静态方法bind()会传入要绑定的 Activity,通过这个 Activity 的类名在运行时反射获取到注解处理器生成的对应的辅助类,然后调用辅助类的bind方法完成控件的 findViewById

public class KButterKnife {public static void bind(Activity activity) {String className = activity.getClass().getName();try {Class<?> clazz = Class.forName(className+"$$ViewBinder");ViewBinder viewBinder = (ViewBinder) clazz.newInstance();viewBinder.bind(activity);} catch (ClassNotFoundException e) {e.printStackTrace();} catch (IllegalAccessException e) {e.printStackTrace();} catch (InstantiationException e) {e.printStackTrace();}}}
复制代码

五、App-应用层

最后来测试使用一下,首先, 要依赖 lib-compiler 模块与 lib-inject 模块

implementation project(':lib-inject')
// lib-compiler 为注解处理器
annotationProcessor project(':lib-compiler')
复制代码

然后在 Activity 里面使用

public class MainActivity extends AppCompatActivity {@BindView(R.id.btnAction)Button btnAction;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);KButterKnife.bind(this);btnAction.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {Toast.makeText(MainActivity.this, "注入成功,哈哈哈", Toast.LENGTH_SHORT).show();}});}
}
复制代码

查看生成的源文件

一步步带你实现简版 ButterKnife相关推荐

  1. Dockerfile 简版大全,附赠编写实例

    基础镜像可以用于创建Docker容器.镜像可以非常基础,仅仅包含操作系统:也可以非常丰富,包含灵巧的应用栈,随时可以发布.当你在使用Docker构建镜像的时候,每一个命令都会在前一个命令的基础上形成一 ...

  2. python3web库_基于 Python3 写的极简版 webserver

    基于 Python3 写的极简版 webserver.用于学习 HTTP协议,及 WEB服务器 工作原理.笔者对 WEB服务器 的工作原理理解的比较粗浅,仅是基于个人的理解来写的,存在很多不足和漏洞, ...

  3. Underscore源码阅读极简版入门

    看了网上的一些资料,发现大家都写得太复杂,让新手难以入门.于是写了这个极简版的Underscore源码阅读. 源码: github.com/hanzichi/un- 一.架构的实现 1.1:架构 (f ...

  4. 《重大人生启示录》极简版

    <重大人生启示录>极简版 献给所有活着和将要死去的人们,献给所有历经悲伤的人们 本文摘录了启示录最重要的内容,不必非要看全本,不必购买书 序言 这是极为特殊的历史转折期,物质文明发展到这一 ...

  5. estore简版商城疑点思考

    estore简版商城疑点思考 一.疑点 1.疑点一:[解决] 读取  web应用中的 资源 文件 解答: 本人博文:javaweb读取 web 应用中的资源文件 http://blog.csdn.ne ...

  6. Atlas 200 DK开发者套件环境部署(1.0.9.alpha)极简版

    Atlas 200 DK开发者套件环境部署(1.0.9.alpha)极简版 前言 Atlas 200 DK开发者套件介绍 环境部署介绍 资源要求 开发环境部署 安装Docker 获取镜像(两种方法任选 ...

  7. 公司网站营销方案(简版)

    公司网站营销方案(简版) 前言: 随着网络的普及,以及人们文化素质的提高,上网是大多数人生活中不可缺少的一部分,同时有相当一大部分的人,积极的参与网上的各种活动,现在越来越多的人,希望利用业余的时间, ...

  8. [置顶]完美简版学生信息管理系统(附有源码)管理系统

    简版学生信息管理系统 目前为止找到的简版系统中最新.最全的java类管理系统 点击进入简版系统 如果无法直接连接,请进入: https://blog.csdn.net/weixin_43419816/ ...

  9. 7句话让Codex给我做了个小游戏,还是极简版塞尔达,一玩简直停不下来

    点击上方"视学算法",选择加"星标"或"置顶" 重磅干货,第一时间送达 梦晨 萧箫 发自 凹非寺 量子位 | 公众号 QbitAI 什么,7 ...

最新文章

  1. python 设计 实践_python实践设计模式(一)概述和工厂模式
  2. 直播 | SemEval-2020自由文本关系抽取冠军方案解读(附NLP竞赛常用技巧总结)
  3. 十分钟搞定 C/C++ 项目自动化构建 —— Xmake 入门指南
  4. postgresql 分词_使用PostgreSQL进行中文全文检索
  5. Spring Security –在一个应用程序中有两个安全领域
  6. java关键字值transient
  7. 疯狂python讲义pdf_重磅!Python再次第一,Java和C下降,凭什么?
  8. Django(五):视图和路由系统
  9. 南阳理工acm括号配对问题
  10. 【华为 OJ 】进制转换
  11. 【学习笔记】尚硅谷大数据项目之Flink实时数仓---数据采集
  12. 郑义宣就任韩国现代汽车集团会长;爱立信携手中国电信运用爱立信频谱共享技术 | 美通企业日报...
  13. Java中利用compareTo方法进行字符串比较排序
  14. 测试员一份工作坚持多久跳槽,才能完美提升薪资?
  15. filter 无效不起作用
  16. ANSYS下载安装+使用学习过程
  17. 史上最全4S店维修潜规则 看完绝不被坑
  18. 网站代码sql注入攻击漏洞修复加固防护措施
  19. 科学家研发真实版的《星际迷航》牵引光束
  20. Hadoop系列-MapReduce设计思想与原理机制(九)

热门文章

  1. inner join 和 exists 效率_19 个让 MySQL 效率提高 3 倍的 SQL 优化技巧
  2. PHP在线无人值守源码交易网站源码,集成支付宝微信接口
  3. tinkphp1.0贺岁版小程序应用平台系统源码
  4. 小清新自适应宇航员404页面丢失svg错误网页源码
  5. HTML5营销代理商设计机构网站模板
  6. WebStorm打开设置界面
  7. android过滤数字,android – GPS卫星数量和位置过滤
  8. H2O_Hyper_V-master网页端管理程序源码
  9. jQuery: 选择器(DOM,name,属性,元素)
  10. Magento数据库结构:EAV