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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章