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

前言

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

AspectJ是什麼

正如面向對象編程是對常見問題的模塊化同樣,面向切面編程是對橫向的同一問題進行模塊化,好比在某個包下的全部類中的某一類方法中都須要解決一個類似的問題,能夠經過AOP的編程方式對此進行模塊化封裝,統一解決。關於AOP的具體解釋,能夠參照維基百科。而AspectJ就是面向切面編程在Java中的一種具體實現。html

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

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

AspectJ還具備不一樣類型的類型間聲明(inter-type declarations),容許程序員修改程序的靜態結構,即其類的成員和類之間的關係。git

AspectJ中的幾個名詞術語解釋

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

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

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

  • Pointcut:告訴代碼注入工具在哪裏注入特定代碼的表達式(即須要在哪些Joint point應用特定的Advice)。它能夠選擇一個這樣的點(例如,一個單一方法的執行)或者許多類似的點(例如,全部被自定義註解@DebugTrace標記的方法)。緩存

  • Aspect: Aspect將pointcut和advice 聯繫在一塊兒。例如,咱們經過定義一個pointcut和給出一個準確的advice實現向咱們的程序中添加一個打印日誌功能的aspect。app

  • 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中是沒法使用的,須要在實際使用過程當中進行總結。

相關文章
相關標籤/搜索