Android 函數(shù)耗時統(tǒng)計工具之Hugo

Hugo

在前文Android AspectJ詳解中,我們了解了AspectJ的使用場景、特點和基本語法,這篇將從沃神開源的Hugo項目實戰(zhàn)分析AspectJ的用法,加深對AspectJ的理解。

Hugo項目是一個調(diào)試函數(shù)調(diào)用耗時的工具,通過對方法或者類添加@DebugLog注解,在運行時會將函數(shù)的耗時打印在控制臺中,通常用于排查函數(shù)耗時,或者用于卡頓檢測。

我在舊文中分析過一些卡頓檢測工具,比如Android卡頓檢測之BlockCanaryMatrix系列文章(一) 卡頓分析工具之Trace Canary,與這兩個工具不同的是,Hugo需手動通過注解打點,侵入性較高,但量級輕、集成簡單、不受卡頓閾值限制,適合小項目測試使用。

使用方法

  1. 項目根目錄build.gradle添加hugo插件依賴
classpath 'com.jakewharton.hugo:hugo-plugin:1.2.1'
  1. 主工程或者library的錄build.gradle中聲明hugo插件。
apply plugin: 'com.jakewharton.hugo'

可通過配置開啟或關閉hugo功能。

hugo {
  enabled false
}
  1. 在類或方法上聲明@DebugLog注解。
@DebugLog
public String getName(String first, String last) {
  SystemClock.sleep(15);
  return first + " " + last;
}

運行程序會在控制臺會打印函數(shù)耗時日志:

V/Example: ? getName(first="Wandering", last="Guy")
V/Example: ? getName [16ms] = "Wandering Guy"

可見日志不僅會打印函數(shù)的耗時 [16ms] ,如果該方法有參數(shù),同時會把實參打印出來。

原理分析

其實整個Hugo項目的源碼非常簡單,也就一百多行代碼,我們完全可以手擼一份。

我們已經(jīng)有了AspectJ的預備知識,現(xiàn)在如果讓你實現(xiàn)一個Hugo項目,你會怎么做呢?三秒思考...


我的思路是這樣的,既然要統(tǒng)計方法的耗時,那需要解決的問題主要有兩個:

  1. 如何標識需要統(tǒng)計耗時的方法?
  2. 如何統(tǒng)計方法的耗時?

對于問題1 通常的做法就是使用自定義注解標識目標方法。

對于問題2 我們這里使用AspectJ完成代碼的織入,由于目標是統(tǒng)計方法的耗時,最簡單的辦法就是在方法執(zhí)行前后各打一點,然后計算這個時間差,而這里說的方法前后打點,在AspectJ中不就可以用 @Around 注解實現(xiàn)嗎?bingo!

使用注解需要額外關注一下保留策略(RetentionPolicy)

這個屬性有三個可選值:

  • SOURCE 只保留在源碼中,編譯成class文件后將丟失。
  • CLASS 保留在class文件中,加載到虛擬機中將丟棄。
  • RUNTIME 運行時仍保留,可通過反射獲取注解信息。

所以三者保留范圍的關系是 RUNTIME > CLASS > SOURCE。

如果我們使用AspectJ作為技術(shù)方案,應該使用哪種保留策略呢?

CLASS,因為AspectJ的作用時機是在class文件生成后,因此不能選擇SOURCE,其次切點pattern支持使用注解作為過濾條件,這就是說在運行時完全用不到注解信息,因此使用RUNTIME是浪費的。

源碼分析

在Hugo中使用的自定義注解就是@DebugLog。

@Target({TYPE, METHOD, CONSTRUCTOR}) @Retention(CLASS)
public @interface DebugLog {
}

Retention屬性上面已經(jīng)分析過了,Target表示可以注解在哪些地方,我們要統(tǒng)計方法的耗時自然是要注解在方法上,因此需要 METHOD和CONSTRUCTOR ,為了方便的統(tǒng)計整個類中所有方法的耗時,Hugo支持 TYPE 。

切面代碼是重點,定義在了Hugo類中。

@Aspect
public class Hugo {
  //①
  @Pointcut("within(@hugo.weaving.DebugLog *)")
  public void withinAnnotatedClass() {}

  //②
  @Pointcut("execution(!synthetic * *(..)) && withinAnnotatedClass()")
  public void methodInsideAnnotatedType() {}

  //③
  @Pointcut("execution(!synthetic *.new(..)) && withinAnnotatedClass()")
  public void constructorInsideAnnotatedType() {}

