APT案例之点击事件

3次阅读

共计 11125 个字符,预计需要花费 28 分钟才能阅读完成。

目录介绍

01. 创建项目步骤

1.1 项目搭建
1.2 项目功能

02. 自定义注解
03. 创建 Processor
04.compiler 配置文件
05. 编译 jar
06. 如何使用
07. 编译生成代码

08. 部分源码说明

8.1 Process 类 -process 方法
8.2 OnceProxyInfo 代理类
8.3 OnceMethod 类

好消息

博客笔记大汇总【16 年 3 月到至今】,包括 Java 基础及深入知识点,Android 技术博客,Python 学习笔记等等,还包括平时开发中遇到的 bug 汇总,当然也在工作之余收集了大量的面试题,长期更新维护并且修正,持续完善……开源的文件是 markdown 格式的!同时也开源了生活博客,从 12 年起,积累共计 N 篇 [近 100 万字,陆续搬到网上],转载请注明出处,谢谢!
链接地址:https://github.com/yangchong2…
如果觉得好,可以 star 一下,谢谢!当然也欢迎提出建议或者问题,万事起于忽微,量变引起质变!

关于 apt 实践与总结开源库地址
https://github.com/yangchong2…
00. 注解系列博客汇总
0.1 注解基础系列博客

01.Annotation 注解详细介绍
[02.Dagger2 深入分析,待更新]()

03. 注解详细介绍
什么是注解,注解分类有哪些?自定义注解分类?运行注解案例展示分析,以一个最简单的案例理解注解……使用注解替代枚举,使用注解限定类型

04.APT 技术详解
什么是 apt?理解注解处理器的作用和用途……android-apt 被替代?annotationProcessor 和 apt 区别?什么是 jack 编译方式?

06. 自定义 annotation 注解
@Retention 的作用?@Target(ElementType.TYPE) 的解释,@Inherited 注解可以被继承吗?Annotation 里面的方法为何不能是 private?

07. 注解之兼容 kotlin
后期更新

08. 注解之处理器类 Processor
处理器类 Processor 介绍,重要方法,Element 的作用,修饰方法的注解和 ExecutableElement,了解修饰属性、类成员的注解和 VariableElement……

10. 注解遇到问题和解决方案
无法引入 javax 包下的类库,成功运行一次,修改代码后再运行就报错

11. 注解代替枚举
在做内存优化时,推荐使用注解代替枚举,因为枚举占用的内存更高,如何说明枚举占用内存高呢?这是为什么呢?

12. 注解练习案例开源代码
注解学习小案例,比较系统性学习注解并且应用实践。简单应用了运行期注解,通过注解实现了 setContentView 功能;简单应用了编译器注解,通过注解实现了防暴力点击的功能,同时支持设置时间间隔;使用注解替代枚举;使用注解一步步搭建简单路由案例。结合相应的博客,在来一些小案例,从此应该对注解有更加深入的理解……

13 ARouter 路由解析
比较详细地分析了阿里路由库

14 搭建路由条件
为何需要路由?实现路由方式有哪些,这些方式各有何优缺点?使用注解实现路由需要具备的条件以及简单原理分析……

15 通过注解去实现路由跳转
自定义 Router 注解,Router 注解里有 path 和 group,这便是仿照 ARouter 对路由进行分组。然后看看注解生成的代码,手写路由跳转代码。

16 自定义路由 Processor 编译器
Processor 介绍,重要方法,Element 的作用,修饰方法的注解和 ExecutableElement

17 利用 apt 生成路由映射文件

在 Activity 类上加上 @Router 注解之后,便可通过 apt 来生成对应的路由表,那么究竟是如何生成的代码呢?
在组件化开发中,有多个 module,为何要在 build.gradle 配置 moduleName,又是如何通过代码拿到 module 名称?
process 处理方法如何生成代码的,又是如何写入具体的路径,写入文件的?
看完这篇文章,应该就能够理解上面这些问题呢!

18 路由框架的设计和初始化

编译期是在你的项目编译的时候,这个时候还没有开始打包,也就是你没有生成 apk 呢!路由框架在这个时期根据注解去扫描所有文件,然后生成路由映射文件。这些文件都会统一打包到 apk 里,app 运行时期做的东西也不少,但总而言之都是对映射信息的处理,如执行执行路由跳转等。那么如何设计框架呢?
生成的注解代码,又是如何把这些路由映射关系拿到手,或者说在什么时候拿到手比较合适?为何注解需要进行初始化操作?
如何得到得到路由表的类名,如何得到所有的 routerAddress—activityClass 映射关系?

