越是深入学习 Android ,就越发感觉到 Gradle 这个构建工具十分强大, Android 插件化都是依赖于 Gradle ,因此有必要学会怎么用 Gradle 来编写插件,从而进一步去理解 Gradle 的自动化构建过程。
由于我同时对 AspectJ 十分感兴趣 ,这里就总结一下我是如何把 AspectJ 做成一个 Gradle 插件 的过程。
Gradle 插件开发
首先新建一个 Java Library Module
,然后手动将工程结构修改为 Groovy
工程结构,也就是将原来的 main 文件夹下的 java 文件夹修改为 groovy :
同时在 Module
里面的 build.gradle
中引入插件开发所需的 gradle
和 groovy
这两个 SDK 的依赖,并把 Module
插件修改为 groovy
和 maven
:
apply plugin: 'groovy'
apply plugin: 'maven'
dependencies {
compile gradleApi()//gradle sdk
compile localGroovy()//groovy sdk
}
//这里建议使用 Java7
sourceCompatibility = "1.7"
targetCompatibility = "1.7"
repositories {
jcenter()
}
这里也没有什么地方需要特别注意的,毕竟要弄成本地插件,所以需要声明 maven
,还有就是建议这里声明 Java 的版本为 1.7 版本,虽说我电脑安装的环境是 Java 1.8 ,但是发现声明为 1.8 会经常出现编译问题,因此建议这里就声明 1.7 吧。
然后就是在 main
目录下面增加 resources/META-INF/gradle-plugins
这样的文件夹目录结构:
暂时就先让最后的 gradle-plugins 文件夹空着,等下再做处理,那么现在整个 Module
的目录如下:
这样,一个基于 Groovy 的 Gradle
插件 Module
就完成了,我们可以来创建一个 groovy 文件来玩耍了。
在包名下新建一个文件,然后手动将文件后缀名修改为 groovy ,是的,你没有看错,就是这么手动去创建的,目前还没发现有其他什么法子可以让 Android Studio 来自动创建 groovy 文件,就先这么干吧:
新创建的 groovy 需要实现 org.gradle.api.Plugin
接口才可以编译成 Gradle
插件,如下所示:
import org.gradle.api.Plugin
import org.gradle.api.Project
public class AspectjPlugin implements Plugin<Project> {
void apply(Project project) {
println("=============")
println("hello world!")
println("=============")
}
}
一开始就不需要做什么,就只需要和 Gradle
插件打个招呼。
还记得之前的 resources\META-INF\gradle-plugins
路径吗?现在我们需要在里面创建一个 .properties
文件了,而文件名就是这个插件的 id 。如下所示:
然后需要在 aspectj.properties
文件里面指向我们的 groovy
文件:
implementation-class=com.fritz.groovy.plugin.AspectjPlugin
这时候已经自定义好了插件,接下来就是要打包到本地或远程仓库了,我们在 build.gradle
里面添加下面的代码:
//生成插件的包名
group='com.fritz.plugin'
//插件的版本号
version='1.0.0'
//插件每次修改都需要重新打包
uploadArchives {
repositories {
mavenDeployer {
//提交到远程服务器
// repository(url: "http://www.xxx.com/xxx") {
// authentication(userName: "admin", password: "admin")
// }
//本地仓库的地址设置为项目根目录的:/repos
repository(url: uri('../repo'))
}
}
}
等待 Gradle 重新编译完成后,就可以看到右边的 Gradle 构建列表里面这个命令了:
点击运行,即可生成本地的 Gradle
插件文件:
可以看到本地已经生成了 jar 的插件文件了,其中 \com\fritz\plugin 是有 group
指定的,第二个 plugin 就是生成插件的模块名。
打开压缩包就可以看到里面的编辑生成文件:
现在插件已经生成了,是时候使用它了,到项目的根目录下的 build.gradle
里面添加本地插件的依赖路径:
buildscript {
repositories {
google()
jcenter()
//添加本地仓库的路径
maven{
url uri('repo')
}
}
dependencies {
classpath "com.android.tools.build:gradle:3.0.1"
//这里就是插件所在的包名:生成插件的模块名:插件的版本号
classpath 'com.fritz.plugin:plugin:1.0.0'
}
}
allprojects {
repositories {
google()
jcenter()
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}
这时候就可以在任意 Module
里面使用刚才定义的插件了,到 App Module 里面使用定义的插件:
apply plugin: 'com.android.application'
//这里添加插件,aspectj 就是插件的 id
apply plugin: 'aspectj'
添加了之后,打开 Android Studio 的 Message 窗口里面的 Gradle Console 窗口,执行一个 clean 操作,就可以看到下面的输出了:
完成 AspectJ 插件
前面已经创建一个简单的 Gradle
插件的模型了,现在就要在它的基础上面,完成一个 AspectJ
的插件,而通过 AspectJ
完成 AOP 编程。
(PS: 不了解 AOP 的朋友可以看这篇文章 https://www.jianshu.com/p/0fa8073fd144 )
到项目的根路径里面添加 aspectj
的依赖路径:
dependencies {
classpath "com.android.tools.build:gradle:3.0.1"
//这里就是插件所在的包名:生成插件的模块名:插件的版本号
classpath 'com.fritz.plugin:plugin:1.0.0'
//aspectj的路径依赖
classpath "org.aspectj:aspectjtools:1.8.9"
}
同时到插件的 module
里面添加 aspectj
的依赖:
dependencies {
compile gradleApi()//gradle sdk
compile localGroovy()//groovy sdk
//aspectj的依赖
compile 'org.aspectj:aspectjtools:1.8.9'
}
这时候就可以开始改造我们刚才的 AspectjPlugin.groovy
文件了:
public class AspectjPlugin implements Plugin<Project> {
@Override
void apply(Project project) {
project.dependencies {
compile 'org.aspectj:aspectjrt:1.8.9'
}
final def log = project.logger
//打印
log.error "========================";
log.error "Aspectj切片开始编织Class!";
log.error "========================";
project.android.applicationVariants.all { variant ->
def javaCompile = variant.javaCompile
javaCompile.doLast {
String[] args = ["-showWeaveInfo",
"-1.7",//对应插件module声明的Java版本
"-inpath", javaCompile.destinationDir.toString(),
"-aspectpath", javaCompile.classpath.asPath,
"-d", javaCompile.destinationDir.toString(),
"-classpath", javaCompile.classpath.asPath,
"-bootclasspath", project.android.bootClasspath.join(File.pathSeparator)]
log.debug "ajc args: " + Arrays.toString(args)
MessageHandler handler = new MessageHandler(true);
new Main().run(args, handler)
for (IMessage message : handler.getMessages(null, true)) {
switch (message.getKind()) {
case IMessage.ABORT:
case IMessage.ERROR:
case IMessage.FAIL:
log.error message.message, message.thrown
break
case IMessage.WARNING:
log.warn message.message, message.thrown
break
case IMessage.INFO:
log.info message.message, message.thrown
break
case IMessage.DEBUG:
log.debug message.message, message.thrown
break
}
}
}
}
}
}
这时候只要重新通过 uploadArchives
来重新生成新的本地插件,我们就可以在项目中使用到 Aspectj
来实现 AOP 编程了。
巧用 Aspectj 防抖
前期准备已经完成了,那么使用 Aspectj
能够帮助我们解决那些开发中的痛点?
它可以解决不少痛点难点,其中最有名就是 抖动点击 :用户可能会因为手误而多次点击了某个按钮,导致同一个页面给多次打开。
使用 Aspectj
,可以很优雅地解决这个问题。
首先创建一个 Java 注解:
/**
* 防止抖动点击
*/
@Retention(RetentionPolicy.CLASS)//只在编译为class时做处理
@Target(ElementType.METHOD)//针对函数方法
public @interface SingleClick {
}
接着创建一个 Aspect 类 SingleClickAspect
,简单来说,它就是一个注解处理器:
@Aspect//这个必须有,声明为 Aspect 类
public class SingleClickAspect {
/**
* 过滤时间
*/
private static final int MIN_CLICK_DELAY_TIME = 600;
/**
* 最近一次点击的时间
*/
private static long mLastClickTime;
/**
* 最近一次点击的控件ID
*/
private static int mLastClickViewId;
/**
* 方法切入点
* com.fritz.java.lib.SingleClick 表示识别拦截的注解
* 第一个 * 表示任意返回值
* 第二个 * 表示任意方法名
* (..) 表示任意参数
*/
@Pointcut("execution(@com.fritz.java.lib.SingleClick * *(..))")
public void methodAnnotated() {
}
/**
* 在连接点进行方法替换
*/
@Around("methodAnnotated()")
public void aroundJoinPoint(ProceedingJoinPoint joinPoint) throws Throwable {
View view = null;
//遍历方法的全部参数,找到View
for (Object arg : joinPoint.getArgs()) {
if (arg instanceof View) {
view = (View) arg;
}
}
if (view != null) {
Log.e("SingleClickAspect", "lastClickTime:" + mLastClickTime);
long currentTime = Calendar.getInstance().getTimeInMillis();
//过滤掉600毫秒内的连续点击
if (currentTime - mLastClickTime < MIN_CLICK_DELAY_TIME && view.getId()
== mLastClickViewId) {
return;
}
mLastClickTime = currentTime;
mLastClickViewId = view.getId();
Log.e("SingleClickAspect", "currentTime:" + currentTime);
//执行原方法
joinPoint.proceed();
}
}
}
关于 AspectJ
的语法,网上资料已经很多了,这里就不在多说了。
现在测试一下,给一个点击方法添加 @SingleClick
注解吧:
@SingleClick
public void click(View view) {
if (view instanceof Button) {
Log.e("SingleClickAspect", "click:" + mCount);
((Button) view).setText(String.valueOf(mCount));
mCount++;
}
}
看下运行效果吧:
输出打印为:
可以看到成功过滤了 600ms 内的点击事件了,这样子的解决方法非常优雅呢?
只要添加一个注解即可进行防抖处理,对代码的侵入性极低,尤其是在团队开发中或旧项目维护中,这个方案尤其受欢迎,因为比起重写 OnClickListener
回调和 使用 RxBinding
之类的方法,它无需强制规定团队其他小伙伴的代码规范,因而没有增加太多的人力成本。
当然了,如果仅仅是为了防抖就是用 AspectJ
就太大材小用了,AspectJ
能做到的事情还有很多的,具体可以参考下面这个开源库 XAOP ,这里就不多说了,我也赶紧把这个库里面的干货挖个干净。
文章 demo 的下载链接:https://download.csdn.net/download/f409031mn/10569939