说明:此文章是基于《AOP AspectJ 字节码 示例 Hugo MD》所做的整理,该博主写得很好,但是本人刚上手时,觉得某些点如果有注释讲解的话,对于刚上手的小白友好度更高,所以就厚颜无耻的按照自己的使用理解整理了此文章,基本都是直接搬的代码,见谅见谅哈 ~

一、引入SDK

就如该博主所说,可以直接使用 AspectJ 的官方库集成配置,但是官方配置对于 Android 开发来说,有以下问题:

  • 不支持 kotlin
  • 不能拦截 jar 包中的类
  • 拦截规则不能写在 jar 包中
  • 需要在每一个 module 都配置脚本

所以采用沪江封装的库集成配置。

// 项目根目录的build.gradle
buildscript {... ...dependencies {classpath 'com.hujiang.aspectjx:gradle-android-plugin-aspectjx:2.0.4'classpath 'org.aspectj:aspectjtools:1.8.13'... ...}
}
// app项目的build.gradle
apply plugin: 'android-aspectjx'
... ...
aspectjx {enabled true //enabled默认为true,即默认AspectJX生效exclude 'android.support' //排除所有package路径中包含`android.support`的class文件及jar文件
}

二、新建切面类拦截 View 的点击行为

Activity 及 layout 代码:

public class AspectJActivity extends BaseActivity {@Overrideprotected void onCreate(@Nullable Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_aspectj);Button btn = (Button) findViewById(R.id.btn);btn.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {// 逻辑代码}});}
}
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"android:layout_width="match_parent"android:layout_height="match_parent"android:orientation="vertical"><Buttonandroid:id="@+id/btn"android:text="点击计数"android:layout_width="wrap_content"android:layout_height="wrap_content" /></LinearLayout>

切面类:

import android.os.SystemClock;
import android.util.Log;
import android.widget.TextView;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;/*** 拦截所有View或其子类的onClick方法,以及通过ButterKnife的注解添加的点击事件*/// TODO 第一步:新建切面类,用@Aspect标注切面类
@Aspect
public class OnClickAspect {// TODO 第二步:在切面类中定义PointCut(切入点)@Pointcut("execution(* android.view.View.On*Listener.on*Click(..)) ")// 定义匹配范围:执行指定方法时拦截public void onClick() {// 匹配View.OnClickListener中的onClick方法和View.OnLongClickListener中的OnLongClickListener方法}@Pointcut("execution(* *.on*ItemClick(..)) ")// 如果有多个匹配范围,可以定义多个,多个规则之间通过 || 或 && 控制public void onItemClick() {// 匹配任意名字以on开头以ItemClick结尾的方法}@Pointcut("execution(@butterknife.OnClick * *(..))")// 匹配通过butterknife的OnClick注解添加的点击事件public void butterKnifeOnClick() {}// TODO 第三步:用@Around标注拦截方法,及其要拦截的切入点@Around("onClick() || onItemClick() || butterKnifeOnClick()")// @Around 拦截方法,这个注解可以同时拦截方法的执行前后public Object around(ProceedingJoinPoint joinPoint) throws Throwable {long beginTime = SystemClock.currentThreadTimeMillis();printJoinPointInfo(joinPoint);if (joinPoint.getSignature() instanceof MethodSignature) {MethodSignature signature = (MethodSignature) joinPoint.getSignature();// 要根据Pointcut匹配的类型强转printMethodSignatureInfo(signature);printArgs(joinPoint);printParameterInfo(joinPoint);}Object result = joinPoint.proceed();Log.i("zzq---", "【@Around】返回值=" + ObjToStringUtils.toString(result)+ "  方法执行耗时=" + (SystemClock.currentThreadTimeMillis() - beginTime));return result;}// 打印切入点信息,必须是静态方法private static void printJoinPointInfo(ProceedingJoinPoint joinPoint) {Log.i("zzq---", "【@Around】MethodSignature"+ "\nKind=" + joinPoint.getKind()+ "\nArgs=" + ObjToStringUtils.toString(joinPoint.getArgs())+ "\nSignature=" + ObjToStringUtils.toString(joinPoint.getSignature())+ "\nSourceLocation=" + ObjToStringUtils.toString(joinPoint.getSourceLocation())+ "\nStaticPart=" + ObjToStringUtils.toString(joinPoint.getStaticPart())+ "\nTarget=" + ObjToStringUtils.toString(joinPoint.getTarget())+ "\nThis=" + ObjToStringUtils.toString(joinPoint.getThis()));}// 打印方法签名信息,必须是静态方法private static void printMethodSignatureInfo(MethodSignature signature) {//下面通过MethodSignature的方式获取方法的详细信息,也基本都可以通过Method对象获取Log.i("zzq---", "【@Around】MethodSignature"+ "\n方法=" + ObjToStringUtils.toString(signature.getMethod())+ "\n方法名=" + signature.getName()+ "\n返回值类型=" + ObjToStringUtils.toString(signature.getReturnType())+ "\n声明类型=" + ObjToStringUtils.toString(signature.getDeclaringType())+ "\n声明类型名=" + signature.getDeclaringTypeName()+ "\n异常类型=" + ObjToStringUtils.toString(signature.getExceptionTypes())+ "\n修饰符=" + signature.getModifiers()+ "\n参数名=" + ObjToStringUtils.toString(signature.getParameterNames())+ "\n参数类型=" + ObjToStringUtils.toString(signature.getParameterTypes()));}// 打印方法参数列表,必须是静态方法private static void printArgs(ProceedingJoinPoint joinPoint) {String[] parameterNames = ((MethodSignature) joinPoint.getSignature()).getParameterNames();//获取参数名列表Object[] parameterValues = joinPoint.getArgs();//获取参数值列表StringBuilder builder = new StringBuilder("");for (int i = 0; i < parameterValues.length; i++) {builder.append("\n").append(parameterNames[i]).append("=")//拼接参数名.append(ObjToStringUtils.toString(parameterValues[i]));//拼接参数值}Log.i("zzq---", "【@Around】参数列表" + builder.toString());}// 打印被拦截的View的属性,必须是静态方法private static void printParameterInfo(ProceedingJoinPoint joinPoint) {Object[] parameterValues = joinPoint.getArgs();//获取参数值列表for (Object obj : parameterValues) {if (obj instanceof TextView) {TextView textView = (TextView) obj;Log.i("zzq---", "【@Around】TextView的信息"+ "  文字=" + textView.getText()+ "  所属界面=" + textView.getContext().getClass().getSimpleName()+ "  ID=" + textView.getId()+ "  父页面名称=" + textView.getParent().getClass().getSimpleName());}}}
}
import java.util.Arrays;
import java.util.HashSet;
import java.util.Locale;
import java.util.Set;/*** 将类信息转化成string类型的工具类*/public class ObjToStringUtils {public static String toString(Object obj) {if (obj == null) {return "null";}if (obj instanceof CharSequence) {return '"' + printableToString(obj.toString()) + '"';}Class<?> cls = obj.getClass();if (Byte.class == cls) {return byteToString((Byte) obj);}if (cls.isArray()) {return arrayToString(cls.getComponentType(), obj);}return obj.toString();}private static String printableToString(String string) {int length = string.length();StringBuilder builder = new StringBuilder(length);for (int i = 0; i < length; ) {int codePoint = string.codePointAt(i);switch (Character.getType(codePoint)) {case Character.CONTROL:case Character.FORMAT:case Character.PRIVATE_USE:case Character.SURROGATE:case Character.UNASSIGNED:switch (codePoint) {case '\n':builder.append("\\n");break;case '\r':builder.append("\\r");break;case '\t':builder.append("\\t");break;case '\f':builder.append("\\f");break;case '\b':builder.append("\\b");break;default:builder.append("\\u").append(String.format("%04x", codePoint).toUpperCase(Locale.US));break;}break;default:builder.append(Character.toChars(codePoint));break;}i += Character.charCount(codePoint);}return builder.toString();}private static String arrayToString(Class<?> cls, Object obj) {if (byte.class == cls) {return byteArrayToString((byte[]) obj);}if (short.class == cls) {return Arrays.toString((short[]) obj);}if (char.class == cls) {return Arrays.toString((char[]) obj);}if (int.class == cls) {return Arrays.toString((int[]) obj);}if (long.class == cls) {return Arrays.toString((long[]) obj);}if (float.class == cls) {return Arrays.toString((float[]) obj);}if (double.class == cls) {return Arrays.toString((double[]) obj);}if (boolean.class == cls) {return Arrays.toString((boolean[]) obj);}return arrayToString((Object[]) obj);}private static String byteArrayToString(byte[] bytes) {StringBuilder builder = new StringBuilder("[");for (int i = 0; i < bytes.length; i++) {if (i > 0) {builder.append(", ");}builder.append(byteToString(bytes[i]));}return builder.append(']').toString();}private static String byteToString(Byte b) {if (b == null) {return "null";}return "0x" + String.format("%02x", b).toUpperCase(Locale.US);}private static String arrayToString(Object[] array) {StringBuilder buf = new StringBuilder();arrayToString(array, buf, new HashSet<Object[]>());return buf.toString();}private static void arrayToString(Object[] array, StringBuilder builder, Set<Object[]> seen) {if (array == null) {builder.append("null");return;}seen.add(array);builder.append('[');for (int i = 0; i < array.length; i++) {if (i > 0) {builder.append(", ");}Object element = array[i];if (element == null) {builder.append("null");} else {Class elementClass = element.getClass();if (elementClass.isArray() && elementClass.getComponentType() == Object.class) {Object[] arrayElement = (Object[]) element;if (seen.contains(arrayElement)) {builder.append("[...]");} else {arrayToString(arrayElement, builder, seen);}} else {builder.append(toString(element));}}}builder.append(']');seen.remove(array);}
}

三、自定义注解拦截特定方法的行为

Activity 及 layout 代码:

public class AspectJActivity extends BaseActivity {@Overrideprotected void onCreate(@Nullable Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_aspectj);Button btn = (Button) findViewById(R.id.btn);btn.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {onCount();}});}@CustomEvent(value = "onCount---")  // 自定义注解public void onCount() {// 逻辑代码}
}
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"android:layout_width="match_parent"android:layout_height="match_parent"android:orientation="vertical"><Buttonandroid:id="@+id/btn"android:text="点击计数"android:layout_width="wrap_content"android:layout_height="wrap_content" /></LinearLayout>

