引言
在Android开发过程中,可能机器配置不够,各种响应慢了,界面跳转响应之类有一点延迟,而用户进行了快速点击,就导致了响应多次执行了,这样就很影响实际的体验了。所以也就有了此文,怎么处理好这种快速点击的误操作,怎么把“手残党”扼杀于摇篮之中,就用来了黑科技AOP,插桩的方式来简化判断的逻辑,开放生产力。
AOP介绍
AOP维基介绍
面向侧面的程序设计(aspect-oriented programming,AOP,又译作面向方面的程序设计、观点导向编程、剖面导向程序设计)是计算机科学中的一个术语,指一种程序设计范型。该范型以一种称为侧面(aspect,又译作方面)的语言构造为基础,侧面是一种新的模块化机制,用来描述分散在对象、类或函数中的横切关注点(crosscutting concern)。
侧面的概念源于对面向对象的程序设计的改进,但并不只限于此,它还可以用来改进传统的函数。与侧面相关的编程概念还包括元对象协议、主题(subject)、混入(mixin)和委托。
gradle plugin依赖
为了方便使用,我已经抽离出来了一个aspectj.gradle文件,需要注意的是,目前我是在app-moudle来进行配置的,注意variants的取值在library-moudle的取值是不同的。
repositories {
jcenter()
google()
}
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'org.aspectj:aspectjtools:1.8.13'
}
}
import org.aspectj.bridge.IMessage
import org.aspectj.bridge.MessageHandler
import org.aspectj.tools.ajc.Main
final def log = project.logger
final def variants = project.android.applicationVariants
variants.all { variant ->
JavaCompile javaCompile = variant.javaCompile
javaCompile.doLast {
String[] args = ["-showWeaveInfo",
"-1.5",
"-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;
}
}
}
}
在app-moudle的build.gradle只需要再引用一下即可
apply plugin: 'com.android.application'
apply from: "aspectj.gradle"
dependencies {
//...
implementation 'org.aspectj:aspectjrt:1.8.13'
}
编写插桩代码
先编写好注解类
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.METHOD)
public @interface SingleClick {
}
其中注意方法切入点的包名+类名要声明正确
@Aspect
public class SingleClickAspectj {
public static final int MIN_CLICK_DELAY_TIME = 500;
static int TIME_TAG = R.id.click_time;
@Pointcut("execution(@com.dreamliner.lib.aspectj.sample.annotation.SingleClick * *(..))")//方法切入点
public void methodAnnotated() {
}
@Around("methodAnnotated()")//在连接点进行方法替换
public void aroundJoinPoint(ProceedingJoinPoint joinPoint) throws Throwable {
View view = null;
for (Object arg : joinPoint.getArgs()) {
if (arg instanceof View) view = ((View) arg);
}
if (view != null) {
Object tag = view.getTag(TIME_TAG);
long lastClickTime = (tag != null) ? (long) tag : 0;
long currentTime = System.currentTimeMillis();
//过滤掉600毫秒内的连续点击
if (currentTime - lastClickTime > MIN_CLICK_DELAY_TIME) {
view.setTag(TIME_TAG, currentTime);
//执行原方法
joinPoint.proceed();
}
}
}
}
使用就很简单,只需要在onClick之类的方法加上注解即可
public class MainActivity extends AppCompatActivity {
private long lastTime;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ActivityMainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
binding.setAct(this);
}
//加上注解,编译的时候就会自动生成中间插桩的代码
@SingleClick
public void onClick(View v) {
Log.e("TAG", "click:\t" + (System.currentTimeMillis() - lastTime));
lastTime = System.currentTimeMillis();
}
}
原理和逆向分析
基本原理就是,编译过程中,生成class会扫描@Aspect的类指定的一些注解类,然后进行@Around的一些插入处理,目前这个就是直接把原有方法的形参来进行一次遍历然后提取view来设置tag,判断是否xx时间内才响应,最后再执行原有方法。
FAQ
- 需要注意的是,如果你地用DataBinding之类,然后用了lambda之类的来转换直接返回一些obj回来而没有view对象,这样会导致遍历形参没有找到,压根执行不下去,所以需要注意这些情况的处理。
总结
整体下来,可以看到,采用AOP的方式来进行防止快速点击的误操作是非常简单而有效的,缺点主要是增加了方法数,整体实现起来非常友好。比RxJava等方式来实现相对优雅不少。
ps: 伸手党福利-AspectSample