[19 路由框架设计注意要点]()
需要注意哪些要点?

20 为何需要依赖注入
有哪些注入的方式可以解耦,你能想到多少?路由框架为何需要依赖注入?路由为何用注解进行依赖注入,而不是用反射方式注入,或者通过构造方法注入,或者通过接口方式注入?

21 Activity 属性注入
在跳转页面时,如何传递 intent 参数,或者如何实现跳转回调处理逻辑?

01. 创建项目步骤
1.1 项目搭建

首先创建一个 Android 项目。然后给我们的项目增加一个 module, 一定要记得是 Java Library。因为 APT 需要用到 jdk 下的【javax.~】包下的类,这在 AndroidSdk 中是没有的。
一定要注意:需要说明的是:我们的目的是写一个 Android 库,APT Moudle 是 java Library,不能使用 Android API。所以还需要创建一个 Android Library,负责框架主体部分. 然后由 Android Library 引用 APT jar 包。

项目目录结构如图:

app:Demo
AptAnnotation:java Library 主要放一些项目中需要用到的自定义注解及相关代码
AptApi:Android Library. OnceClick 是我们真正对外发布并交由第三方使用的库,它引用了 apt-jar 包
AptCompiler:java Library 主要是应用 apt 技术处理注解,生成相关代码或者相关源文件,是核心所在。

1.2 项目功能
在一定时间内,按钮点击事件只能执行一次。未到指定时间,不执行点击事件。
02. 自定义注解

创建 Annotation Module,需要创建一个 Java Library,名称可为 annotation,主要放一些项目中需要用到的自定义注解及相关代码

新建一个类,OnceClick。就是我们自定义的注解。
/**
* <pre>
* @author 杨充
* blog : https://github.com/yangchong211
* time : 2017/06/21
* desc : 一定 time 时间内该点击事件只能执行一次
* revise:

*/
//@Retention 用来修饰这是一个什么类型的注解。这里表示该注解是一个编译时注解。
@Retention(RetentionPolicy.CLASS)
//@Target 用来表示这个注解可以使用在哪些地方。
// 比如:类、方法、属性、接口等等。这里 ElementType.METHOD 表示这个注解可以用来修饰:方法
@Target(ElementType.METHOD)
// 这里的 interface 并不是说 OnceClick 是一个接口。就像申明类用关键字 class。申明注解用的就是 @interface。
public @interface OnceClick {
// 返回值表示这个注解里可以存放什么类型值
int value();
}
“`

03. 创建 Processor

创建 Compiler Module,需要再创建一个 Java Library,名称可为 compiler,主要是应用 apt 技术处理注解,生成相关代码或者相关源文件,是核心所在。

Processor 是用来处理 Annotation 的类。继承自 AbstractProcessor。
/**
* <pre>
* @author 杨充
* blog : https://github.com/yangchong211
* time : 2017/06/21
* desc : 自定义 Processor 编译器
* revise:

*/
@SupportedSourceVersion(SourceVersion.RELEASE_7)
public class OnceClickProcessor extends AbstractProcessor {

private Messager messager;
private Elements elementUtils;

@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
messager = processingEnv.getMessager();
elementUtils = processingEnv.getElementUtils();
}

@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
// 获取 proxyMap
Map<String, OnceProxyInfo> proxyMap = getProxyMap(roundEnv);
// 遍历 proxyMap,并生成代码
for (String key : proxyMap.keySet()) {
OnceProxyInfo proxyInfo = proxyMap.get(key);
writeCode(proxyInfo);
}
return true;
}

@Override
public Set<String> getSupportedAnnotationTypes() {
Set<String> types = new LinkedHashSet<>();
types.add(OnceClick.class.getCanonicalName());
return types;
}

@Override
public SourceVersion getSupportedSourceVersion() {
return super.getSupportedSourceVersion();
}
}
“`

04.compiler 配置文件

build.gradle 文件配置

auto-service 的作用是向系统注册 processor(自定义注解处理器),执行编译时使用 processor 进行处理。
javapoet 提供了一套生成 java 代码的 api,利用这些 api 处理注解,生成新的代码或源文件。
OnceClickAnnotation 是上文创建的注解 module。

apply plugin: ‘java-library’

dependencies {
implementation fileTree(dir: ‘libs’, include: [‘*.jar’])
implementation ‘com.google.auto.service:auto-service:1.0-rc3’
implementation ‘com.squareup:javapoet:1.10.0’
implementation project(‘:OnceClickAnnotation’)
}

sourceCompatibility = “7”
targetCompatibility = “7”

