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

说明:此文章是基于《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 {

    @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

你可能感兴趣的:(Android,第三方,SDK)