利用AspectJ實現Android端非侵入式埋點

前言

最近在項目中遇到通過埋點對用戶行為進行收集的需求,由于項目運行在局域網,而且有一些很細化的需求,比較幾種技術方案之后,選擇了通過AspectJ進行埋點。本文主要介紹筆者對學習和使用AspectJ的總結。

AspectJ是什么

正如面向對象編程是對常見問題的模塊化一樣,面向切面編程是對橫向的同一問題進行模塊化,比如在某個包下的所有類中的某一類方法中都需要解決一個相似的問題,可以通過AOP的編程方式對此進行模塊化封裝,統一解決。關于AOP的具體解釋,可以參照維基百科。而AspectJ就是面向切面編程在Java中的一種具體實現。

AspectJ向Java引入了一個新的概念——join point,它包括幾個新的結構: pointcuts,advice,inter-type declarations 和 aspects。

join point是在程序流中被定義好的點。pointcut在那些點上選出特定的join point和值。advice是到達join point時被執行的代碼。

AspectJ還具有不同類型的類型間聲明(inter-type declarations),允許程序員修改程序的靜態結構,即其類的成員和類之間的關系。

AspectJ中的幾個名詞術語解釋

  • Cross-cutting concerns:即使在面向對象編程中大多數類都是執行一個單一的、特定的功能,它們也有時候需要共享一些通用的輔助功能。比如我們想要在一個線程進入和退出一個方法時,在數據層和UI層加上輸出log的功能。盡管每一個類的主要功能時不同的,但是它們所需要執行的輔助功能是相似的。

  • Advice:需要被注入到.class字節碼文件的代碼。通常有三種:before,after和around,分別是在目標方法執行前,執行后以及替換目標代碼執行。除了注入代碼到方法中外,更進一步的,你還可以做一些別的修改,例如添加成員變量和接口到一個類中。

  • Join point:程序中執行代碼插入的點,例如方法調用時或者方法執行時。

  • Pointcut:告訴代碼注入工具在哪里注入特定代碼的表達式(即需要在哪些Joint point應用特定的Advice)。它可以選擇一個這樣的點(例如,一個單一方法的執行)或者許多相似的點(例如,所有被自定義注解@DebugTrace標記的方法)。

  • Aspect: Aspect將pointcut和advice 聯系在一起。例如,我們通過定義一個pointcut和給出一個準確的advice實現向我們的程序中添加一個打印日志功能的aspect。

  • Weaving:向目標位置(join point)注入代碼(advice)的過程。

上面幾個名詞間的關系的示意圖如下:

AOP編程的具體使用場景

  • 日志記錄
  • 持久化
  • 行為監測
  • 數據驗證
  • 緩存
    ...

注入代碼的時機

  • 運行時:你的代碼對增強代碼的需求很明確,比如,必須使用動態代理(這可以說并不是真正的代碼注入)。

  • 加載時:當目標類被Dalvik或者ART加載的時候修改才會被執行。這是對Java字節碼文件或者Android的dex文件進行的注入操作。

  • 編譯時:在打包發布程序之前,通過向編譯過程添加額外的步驟來修改被編譯的類。

具體使用哪一種方式視使用情況而定。

幾個常用的工具和類庫

  • AspectJ:和Java語言無縫銜接的面向切面的編程的擴展工具(可用于Android)。

  • Javassist for Android:一個移植到Android平臺的非常知名的操縱字節碼的java庫。

  • DexMaker:用于在Dalvik VM編譯時或運行時生成代碼的基于java語言的一套API。

  • ASMDEX:一個字節碼操作庫(ASM),但它處理Android可執行文件(DEX字節碼)。

為什么選擇AspectJ

  • 非常強大

  • 易于使用

  • 支持編譯時和加載時的代碼注入

舉個栗子

現在有一個需求,我們需要計算一個方法的運行時間,我們想通過給這個方法加上我們自定義的注解@DebugTrace來實現這個需求,而不是在業務代碼中很生硬地插入計算時間的代碼。這里我們就可以通過AspectJ來實現我們的目的。

