利用AspectJ實(shí)現(xiàn)Android端非侵入式埋點(diǎn)

前言

最近在項(xiàng)目中遇到通過埋點(diǎn)對用戶行為進(jìn)行收集的需求,由于項(xiàng)目運(yùn)行在局域網(wǎng),而且有一些很細(xì)化的需求,比較幾種技術(shù)方案之后,選擇了通過AspectJ進(jìn)行埋點(diǎn)。本文主要介紹筆者對學(xué)習(xí)和使用AspectJ的總結(jié)。

AspectJ是什么

正如面向?qū)ο缶幊淌菍ΤR妴栴}的模塊化一樣,面向切面編程是對橫向的同一問題進(jìn)行模塊化,比如在某個包下的所有類中的某一類方法中都需要解決一個相似的問題,可以通過AOP的編程方式對此進(jìn)行模塊化封裝,統(tǒng)一解決。關(guān)于AOP的具體解釋,可以參照維基百科。而AspectJ就是面向切面編程在Java中的一種具體實(shí)現(xiàn)。

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

join point是在程序流中被定義好的點(diǎn)。pointcut在那些點(diǎn)上選出特定的join point和值。advice是到達(dá)join point時被執(zhí)行的代碼。

AspectJ還具有不同類型的類型間聲明(inter-type declarations),允許程序員修改程序的靜態(tài)結(jié)構(gòu),即其類的成員和類之間的關(guān)系。

AspectJ中的幾個名詞術(shù)語解釋

  • Cross-cutting concerns:即使在面向?qū)ο缶幊讨写蠖鄶?shù)類都是執(zhí)行一個單一的、特定的功能,它們也有時候需要共享一些通用的輔助功能。比如我們想要在一個線程進(jìn)入和退出一個方法時,在數(shù)據(jù)層和UI層加上輸出log的功能。盡管每一個類的主要功能時不同的,但是它們所需要執(zhí)行的輔助功能是相似的。

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

  • Join point:程序中執(zhí)行代碼插入的點(diǎn),例如方法調(diào)用時或者方法執(zhí)行時。

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

  • Aspect: Aspect將pointcut和advice 聯(lián)系在一起。例如,我們通過定義一個pointcut和給出一個準(zhǔn)確的advice實(shí)現(xiàn)向我們的程序中添加一個打印日志功能的aspect。

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

上面幾個名詞間的關(guān)系的示意圖如下:

AOP編程的具體使用場景

  • 日志記錄
  • 持久化
  • 行為監(jiān)測
  • 數(shù)據(jù)驗(yàn)證
  • 緩存
    ...

注入代碼的時機(jī)

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

  • 加載時:當(dāng)目標(biāo)類被Dalvik或者ART加載的時候修改才會被執(zhí)行。這是對Java字節(jié)碼文件或者Android的dex文件進(jìn)行的注入操作。

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

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

幾個常用的工具和類庫

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

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

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

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

為什么選擇AspectJ

  • 非常強(qiáng)大

  • 易于使用

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

舉個栗子

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

這里我們有兩點(diǎn)需要知道:

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

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

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

在這個實(shí)例中,我們將分出兩個module,一個用于業(yè)務(wù)代碼,一個用于利用AspectJ進(jìn)行代碼注入。(這里要說明一下,AspectJ本身是一套java library,為了讓AspectJ在Android上正確運(yùn)行,我們使用了android library,因?yàn)槲覀儽仨氃诰幾g應(yīng)用程序時使用一些鉤子,只能使用android-library gradle插件。)

創(chuàng)建注解
@Retention(RetentionPolicy.CLASS)
@Target({ ElementType.CONSTRUCTOR, ElementType.METHOD })
public @interface DebugTrace {}
創(chuàng)建用于控制監(jiān)聽的類
/**
 * 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);
  }
}
關(guān)鍵的Aspect類的實(shí)現(xiàn)
/**
 * 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();
    // 被注解的方法在這一行代碼被執(zhí)行
    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();
  }
}

關(guān)于上面這段代碼這里提兩點(diǎn):

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

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

下面的一張圖將有助于理解pointcut的構(gòu)成:

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

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

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();
    }
  }

運(yùn)行結(jié)果:

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

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

總結(jié)

AOP編程在進(jìn)行用戶行為統(tǒng)計(jì)是是一種非??煽康慕鉀Q方案,避免了直接在業(yè)務(wù)代碼中進(jìn)行埋點(diǎn),而AOP編程的應(yīng)用還不僅于此,它在性能監(jiān)控,數(shù)據(jù)采集等方面也有著廣泛的應(yīng)用,后續(xù)將繼續(xù)研究,并整理發(fā)布。AspectJ是一個很強(qiáng)大的用于AOP編程的庫,使用AspectJ關(guān)鍵在于掌握它的pointcut的語法,這里給一個AspectJ的官方的doc鏈接,需要注意的是,經(jīng)過實(shí)際測試,有一些語法在Android中是無法使用的,需要在實(shí)際使用過程中進(jìn)行總結(jié)。

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

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

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

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