自定义注解:

package com.zzq.mydemo.aspectj;import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.CONSTRUCTOR;
import static java.lang.annotation.ElementType.METHOD;/*** 自定义注解*/@Target({METHOD, CONSTRUCTOR})
@Retention(RetentionPolicy.RUNTIME)
public @interface CustomEvent {String value();
}

切面类:

import android.util.Log;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.aspectj.lang.reflect.SourceLocation;/*** 定义拦截规则(注意要更改为正确的包名)*/@Aspect
public class CustomEventAspect {// 带有CustomEvent注解的所有类@Pointcut("within(@com.zzq.mydemo.aspectj.CustomEvent *)")public void withinAnnotatedClass() {}// 带有CustomEvent注解的所有类,除去synthetic修饰的方法@Pointcut("execution(!synthetic * *(..)) && withinAnnotatedClass()")public void methodInsideAnnotatedType() {}// 带有CustomEvent注解的所有类,除去synthetic修饰的构造方法@Pointcut("execution(!synthetic *.new(..)) && withinAnnotatedClass()")public void constructorInsideAnnotatedType() {}// 带有CustomEvent注解的方法@Pointcut("execution(@com.zzq.mydemo.aspectj.CustomEvent * *(..)) || methodInsideAnnotatedType()")public void method() {}// 带有CustomEvent注解的构造方法@Pointcut("execution(@com.zzq.mydemo.aspectj.CustomEvent *.new(..)) || constructorInsideAnnotatedType()")public void constructor() {}@Before("method() || constructor()")public void before(JoinPoint joinPoint) {SourceLocation location = joinPoint.getSourceLocation();Log.i("zzq---", "【自定义事件 before 时间戳:" + System.currentTimeMillis() + "(" + location.getFileName() + ":" + location.getLine() + ")");MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();CustomEvent annotation = methodSignature.getMethod().getAnnotation(CustomEvent.class);String value = annotation.value();if (!value.isEmpty()) {Log.i("zzq---", value);}}@After("method() || constructor()")public void after(JoinPoint joinPoint) {SourceLocation location = joinPoint.getSourceLocation();Log.i("zzq---", "【自定义事件 after 时间戳:" + System.currentTimeMillis() + "(" + location.getFileName() + ":" + location.getLine() + ")");}// before、after不能和around同时使用// @Around("onClick() || onItemClick() || butterKnifeOnClick()")//@Around 拦截方法,这个注解可以同时拦截方法的执行前后// public Object around(ProceedingJoinPoint joinPoint) throws Throwable {}
}

实现了自动化埋点后,就可以以很少的代价得知运行中 APP 的行为,打印运行日志,上传后台进行运营分析,低耦合,易维护 ~


参考文章:
1、https://www.cnblogs.com/baiqiantao/p/373ed2c28b94e268b82a0c18516f9348.html
2、https://blog.csdn.net/xwh_1230/article/details/78225258
3、https://blog.csdn.net/xwh_1230/article/details/78213160
4、https://blog.csdn.net/Fly0078/article/details/80719863
5、https://www.cnblogs.com/poptest/p/5113673.html
6、https://www.jianshu.com/p/98a91bbabec6