  //④
  @Pointcut("execution(@hugo.weaving.DebugLog * *(..)) || methodInsideAnnotatedType()")
  public void method() {}
  
  //⑤
  @Pointcut("execution(@hugo.weaving.DebugLog *.new(..)) || constructorInsideAnnotatedType()")
  public void constructor() {}
  
  //⑥
  @Around("method() || constructor()")
  public Object logAndExecute(ProceedingJoinPoint joinPoint) throws Throwable {
    ...
  }
 }

切點表達式比較復雜,大致分為兩類,一類是方法一類是構(gòu)造函數(shù),滿足其一就可以,對應⑥。

①處within表達式聲明的是一個TypePattern,也就是類型限制,范圍是任意聲明DebugLog注解的類型。

②處使用條件運算,指代任意聲明了DebugLog注解的類中所有非內(nèi)部類中的方法。再結(jié)合④處,加上那些被DebugLog注解修飾的任意方法,這樣就構(gòu)成了所有method的范圍。簡而言之,也分為兩部分:

  • 所有聲明了DebugLog注解的類中所有的方法。
  • 所有聲明了DebugLog注解的方法。

切點表達式中使用了 !synthetic 排除內(nèi)部類中的方法 是為了避免再次為內(nèi)部類聲明DebugLog注解時重復織入的問題。

對于構(gòu)造函數(shù)的切點表達式,同理。

切點選好了,我們來看具體織入的代碼。

@Around("method() || constructor()")
public Object logAndExecute(ProceedingJoinPoint joinPoint) throws Throwable {
    enterMethod(joinPoint);
    //start 打點
    long startNanos = System.nanoTime();
    Object result = joinPoint.proceed();
    //end 打點
    long stopNanos = System.nanoTime();
    //計算耗時
    long lengthMillis = TimeUnit.NANOSECONDS.toMillis(stopNanos - startNanos);
    
    exitMethod(joinPoint, result, lengthMillis);
    
    return result;
}

顯然就是在切點代碼 joinPoint.proceed() 前后打上時間戳。

來看enterMethod:

private static void enterMethod(JoinPoint joinPoint) {
    if (!enabled) return;

    CodeSignature codeSignature = (CodeSignature) joinPoint.getSignature();

    Class<?> cls = codeSignature.getDeclaringType();
    //獲取方法名
    String methodName = codeSignature.getName();
    //方法參數(shù)列表
    String[] parameterNames = codeSignature.getParameterNames();
    //方法參數(shù)值列表
    Object[] parameterValues = joinPoint.getArgs();

    //字符串拼接
    StringBuilder builder = new StringBuilder("\u21E2 ");
    builder.append(methodName).append('(');
    for (int i = 0; i < parameterValues.length; i++) {
      if (i > 0) {
        builder.append(", ");
      }
      builder.append(parameterNames[i]).append('=');
      builder.append(Strings.toString(parameterValues[i]));
    }
    builder.append(')');

    if (Looper.myLooper() != Looper.getMainLooper()) {
      builder.append(" [Thread:\"").append(Thread.currentThread().getName()).append("\"]");
    }
    //打印日志
    Log.v(asTag(cls), builder.toString());
    ...
  }

執(zhí)行完enterMethod方法就打印出了類似這樣的日志。

V/Example: ? getName(first="Wandering", last="Guy")

再來看exitMethod:

private static void exitMethod(JoinPoint joinPoint, Object result, long lengthMillis) {
    if (!enabled) return;
    
    Signature signature = joinPoint.getSignature();
    
    Class<?> cls = signature.getDeclaringType();
    String methodName = signature.getName();
    //判斷是否有返回值
    boolean hasReturnType = signature instanceof MethodSignature
    && ((MethodSignature) signature).getReturnType() != void.class;
    
    //打印函數(shù)耗時
    StringBuilder builder = new StringBuilder("\u21E0 ")
    .append(methodName)
    .append(" [")
    .append(lengthMillis)
    .append("ms]");
    
    //打印返回值
    if (hasReturnType) {
    builder.append(" = ");
    builder.append(Strings.toString(result));
    }
    
    Log.v(asTag(cls), builder.toString());
}

這樣就輸出了類似這樣的日志:

V/Example: ? getName [16ms] = "Wandering Guy"

總結(jié)

整個流程分析下來,并沒有很復雜的地方,我們完全可以借鑒Hugo的思路完成一些常見場景的AOP需求。

我們常用的滬江 aspectjx插件 正是受Hugo項目的影響而設計,并在AspectJ的基礎上擴展支持AAR, JAR及Kotlin。

開源是神器,吾輩共勉之!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容