说明:此文章是基于《AOP AspectJ 字节码 示例 Hugo MD》所做的整理,该博主写得很好,但是本人刚上手时,觉得某些点如果有注释讲解的话,对于刚上手的小白友好度更高,所以就厚颜无耻的按照自己的使用理解整理了此文章,基本都是直接搬的代码,见谅见谅哈 ~
就如该博主所说,可以直接使用 AspectJ 的官方库集成配置,但是官方配置对于 Android 开发来说,有以下问题:
所以采用沪江封装的库集成配置。
// 项目根目录的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文件
}
Activity 及 layout 代码:
public class AspectJActivity extends BaseActivity {
@Override
protected 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() {
@Override
public 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">
<Button
android: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 {
@Override
protected 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() {
@Override
public 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">
<Button
android: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