05. 编译 jar

这里有一个坑,主 Module 是不可以直接引用这个 java Module 的。(直接引用,可以成功运行一次~ 修改代码以后就不能运行了)而如何单独编译这个 java Module 呢?在编译器 Gradle 视图里,找到 Module apt 下的 build 目录下的 Build 按钮。双击运行。
代码没有问题编译通过的话,会有 BUILD SUCCESS 提示。生成的 jar 包在 apt 下的 build 目录下的 libs 下。将 apt.jar 拷贝到 app 下的 libs 目录,右键该 jar,点击 Add as Library,添加 Library

06. 如何使用

代码如下所示
public class MainActivity extends AppCompatActivity {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 初始化 OnceClick, 并设置点击事件间隔是 2 秒
OnceInit.once(this,2000);
}

@OnceClick(R.id.tv_1)
public void Click1(){
Log.d(“tag——————–“,”tv_1”);
}

@OnceClick(R.id.tv_2)
public void Click2(View v){
Log.d(“tag——————–“,”tv_2”);
}
}

07. 编译生成代码

编译之后生成的代码路径,在项目中的 build 文件夹,如图所示

编译之后生成的代码
// 编译生成的代码,不要修改
// 更多内容:https://github.com/yangchong211
package com.ycbjie.ycapt;

import android.view.View;
import com.ycbjie.api.Finder;
import com.ycbjie.api.AbstractInjector;

public class MainActivity$$_Once_Proxy<T extends MainActivity> implements AbstractInjector<T> {

public long intervalTime;

@Override
public void setIntervalTime(long time) {
intervalTime = time;
}

@Override
public void inject(final Finder finder, final T target, Object source) {
View view;
view = finder.findViewById(source, 2131165325);
if(view != null){
view.setOnClickListener(new View.OnClickListener() {
long time = 0L;
@Override
public void onClick(View v) {
long temp = System.currentTimeMillis();
if (temp – time >= intervalTime) {
time = temp;
target.Click1();
}
}});
}
view = finder.findViewById(source, 2131165326);
if(view != null){
view.setOnClickListener(new View.OnClickListener() {
long time = 0L;
@Override
public void onClick(View v) {
long temp = System.currentTimeMillis();
if (temp – time >= intervalTime) {
time = temp;
target.Click2(v);
}
}});
}
}

}

08. 部分源码说明
8.1 Process 类 -process 方法

当某个类 Activity 使用了 @OnceClick 注解之后,我们就应该为其生成一个对应的代理类,代理类实现我们框架的功能:为某个 View 设置点击事件,并且这个点击事件一定时间内只能执行一次。所以,一个代理类可能有多个需要处理的 View。

先看 process 代码:

ProxyInfo 对象:存放生成代理类的必要信息,并生成代码。
getProxyMap 方法:使用参数 roundEnv,遍历所有 @OnceClick 注解,并生成代理类 ProxyInfo 的 Map。
writeCode 方法:真正生成代码的方法。
总结一下:编译时,取得所有需要生成的代理类信息。遍历代理类集合,根据代理类信息,生成代码。

@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
// 获取 proxyMap
Map<String, OnceProxyInfo> proxyMap = getProxyMap(roundEnv);
// 遍历 proxyMap,并生成代码
for (String key : proxyMap.keySet()) {
OnceProxyInfo proxyInfo = proxyMap.get(key);
// 写入代码
writeCode(proxyInfo);
}
return true;
}

8.2 OnceProxyInfo 代理类

