1. 程式人生 > >Android中的AOP程式設計

Android中的AOP程式設計

面向切面程式設計(AOP,Aspect-oriented programming)需要把程式邏輯分解成『關注點』(concerns,功能的內聚區域)。這意味著,在 AOP 中,我們不需要顯式的修改就可以向程式碼中新增可執行的程式碼塊。這種程式設計正規化假定『橫切關注點』(cross-cutting concerns,多處程式碼中需要的邏輯,但沒有一個單獨的類來實現)應該只被實現一次,且能夠多次注入到需要該邏輯的地方。

程式碼注入是 AOP 中的重要部分:它在處理上述提及的橫切整個應用的『關注點』時很有用,例如日誌或者效能監控。這種方式,並不如你所想的應用甚少,相反的,每個程式設計師都可以有使用這種注入程式碼能力的場景,這樣可以避免很多痛苦和無奈。

AOP 是一種已經存在了很多年的程式設計正規化。我發現把它應用到 Android 開發中也很有用。經過一番調研後,我認為我們用它可以獲得很多好處和有用的東西。

術語(迷你術語表)

在開始之前,我們先看看需要了解的詞彙:

  • Cross-cutting concerns(橫切關注點): 儘管面向物件模型中大多數類會實現單一特定的功能,但通常也會開放一些通用的附屬功能給其他類。例如,我們希望在資料訪問層中的類中新增日誌,同時也希望當UI層中一個執行緒進入或者退出呼叫一個方法時新增日誌。儘管每個類都有一個區別於其他類的主要功能,但在程式碼裡,仍然經常需要新增一些相同的附屬功能。

  • Advice(通知):

    注入到class檔案中的程式碼。典型的 Advice 型別有 before、after 和 around,分別表示在目標方法執行之前、執行後和完全替代目標方法執行的程式碼。 除了在方法中注入程式碼,也可能會對程式碼做其他修改,比如在一個class中增加欄位或者介面。

  • Joint point(連線點): 程式中可能作為程式碼注入目標的特定的點,例如一個方法呼叫或者方法入口。

  • Pointcut(切入點): 告訴程式碼注入工具,在何處注入一段特定程式碼的表示式。例如,在哪些 joint points 應用一個特定的 Advice。切入點可以選擇唯一一個,比如執行某一個方法,也可以有多個選擇,比如,標記了一個定義成@DebguTrace 的自定義註解的所有方法。

  • Aspect(切面): Pointcut 和 Advice 的組合看做切面。例如,我們在應用中通過定義一個 pointcut 和給定恰當的advice,新增一個日誌切面。

  • Weaving(織入): 注入程式碼(advices)到目標位置(joint points)的過程。

下面這張圖簡要總結了一下上述這些概念。


那麼...我們何時何地應用AOP呢?

一些示例的 cross-cutting concerns 如下:

取決於你所選的其中一種或其他方案 :)。

工具和庫

有一些工具和庫幫助我們使用 AOP:

  • AspectJ: 一個 JavaTM 語言的面向切面程式設計的無縫擴充套件(適用Android)。

  • Javassist for Android: 用於位元組碼操作的知名 java 類庫 Javassist 的 Android 平臺移植版。

  • DexMaker: Dalvik 虛擬機器上,在編譯期或者執行時生成程式碼的 Java API。

  • ASMDEX: 一個類似 ASM 的位元組碼操作庫,執行在Android平臺,操作Dex位元組碼。

為什麼用 AspectJ?

我們下面的例子選用 AspectJ,有以下原因:

  • 功能強大
  • 支援編譯期和載入時程式碼注入
  • 易於使用

示例

比方說,我們要測量一個方法的效能(執行這個方法需要多長時間)。為此我們用一個 @DebugTrace 的註解標記我們的這個方法,並且無需在每個註解過的方法中編寫程式碼,就可以通過 logcat 輸出結果。我們的方法是使用 AspectJ 達到這個目的。

我們看下在底層到底發生了什麼:

  • 我們在編譯過程中增加一個新的步驟處理註解。
  • 註解的方法內會生成和注入必要的樣板程式碼。

在此,我必須要提到當我研究這些時,發現了Jake Wharton’s Hugo Library 這個專案,支援做同樣的事情。因此,我重構了我的程式碼,看上去和它類似。儘管,我的程式碼是一個更加原始和簡化的版本(順便提一下,通過看這個專案的程式碼,我學到了很多)。


