网易首页 > 网易号 > 正文 申请入驻

AspectJ 在 Android 中的使用攻略!

0
分享至

  作者 | 唯鹿 责编 | 欧阳姝黎

  出品 | CSDN博客

  AOP(aspect-oriented programming),指的是面向切面编程。而AspectJ是实现AOP的其中一款框架,内部通过处理字节码实现代码注入。

  AspectJ从2001年发展至今,已经非常成熟稳定,同时使用简单是它的一大优点。至于它的使用场景,可以看本文中的一些小例子,或取能给你启发。

  集成AspectJ

  •   使用插件gradle-android-aspectj-plugin

  这种方式接入简单。但是此插件截至目前已经一年多没有维护了,考虑到AGP的兼容性,害怕以后无法使用。这里就不推荐了。(这里存在特殊情况,文章后面会提到。)

  •   常规的Gradle 配置方式

  这种方法相对配置会多一些,但相对可控。

  首先在项目根目录的build.gradle中添加:

  
classpath "com.android.tools.build:gradle:4.2.1"classpath 'org.aspectj:aspectjtools:1.9.6'

  然后在app的build.gradle中添加:

  
dependencies {implementation 'org.aspectj:aspectjrt:1.9.6'
import org.aspectj.bridge.IMessageimport org.aspectj.bridge.MessageHandlerimport org.aspectj.tools.ajc.Main
final def log = project.loggerfinal def variants = project.android.applicationVariants
variants.all { variant ->// 注意这里控制debug下生效,可以自行控制是否生效if (!variant.buildType.isDebuggable()) {log.debug("Skipping non-debuggable build type '${variant.buildType.name}'.")return
JavaCompile javaCompile = variant.javaCompileProvider.get()javaCompile.doLast {String[] args = ["-showWeaveInfo","-1.8","-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.thrownbreakcase IMessage.WARNING:log.warn message.message, message.thrownbreakcase IMessage.INFO:log.info message.message, message.thrownbreakcase IMessage.DEBUG:log.debug message.message, message.thrownbreak

  在 module 使用的话一样需要添加配置代码(略有不同):

  
dependencies {implementation 'org.aspectj:aspectjrt:1.9.6'

import org.aspectj.bridge.IMessageimport org.aspectj.bridge.MessageHandlerimport org.aspectj.tools.ajc.Main
final def log = project.logger
android.libraryVariants.all{ variant ->if (!variant.buildType.isDebuggable()) {log.debug("Skipping non-debuggable build type '${variant.buildType.name}'.")return
JavaCompile javaCompile = variant.javaCompileProvider.get()javaCompile.doLast {String[] args = ["-showWeaveInfo","-1.8","-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.thrownbreakcase IMessage.WARNING:log.warn message.message, message.thrownbreakcase IMessage.INFO:log.info message.message, message.thrownbreakcase IMessage.DEBUG:log.debug message.message, message.thrownbreak

  AspectJ基础语法

  Join Points

  连接点,用来连接我们需要操作的位置。比如连接普通方法、构造方法还是静态初始化块等位置,以及是调用方法外部还是调用方法内部。常用类型有Method call、Method execution、Constructor call、Constructor execution等。

  Pointcuts

  切入点,是带条件的Join Points,确定切入点位置。

  execution和call的区别如下图:

  Pattern规则如下:

  •   上表中中括号为可选项,没有可以不写

  •   方法匹配例子:


1) java.*.Date:可以表示java.sql.Date,也可以表示java.util.Date2) Test*:可以表示TestBase,也可以表示TestDervied3) java..*:表示java任意子类4) java..*Model+:表示Java任意package中名字以Model结尾的子类,比如TabelModel,TreeModel 等
  •   参数匹配例子:


1) (int, char):表示参数只有两个,并且第一个参数类型是int,第二个参数类型是char2) (String, ..):表示至少有一个参数。并且第一个参数类型是String,后面参数类型不限.3) ..代表任意参数个数和类型4) (Object ...):表示不定个数的参数,且类型都是Object,这里的...不是通配符,而是Java中代表不定参数的意思
Advice

  用来指定代码插入到Pointcuts的什么位置。

  After、Before 示例

  这里我们实现一个功能,在所有Activity的onCreate方法中添加Trace方法,来统计onCreate方法耗时。

  
@Aspect // <-注意添加,才会生效参与编译public class TraceTagAspectj {
@Before("execution(* android.app.Activity+.onCreate(..))")public void before(JoinPoint joinPoint) {Trace.beginSection(joinPoint.getSignature().toString());
@After("execution(* android.app.Activity+.onCreate(..))")public void after() {Trace.endSection();

  编译后的class代码如下:

可以看到经过处理后,它并不会直接把 Trace 函数直接插入到代码中,而是经过一系列自己的封装。如果想针对所有的函数都做插桩,AspectJ 会带来不少的性能影响。 不过大部分情况,我们可能只会插桩某一小部分函数,这样 AspectJ 带来的性能影响就可以忽略不计了。

  AfterReturning示例

  获取切点的返回值,比如这里我们获取TextView,打印它的text值。

  
private TextView testAfterReturning() {return findViewById(R.id.tv);
@Aspectpublic class TextViewAspectj {
@AfterReturning(pointcut = "execution(* *..*.testAfterReturning())", returning = "textView") // "textView"必须和下面参数名称一样public void getTextView(TextView textView) {Log.d("weilu", "text--->" + textView.getText().toString());

  编译后的class代码如下:

  log打印:

  使用@AfterReturning你可以对方法的返回结果做一些修改(注意是“=”赋值,String无法通过此方法修改)。

  AfterThrowing示例

  当方法执行出现异常,且异常没有处理时,可以使用@AfterThrowing。比如下面的例子中,我们捕获异常并上报(这里用log输出实现)

  
public class MainActivity extends AppCompatActivity {
@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);testAfterThrowing();
private void testAfterThrowing() {TextView textView = null;textView.setText("aspectj");
@Aspectpublic class ReportExceptionAspectj {
@AfterThrowing(pointcut = "call(* *..*.testAfterThrowing())", throwing = "throwable") // "throwable"必须和下面参数名称一样public void reportException(Throwable throwable) {Log.e("weilu", "throwable--->" + throwable);

  log打印

  这里要注意的是,程序最终还是会崩溃,因为最后执行了throw var3。如果你想不崩溃,可以使用@Around。

  Around示例

  接着上面的例子,我们这次直接try catch住异常代码:

  
@Aspectpublic class TryCatchAspectj {@Pointcut("execution(* *..*.testAround())")public void methodTryCatch() {
@Around("methodTryCatch()")public void aroundTryJoinPoint(ProceedingJoinPoint joinPoint) throws Throwable {try {joinPoint.proceed(); // <- 调用原代码} catch (Exception e) {e.printStackTrace();

  编译后的class代码如下:

  @Around 明显更加灵活,我们可以自定义,实现"偷梁换柱"的效果,比如上面提到的替换方法的返回值。

  
进阶

  withincode

  withincode表示某个方法执行过程中涉及到的JPoint,通常用来过滤切点。例如我们有一个Person对象:

  
public class Person {
private String name;private int age;
public Person() {this.name = "weilu";this.age = 18;
public String getName() {return name;
public void setName(String name) {this.name = name;
public int getAge() {return age;
public void setAge(int age) {this.age = age;

  Person对象中有两处set age的地方,如果我们只想让构造方法的生效,让setAge方法失效,可以使用@Around("execution(* com.weilu.aspectj.demo.Person.setAge(..))")不过如果有更多处set age的地方,我们这样一个个去匹配就很麻烦。

  这里就可以考虑使用set这个Pointcuts:

  
public class FieldAspectJ {
@Around("set(int com.weilu.aspectj.demo.Person.age)")public void aroundFieldSet(ProceedingJoinPoint joinPoint) throws Throwable {Log.e("weilu", "around->" + joinPoint.getTarget().toString() + "#" + joinPoint.getSignature().getName());

  由于set(FieldPattern)的FieldPattern限制,不能指定参数,这样会将所有的set age都切入:

  这时就可以使用withincode添加过滤条件:

  
@Aspectpublic class FieldAspectJ {
@Pointcut("!withincode(com.weilu.aspectj.demo.Person.new())")public void invokePerson() {
@Around("set(int com.weilu.aspectj.demo.Person.age) && invokePerson()")public void aroundFieldSet(ProceedingJoinPoint joinPoint) throws Throwable {Log.e("weilu", "around->" + joinPoint.getTarget().toString() + "#" + joinPoint.getSignature().getName());

  结果如下:

  还有一个within,它和withincode类似。不同的是,它的范围是类,而withincode是方法。例如:within(com.weilu.activity.*)表示此包下任意的JPoint。

  args

  用来指定当前执行方法的参数条件。比如上一个例子中,如果需要指定第一个参数是int,后面参数不限。就可以这样写。

  
@Around("execution(* com.weilu.aspectj.withincode.Person.setAge(..)) && args(int,..)")

  cflow

  cflow是call flow的意思,cflow的条件是一个pointcut

  举一个例子来说明一下它的用途,a方法中调用了b、c、d方法。此时要统计各个方法的耗时,如果按之前掌握的语法,我们最多需要写四个Pointcut,方法越多越麻烦。

  使用cflow,我们可以方便的掌握方法的“调用流”。我们测试方法如下:

  
private void test() {testAfterReturning();testAround();testWithInCode();

  实现如下:

  
@Aspectpublic class TimingAspect {
@Around("execution(* *(..)) && cflow(execution(* com.weilu.aspectj.demo.MainActivity.test(..)))")public Object measureExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {long startTime = currentTimeMillis();Object result = joinPoint.proceed();long endTime = currentTimeMillis();Log.e("weilu", joinPoint.getSignature().toString() + " -> " + (endTime - startTime) + " ms");return result;

  cflow(execution(* com.weilu.aspectj.demo.MainActivity.test(..)))表示调用test方法时所包含的JPoint,包括自身JPoint。

  execution(* *(..))的作用是去除TimingAspect自身的代码,避免自己拦截自己,形成死循环。

  log结果如下:

  还有一个cflowbelow,它和cflow类似。不同的是,它不包括自身JPoint。也就是例子中不会获取test方法的耗时。

  实战

  拦截点击

  拦截点击的目的是避免因快速点击控件,导致重复执行点击事件。例如打开多次页面,弹出多次弹框,请求多次接口,我之前发现在部分机型上,很容易复现此类情况。所以避免抖动这算是项目中的一个常见需求。

  例如butterknife中就自带DebouncingOnClickListener来避免此类问题。

  如果你已不在使用butterknife,也可以复制这段代码。一个个的替换已有的View.OnClickListener。还有以前使用Rxjava操作符来处理防抖。但这些方式侵入式大且替换的工作量也大。

  这种场景就可以考虑AOP的方式处理。拦截onClick方法,判断是否可以点击。

  

@Aspectpublic class InterceptClickAspectJ {
// 最后一次点击的时间private Long lastTime = 0L;// 点击间隔时长private static final Long INTERVAL = 300L;
@Around("execution(* android.view.View.OnClickListener.onClick(..))")public void clickIntercept(ProceedingJoinPoint joinPoint) throws Throwable {// 大于间隔时间可点击if (System.currentTimeMillis() - lastTime >= INTERVAL) {// 记录点击时间lastTime = System.currentTimeMillis();// 执行点击事件joinPoint.proceed();} else {Log.e("weilu", "重复点击");

  实现代码很简单,效果如下:

  考虑到有些view的点击事件不需要防抖,例如checkBox。否则checkBox状态变了,但事件没有执行。我们可以定义一个注解,用withincode过滤有此注解的方法。具体需求可以根据实际项目自行拓展,这里仅提供思路。

  埋点

  前面的例子中都是无侵入的方式使用AspectJ。这里说一下侵入式的方式,简单说就是使用自定义注解,用注解作为切入点的规则。(其实也可以自定义一种方法命名,来当做切入规则)

  首先定义两个注解,一个用来传固定参数比如eventName、eventId,同时负责当做切入点,一个用来传动态参数的key。

  
@Retention(RetentionPolicy.RUNTIME)public @interface TrackEvent {* 事件名称String eventName() default "";
* 事件idString eventId() default "";
@Target(ElementType.PARAMETER)@Retention(RetentionPolicy.RUNTIME)public @interface TrackParameter {
String value() default "";

  Aspectj代码如下:

  
@Aspectpublic class TrackEventAspectj {
@Around("execution(@com.weilu.aspectj.tracking.TrackEvent * *(..))")public void trackEvent(ProceedingJoinPoint joinPoint) throws Throwable {MethodSignature signature = (MethodSignature) joinPoint.getSignature();
// 获取方法上的注解TrackEvent trackEvent = signature.getMethod().getAnnotation(TrackEvent.class);
String eventName = trackEvent.eventName();String eventId = trackEvent.eventId();
JSONObject params = new JSONObject();params.put("eventName", eventName);params.put("eventId", eventId);
// 获取方法参数的注解Annotation[][] parameterAnnotations = signature.getMethod().getParameterAnnotations();
if (parameterAnnotations.length != 0) {int i = 0;for (Annotation[] parameterAnnotation : parameterAnnotations) {for (Annotation annotation : parameterAnnotation) {if (annotation instanceof TrackParameter) {// 获取key valueString key = ((TrackParameter) annotation).value();params.put(key, joinPoint.getArgs()[i++]);
// 上报Log.e("weilu", "上报数据---->" + params.toString());
try {joinPoint.proceed();} catch (Throwable throwable) {throwable.printStackTrace();

  使用方法:

  
@TrackEvent(eventName = "点击按钮", eventId = "100")private void trackMethod(@TrackParameter("uid") int uid, String name) {Intent intent = new Intent(this, KotlinActivity.class);intent.putExtra("uid", uid);intent.putExtra("name", name);startActivity(intent);trackMethod(10, "weilu");

  结果如下:

  由于匹配key value的代码问题,建议将需要动态传入的参数都写在前面,避免下标越界。

  由于匹配key value的代码问题,建议将需要动态传入的参数都写在前面,避免下标越界。

  还有一些使用场景,比如权限控制。总结一下,AOP适合将一些通用逻辑分离出来,然后通过AOP将此部分注入到业务代码中。这样我们可以更加注重业务的实现,代码也显得清晰起来。

  其他问题

  lambda

  如果我们代码中有使用lambda,例如点击事件会变为:

  
tv.setOnClickListener(v -> Log.e("weilu", "点击事件执行"));

  这样之前的点击切入点就无效了,这里涉及到D8这个脱糖工具和invokedynamic字节码指令相关知识,这里我也无法说的清楚详细。简单说使用lambda会生成lambda$开头的中间方法,所以只能如下处理:

  
@Around("execution(* *..lambda$*(android.view.View))")

  这种暂时处理起来比较麻烦,且可以看出容错率也比较低,很容易切入其他无关方法,所以建议AOP不要使用lambda。

  配置

  一开始介绍了两种配置,虽说AspectJX插件最近不太维护了,但是它的支持了AAR、JAR及Kotlin的切入,而默认仅是对自己的代码进行切入。

在AspectJ常规配置中有这样的代码:"-inpath", javaCompile.destinationDir.toString(),代表只对源文件进行织入。在查看Aspectjx源码时,发现在“-inputs”配置加入了.jar文件,使得class类可以被织入代码。这么理解来看,AspectJ也是支持对class文件的织入的,只是需要对它进行相关的配置,而配置比较繁琐,所以诞生了AspectJx等插件。

  例如Kotlin在需要在常规的Gradle 配置上增加如下配置:

  
def buildType = variant.buildType.nameString[] kotlinArgs = ["-showWeaveInfo","-1.8","-inpath", project.buildDir.path + "/tmp/kotlin-classes/" + buildType,"-aspectpath", javaCompile.classpath.asPath,"-d", project.buildDir.path + "/tmp/kotlin-classes/" + buildType,"-classpath", javaCompile.classpath.asPath,"-bootclasspath", project.android.bootClasspath.join(File.pathSeparator)]MessageHandler handler = new MessageHandler(true)new Main().run(kotlinArgs, handler)

  同时注意用kotlin写对应的Aspect类,毕竟你需要注入的是kotlin代码,用java的肯定不行,但是反过来却可行。

  建议有AAR、JAR及Kotlin需求的使用插件方式,即使后期无人维护,可自行修改源码适配GAP,相对难度不大。

  这部分内容较多同时也比较枯燥,断断续续整理了一周的时间。基本介绍了AspectJ在Android 中的配置,以及常用的语法与使用场景。对于应用AspectJ来说够用了。

  最后本篇涉及的代码都已上传至Github,有兴趣的同学可以用做参考。

  参考

  •   AOP之AspectJ在Android中的应用

  •   AOP 之 AspectJ 全面剖析 in Android

  •   编译插桩的三种方法:AspectJ、ASM、ReDex

  •   Android 引入AspectJ的记录

特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。

Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.

相关推荐
热点推荐
清朝末代格格们都嫁给了谁?好几位大明星,其实都是皇室后裔

清朝末代格格们都嫁给了谁?好几位大明星,其实都是皇室后裔

百年历史老号
2024-06-12 07:40:13
惊呆!这里房价暴跌70%,真应了马云说的白菜价

惊呆!这里房价暴跌70%,真应了马云说的白菜价

山丘楼评
2024-06-14 23:55:48
这样的情况,军队真的允许吗?

这样的情况,军队真的允许吗?

娱记掌门
2024-06-15 09:40:01
马龙回应无缘奥运单打:以自己目前能力和状态,可能有点吃力

马龙回应无缘奥运单打:以自己目前能力和状态,可能有点吃力

懂球帝
2024-06-15 12:58:10
中国足球又抽到韩国,真是冤家路窄!必须把他们干怕!

中国足球又抽到韩国,真是冤家路窄!必须把他们干怕!

知球者也
2024-06-15 15:42:49
乌军再次空袭莫洛佐夫斯克基地!俄军弃桥保船,捷克炮弹已交付

乌军再次空袭莫洛佐夫斯克基地!俄军弃桥保船,捷克炮弹已交付

鹰眼Defence
2024-06-15 17:30:17
拜登未料到,中国三大部门宣布,断供航天发动机、复合材料

拜登未料到,中国三大部门宣布,断供航天发动机、复合材料

嘿哥哥科技
2024-06-14 08:38:03
女生会接受一个性能力不好的男朋友吗?评论区的回答惊呆上万读者

女生会接受一个性能力不好的男朋友吗?评论区的回答惊呆上万读者

社会潜伏者
2024-05-13 01:15:15
足球报:河南队六轮一循环让南基一再续命 俱乐部正加大补强力度

足球报:河南队六轮一循环让南基一再续命 俱乐部正加大补强力度

直播吧
2024-06-16 12:48:06
广东深圳,男子喜获双胞胎儿子,可他无意间发现,两个儿子竟然两个爹

广东深圳,男子喜获双胞胎儿子,可他无意间发现,两个儿子竟然两个爹

娱乐圈见解说
2024-06-16 07:05:18
还顾得上破船?解放军舰队突然现身棉兰老岛,马科斯担忧成真

还顾得上破船?解放军舰队突然现身棉兰老岛,马科斯担忧成真

小阿文热点军
2024-06-15 19:05:04
你什么时候意识到自己没见过世面?网友:体制内,不知道水牌是啥

你什么时候意识到自己没见过世面?网友:体制内,不知道水牌是啥

热闹的河马
2024-06-14 10:46:15
毛主席去南泥湾视察,王震请客吃烧鸡,饭后主席却执意带走鸡架

毛主席去南泥湾视察,王震请客吃烧鸡,饭后主席却执意带走鸡架

历史实战派
2024-06-15 11:42:41
独行侠G4大胜追至1-3:7数据证绿凯优势仍巨大 再赢一场概率14.2%

独行侠G4大胜追至1-3:7数据证绿凯优势仍巨大 再赢一场概率14.2%

颜小白的篮球梦
2024-06-15 11:02:50
老戏骨离世!66岁抗癌失败,演员妻子忙着拍戏,最后一面都没见到

老戏骨离世!66岁抗癌失败,演员妻子忙着拍戏,最后一面都没见到

综艺拼盘汇
2024-06-15 04:27:27
赵丽颖古早黑历史曝光,惊人往事让人不敢相信,疑似没文化还当三

赵丽颖古早黑历史曝光,惊人往事让人不敢相信,疑似没文化还当三

花哥扒娱乐
2024-04-18 22:17:33
稳健!卡拉菲奥里争顶、过人、抢断成功率100%,传球成功率93%

稳健!卡拉菲奥里争顶、过人、抢断成功率100%,传球成功率93%

直播吧
2024-06-16 07:38:08
英国王妃凯特半年来首次露面!瘦成一道闪电击破各种谣言

英国王妃凯特半年来首次露面!瘦成一道闪电击破各种谣言

九方鱼论
2024-06-15 22:51:35
卡罗拉发起最后反攻:7.98万起!要和比亚迪刚到底?

卡罗拉发起最后反攻:7.98万起!要和比亚迪刚到底?

汽车扒壹扒
2024-06-15 21:28:33
已做牺牲准备!央视曝光东部战区激烈对峙, 外机亮导弹被轰6逼退

已做牺牲准备!央视曝光东部战区激烈对峙, 外机亮导弹被轰6逼退

影孖看世界
2024-06-11 20:33:13
2024-06-16 13:10:44
CSDN
CSDN
成就一亿技术人
24726文章数 241820关注度
往期回顾 全部

科技要闻

iPhone 16会杀死大模型APP吗?

头条要闻

媒体:普京开出的停火条件有重大变化 已亮出战略底牌

头条要闻

媒体:普京开出的停火条件有重大变化 已亮出战略底牌

体育要闻

没人永远年轻 但青春如此无敌还是离谱了些

娱乐要闻

上影节红毯:倪妮好松弛,娜扎吸睛

财经要闻

打断妻子多根肋骨 上市公司创始人被公诉

汽车要闻

售17.68万-21.68万元 极狐阿尔法S5正式上市

态度原创

艺术
本地
教育
时尚
军事航空

艺术要闻

穿越时空的艺术:《马可·波罗》AI沉浸影片探索人类文明

本地新闻

粽情一夏|海河龙舟赛,竟然成了外国人的大party!

教育要闻

高考结束不代表万事大吉,考生别着急丢准考证,这8个用途需了解

中年女性还是穿连衣裙最有气质!裙摆过膝、腰部收紧,巨显瘦

军事要闻

普京提停火和谈条件 美防长迅速回应

无障碍浏览 进入关怀版