其实这个类,才是这个框架的重中之重,因为生成什么代码,全靠这个类说了算。这个类也没什么好讲的,就是用 StringBuidler 拼出一个类来。ProxyInfo 保存的是类信息,方法信息我们用 List methods 保存。然后根据这些信息生成类。
public class OnceProxyInfo {

private String packageName;
private String targetClassName;
private String proxyClassName;
private TypeElement typeElement;
private List<OnceMethod> methods;
private static final String PROXY = “_Once_Proxy”;

OnceProxyInfo(String packageName, String className) {
this.packageName = packageName;
this.targetClassName = className;
this.proxyClassName = className + “$$” + PROXY;
}

String getProxyClassFullName() {
return packageName + “.” + proxyClassName;
}

String generateJavaCode() throws OnceClickException {

StringBuilder builder = new StringBuilder();
builder.append(“// 编译生成的代码,不要修改 \n”);
builder.append(“// 更多内容:https://github.com/yangchong211\n”);
builder.append(“package “).append(packageName).append(“;\n\n”);

// 写入导包
builder.append(“import android.view.View;\n”);
builder.append(“import com.ycbjie.api.Finder;\n”);
builder.append(“import com.ycbjie.api.AbstractInjector;\n”);
builder.append(‘\n’);

builder.append(“public class “).append(proxyClassName)
.append(“<T extends “).append(getTargetClassName()).append(“>”)
.append(” implements AbstractInjector<T>”).append(” {\n”);
builder.append(‘\n’);

generateInjectMethod(builder);
builder.append(‘\n’);

builder.append(“}\n”);
return builder.toString();

}

private String getTargetClassName() {
return targetClassName.replace(“$”, “.”);
}

private void generateInjectMethod(StringBuilder builder) throws OnceClickException {
builder.append(” public long intervalTime; \n”);
builder.append(‘\n’);

builder.append(” @Override \n”)
.append(” public void setIntervalTime(long time) {\n”)
.append(” intervalTime = time;\n} \n”);
builder.append(‘\n’);

builder.append(” @Override \n”)
.append(” public void inject(final Finder finder, final T target, Object source) {\n”);
builder.append(” View view;”);
builder.append(‘\n’);

// 这一步是遍历所有的方法
for (OnceMethod method : getMethods()) {
builder.append(” view = “)
.append(“finder.findViewById(source, “)
.append(method.getId())
.append(“);\n”);
builder.append(” if(view != null){\n”)
.append(” view.setOnClickListener(new View.OnClickListener() {\n”)
.append(” long time = 0L;\n”);
builder.append(” @Override\n”)
.append(” public void onClick(View v) {\n”);
builder.append(” long temp = System.currentTimeMillis();\n”)
.append(” if (temp – time >= intervalTime) {\n” +
” time = temp;\n”);
if (method.getMethodParametersSize() == 1) {
if (method.getMethodParameters().get(0).equals(“android.view.View”)) {
builder.append(” target.”)
.append(method.getMethodName()).append(“(v);”);
} else {
throw new OnceClickException(“Parameters must be android.view.View”);
}
} else if (method.getMethodParametersSize() == 0) {
builder.append(” target.”)
.append(method.getMethodName()).append(“();”);
} else {
throw new OnceClickException(“Does not support more than one parameter”);
}
builder.append(“\n}\n”)
.append(”}”)
.append(“});\n }\n”);
}

builder.append(”}\n”);
}

TypeElement getTypeElement() {
return typeElement;
}

void setTypeElement(TypeElement typeElement) {
this.typeElement = typeElement;
}

List<OnceMethod> getMethods() {
return methods == null ? new ArrayList<OnceMethod>() : methods;
}

void addMethod(OnceMethod onceMethod) {
if (methods == null) {
methods = new ArrayList<>();
}
methods.add(onceMethod);
}
}

8.3 OnceMethod 类

需要讲的一点是,每一个使用了 @OnceClick 注解的 Activity 或 View,都会为其生成一个代理类,而一个代理中有可能有很多个 @OnceClick 修饰的方法,所以我们专门为每个方法有创建了一个 javaBean 用于保存方法信息:
public class OnceMethod {

private int id;
private String methodName;
private List<String> methodParameters;

OnceMethod(int id, String methodName, List<String> methodParameters) {
this.id = id;
this.methodName = methodName;
this.methodParameters = methodParameters;
}

int getMethodParametersSize() {
return methodParameters == null ? 0 : methodParameters.size();
}

int getId() {
return id;
}

String getMethodName() {
return methodName;
}

List<String> getMethodParameters() {
return methodParameters;
}

}

关于其他内容介绍
01. 关于博客汇总链接

1. 技术博客汇总

2. 开源项目汇总

3. 生活博客汇总

4. 喜马拉雅音频汇总

5. 其他汇总

02. 关于我的博客

我的个人站点:www.yczbj.org,www.ycbjie.cn
github:https://github.com/yangchong211

知乎:https://www.zhihu.com/people/…

简书:http://www.jianshu.com/u/b7b2…

csdn:http://my.csdn.net/m0_37700275

喜马拉雅听书:http://www.ximalaya.com/zhubo…

开源中国:https://my.oschina.net/zbj161…

泡在网上的日子:http://www.jcodecraeer.com/me…

邮箱:yangchong211@163.com
阿里云博客:https://yq.aliyun.com/users/a… 239.headeruserinfo.3.dT4bcV
segmentfault 头条:https://segmentfault.com/u/xi…

掘金:https://juejin.im/user/593943…

关于 apt 实践与总结开源库地址
https://github.com/yangchong2…

正文完
 0