Android 自动化埋点:基于AspectJ的沪江SDK的使用整理相关推荐

  1. 老李分享:Android -自动化埋点 2

    除了上述的事件,Android提供了一个OnTouchListener的监听器,当事件传递到控件的时候,如果控件注册了这个监听器,则会执行监听器中的onTouch方法.同时,如果它返回true,则事件 ...

  2. Android自动化埋点(一) - JVM字节码

    JVM字节码 开头 这一系列文章,主要是讲自动化埋点又叫无痕埋点,或者字节码插桩技术,写这个系列文章的目的是 偶然间发现,网上关于这方面的博客很少,所以我根据自己的一些实战经验,整理了这个系列的文章. ...

  3. android自动化持续集成,基于持续集成的Android自动化测试.pdf

    基于持续集成的Android自动化测试.pdf 2015 年 第24 卷 第 5 期 计 算 机 系 统 应 用 ① 基于持续集成的Android 自动化测试 王 焱, 张 征 (华中科技大学 自动化 ...

  4. Android自动化埋点的实践

    前几天在app里加上了按钮点击事件的自动埋点功能,这个功能的实现在面试中问过很多次,得到的答案都不尽如人意,归根到底是没有理解"自动"这个需求,自己也思考过一些方案,但是一直没有一 ...

  5. android应用中自动化埋点的实现,Android 自动化埋点方案

    一.事件实现原理: ① View设置AccessibilityDelegate ②而当View 产生了click,long_click 等事件的时候.会在响应原有的Listener方法 ③原有的Lis ...

  6. Android自动化大讲堂34--终极自动化框架UIAutomator使用详解

    <深入理解Android自动化测试> 又双叒叕重印咯!!! 无以为报,只能改版得更漂亮一点来答谢各位的厚爱! 好了,废话少说,咱们开始吧! 终极自动化框架UIAutomator使用详解 注 ...

  7. [转] android自动化之MonkeyRunner测试环境配置(一)

    Android自动化测试之MonkeyRunner 一.Android自动化测试之环境搭建 1.1  Android-sdk介绍 ¢ SDK(Software development kit)软件开发 ...

  8. 【金阳光测试】基于控件核心技术探讨---Android自动化系列(2)---2013年5月

    第一讲分享了下安卓自动化一些概况和一些自动化框架现状和技术能够解决什么样的问题.这次课就深入到android世界里面.遨游.翱翔,深入了解自动化测试核心技术. 搞过编程开发的同学听到instrumen ...

  9. 重磅新书 | 《Android 全埋点解决方案》预售正式开启!

    新书抢先看 这是一本实战为导向的.翔实的 Android 全埋点技术与解决方案手册,是国内知名大数据公司神策数据在该领域多年实践经验的总结. 本书详细阐述了 Android 全埋点的 8 种解决方案, ...

最新文章

  1. 在线考试系统html模板,请问谁有在线考试系统的网页模板?
  2. Java初学者如何迈出AOP第一步--使用Java 动态代理实现AOP
  3. python密码传参有特殊字符如何解决_python生成带特殊字符的密码
  4. 计算机应用技术商务网站运营,计算机应用技术云计算与商务网站运营有什么区别...
  5. 【转】Windows Server2008 R2下安装Oracle 10g
  6. iwconfig的使用
  7. js学习小计1-onbeforeunload
  8. php的date比较时间差,php date 时间差
  9. IoT 打响安防保卫战!
  10. 系统设计(一)——Overview
  11. MySQL Workbench给其他IP用户分配权限
  12. 使用HslCommunication实现PLC数据的远程客户端监视,以及web端实时监视,远程操作设备示例...
  13. Wpf ListBox数据绑定实例1--绑定字典集合
  14. USB composite 设计
  15. 7月编程语言排行榜揭晓!
  16. coffeescript基本语法
  17. 支持服务器和客户端杀毒软件,服务器上小心使用杀毒软件和软件防火墙
  18. MySQL基础必会,简单易懂
  19. 互联网医疗的定义及架构
  20. 一张表格分成两页打印_表格被分成两页怎么处理

热门文章

  1. AJAX学习-----ASP/PHP 请求实例
  2. PCIE-5565PIORC GE 光纤网络节点卡
  3. 北京朝阳一互联网公司被端,23人被警方带走…这种开发千万别干!
  4. 标题 穿越雷区 java_标题:穿越雷区
  5. CAD制图初学入门教程:CAD怎么复制?
  6. SunJDK的历史转折点
  7. error Command failed with exit code 1.
  8. Java中值得注意的『运算符、逻辑控制、输入输出』
  9. HTML5期末大作业:学校校园网站设计——基于web的静态校园网站系统设计与实现(30页含论文) HTML+CSS+JavaScript
  10. Redhat Linux无显示器无键盘无鼠标启动,并提供远程服务