工程結構

我們會把一個簡單的示例應用拆分成兩個 modules,第一個包含我們的 Android App 程式碼,第二個是一個 Android Library 工程,使用 AspectJ 織入程式碼(程式碼注入)。

你可能會想知道為什麼我們用一個 Android Library 工程,而不是用一個純的 Java Library:原因是為了使 AspectJ 能在 Android 上執行,我們必須在編譯時做一些 hook。這隻能使用 andorid-library gradle 外掛完成。(先不要為此擔心,後面我會給出更多細節。)

建立註解

首先我們建立我們的Java註解。這個註解週期宣告在 class 檔案上(RetentionPolicy.CLASS),可以註解建構函式和方法(ElementType.CONSTRUCTOR 和 ElementType.METHOD)。因此,我們的 DebugTrace.java 檔案看上是這樣的:

@Retention(RetentionPolicy.CLASS)
@Target({ ElementType.CONSTRUCTOR, ElementType.METHOD })
public @interface DebugTrace {}

我們的效能監控計時類

我已經建立了一個簡單的計時類,包含 start/stop 方法。下面是 StopWatch.java 檔案:

/**
 * 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;
  }
}

DebugLog 類

我只是包裝了一下 “android.util.Log”,因為我首先想到的是向 android 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 類(TraceAspect.java)了。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();
  }
}

幾個在此提到的重點:

  • 我們聲明瞭兩個作為 pointcuts 的 public 方法,篩選出所有通過 “org.android10.gintonic.annotation.DebugTrace” 註解的方法和建構函式。
  • 我們使用 “@Around” 註解定義了“weaveJointPoint(ProceedingJoinPoint joinPoint)”方法,使我們的程式碼注入在使用"@DebugTrace"註解的地方生效。
  • “Object result = joinPoint.proceed();”這行程式碼是被註解的方法執行的地方。因此,在此之前,我們啟動我們的計時類計時,在這之後,停止計時。
  • 最後,我們構造日誌資訊,用 Android Log 輸出。

使 AspectJ 執行在 Anroid 上

現在,所有程式碼都可以正常工作了,但是,如果我們編譯我們的例子,我們並沒有看到任何事情發生。原因是我們必須使用 AspectJ 的編譯器(ajc,一個java編譯器的擴充套件)對所有受 aspect 影響的類進行織入。這就是為什麼,我之前提到的,我們需要在 gradle 的編譯 task 中增加一些額外配置,使之能正確編譯執行。

我們的 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;
      }
    }
  }
}

我們的測試方法

我們新增一個測試方法,來使用我們炫酷的 aspect 註解。我已經在主 Activity 類中增加了一個方法用來測試。看下程式碼:

  @DebugTrace
  private void testAnnotatedMethod() {
    try {
      Thread.sleep(10);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
  }

執行我們的應用

我們用 gradle 命令編譯部署我們的 app 到 android 裝置或者模擬器上:

gradlew clean build installDebug

If we open the logcat and execute our sample, we will see a debug log with:
如果我們開啟 logcat,執行我們的例子,會看到一條 debug 日誌:

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

我們的第一個使用 AOP 的 Androd 應用可以工作了!
你可以用 Dex Dump 或者任何其他的逆向工具反編譯 apk 檔案,看一下生成和注入的程式碼。

回顧

回顧總結如下:

  • 我們已經對面向切面程式設計(AOP)這一正規化有了初步體驗。
  • 程式碼注入是 AOP 中的重要部分。
  • AspectJ 是在 Android 應用中進行程式碼織入的強大且易用的工具。
  • 我們已經使用 AOP 能力建立了一個可以工作的示例。

結論

面向切面程式設計很強大。通過正確使用,你可以在開發你的 Android 應用時,避免在『cross-cutting concerns』處複製大量程式碼,比如我們在示例中看到的效能監控部分。我非常鼓勵你嘗試一下,你會發現它非常有用。

我希望你能喜歡這篇文章,文章的目的是分享我學到的東西,所以,歡迎評論和反饋,如果能 fork 程式碼玩一下就更好了。

我確信我們能在示例 app 的 AOP 模組裡增加些有趣的東西,歡迎提出你的想法;)。

原始碼

資源



文/mao眼(簡書作者)
原文連結:http://www.jianshu.com/p/0fa8073fd144
著作權歸作者所有,轉載請聯絡作者獲得授權,並標註“簡書作者”。