這里我們有兩點需要知道:

  • 注解將在我們編譯過程中的一個新步驟中被處理。

  • 必要的模板代碼將會被生成和注入到被注解的方法中。

這個過程可以通過下面的示意圖理解:

在這個實例中,我們將分出兩個module,一個用于業務代碼,一個用于利用AspectJ進行代碼注入。(這里要說明一下,AspectJ本身是一套java library,為了讓AspectJ在Android上正確運行,我們使用了android library,因為我們必須在編譯應用程序時使用一些鉤子,只能使用android-library gradle插件。)

創建注解
@Retention(RetentionPolicy.CLASS)
@Target({ ElementType.CONSTRUCTOR, ElementType.METHOD })
public @interface DebugTrace {}
創建用于控制監聽的類
/**
 * Class representing a StopWatch for measuring time.
 */
public class StopWatch {
  private long startTime;
  private long endTime;
  private long elapsedTime;

  public StopWatch() {
    //empty
  }

  private void reset() {
    startTime = 0;
    endTime = 0;
    elapsedTime = 0;
  }

  public void start() {
    reset();
    startTime = System.nanoTime();
  }

  public void stop() {
    if (startTime != 0) {
      endTime = System.nanoTime();
      elapsedTime = endTime - startTime;
    } else {
      reset();
    }
  }

  public long getTotalTimeMillis() {
    return (elapsedTime != 0) ? TimeUnit.NANOSECONDS.toMillis(endTime - startTime) : 0;
  }
}
封裝一下android.util.Log
/**
 * Wrapper around {@link android.util.Log}
 */
public class DebugLog {

  private DebugLog() {}

  /**
   * Send a debug log message
   *
   * @param tag Source of a log message.
   * @param message The message you would like logged.
   */
  public static void log(String tag, String message) {
    Log.d(tag, message);
  }
}
關鍵的Aspect類的實現
/**
 * Aspect representing the cross cutting-concern: Method and Constructor Tracing.
 */
@Aspect
public class TraceAspect {

  private static final String POINTCUT_METHOD =
      "execution(@org.android10.gintonic.annotation.DebugTrace * *(..))";

  private static final String POINTCUT_CONSTRUCTOR =
      "execution(@org.android10.gintonic.annotation.DebugTrace *.new(..))";

  @Pointcut(POINTCUT_METHOD)
  public void methodAnnotatedWithDebugTrace() {}

  @Pointcut(POINTCUT_CONSTRUCTOR)
  public void constructorAnnotatedDebugTrace() {}

  @Around("methodAnnotatedWithDebugTrace() || constructorAnnotatedDebugTrace()")
  public Object weaveJoinPoint(ProceedingJoinPoint joinPoint) throws Throwable {
    MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
    String className = methodSignature.getDeclaringType().getSimpleName();
    String methodName = methodSignature.getName();

    final StopWatch stopWatch = new StopWatch();
    stopWatch.start();
    // 被注解的方法在這一行代碼被執行
    Object result = joinPoint.proceed();
    stopWatch.stop();

    DebugLog.log(className, buildLogMessage(methodName, stopWatch.getTotalTimeMillis()));

    return result;
  }

  /**
   * Create a log message.
   *
   * @param methodName A string with the method name.
   * @param methodDuration Duration of the method in milliseconds.
   * @return A string representing message.
   */
  private static String buildLogMessage(String methodName, long methodDuration) {
    StringBuilder message = new StringBuilder();
    message.append("Gintonic --> ");
    message.append(methodName);
    message.append(" --> ");
    message.append("[");
    message.append(methodDuration);
    message.append("ms");
    message.append("]");

    return message.toString();
  }
}

關于上面這段代碼這里提兩點:

  • 我們聲明了兩個公共方法和兩個pointcut用于過濾所有被"org.android10.gintonic.annotation.DebugTrace"標記的方法和構造器。

  • 我們定義的 "weaveJointPoint(ProceedingJoinPoint joinPoint)" 這個方法被添加了"@Around"注解,這意味著我們的代碼注入將發生在被"@DebugTrace"注解標記的方法前后。

下面的一張圖將有助于理解pointcut的構成:

在build.gradle文件中的一些必要的配置

要是AspectJ在Android上正確運行,還需要在build.gradle文件中進行一些必要的配置,如下:

import com.android.build.gradle.LibraryPlugin
import org.aspectj.bridge.IMessage
import org.aspectj.bridge.MessageHandler
import org.aspectj.tools.ajc.Main

buildscript {
  repositories {
    mavenCentral()
  }
  dependencies {
    classpath 'com.android.tools.build:gradle:0.12.+'
    classpath 'org.aspectj:aspectjtools:1.8.1'
  }
}

apply plugin: 'android-library'

repositories {
  mavenCentral()
}

dependencies {
  compile 'org.aspectj:aspectjrt:1.8.1'
}

android {
  compileSdkVersion 19
  buildToolsVersion '19.1.0'

  lintOptions {
    abortOnError false
  }
}

android.libraryVariants.all { variant ->
  LibraryPlugin plugin = project.plugins.getPlugin(LibraryPlugin)
  JavaCompile javaCompile = variant.javaCompile
  javaCompile.doLast {
    String[] args = ["-showWeaveInfo",
                     "-1.5",
                     "-inpath", javaCompile.destinationDir.toString(),
                     "-aspectpath", javaCompile.classpath.asPath,
                     "-d", javaCompile.destinationDir.toString(),
                     "-classpath", javaCompile.classpath.asPath,
                     "-bootclasspath", plugin.project.android.bootClasspath.join(
        File.pathSeparator)]

    MessageHandler handler = new MessageHandler(true);
    new Main().run(args, handler)

    def log = project.logger
    for (IMessage message : handler.getMessages(null, true)) {
      switch (message.getKind()) {
        case IMessage.ABORT:
        case IMessage.ERROR:
        case IMessage.FAIL:
          log.error message.message, message.thrown
          break;
        case IMessage.WARNING:
        case IMessage.INFO:
          log.info message.message, message.thrown
          break;
        case IMessage.DEBUG:
          log.debug message.message, message.thrown
          break;
      }
    }
  }
}
測試方法
@DebugTrace
  private void testAnnotatedMethod() {
    try {
      Thread.sleep(10);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
  }

運行結果:

Gintonic --> testAnnotatedMethod --> [10ms]

我們可以通過對apk文件進行反編譯來查看被注入后的代碼。

總結

AOP編程在進行用戶行為統計是是一種非常可靠的解決方案,避免了直接在業務代碼中進行埋點,而AOP編程的應用還不僅于此,它在性能監控,數據采集等方面也有著廣泛的應用,后續將繼續研究,并整理發布。AspectJ是一個很強大的用于AOP編程的庫,使用AspectJ關鍵在于掌握它的pointcut的語法,這里給一個AspectJ的官方的doc鏈接,需要注意的是,經過實際測試,有一些語法在Android中是無法使用的,需要在實際使用過程中進行總結。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 173,357評論 25 708
  • What? As we all know,在進行項目構建時,追求各模塊高內聚,模塊間低耦合。然而現實并不總是如此美...
    MasterNeo閱讀 2,099評論 0 17
  • 基本知識 其實, 接觸了這么久的 AOP, 我感覺, AOP 給人難以理解的一個關鍵點是它的概念比較多, 而且坑爹...
    永順閱讀 8,357評論 5 114
  • 本章內容: 面向切面編程的基本原理 通過POJO創建切面 使用@AspectJ注解 為AspectJ切面注入依賴 ...
    謝隨安閱讀 3,193評論 0 9
  • 一直記得宣老師說的 人生只為某些片刻而活 如今回頭看理解更加深刻 1月 新東方實習助教 雅思 好老師 方向 未來 ...
    李Sweet恬恬閱讀 391評論 0 0