【翻譯】Android中的AOP編程

Android 中的 AOP 編程

面向切面編程(AOP首懈,Aspect-oriented programming)需要把程序邏輯分解成『關(guān)注點(diǎn)』(concerns勋功,功能的內(nèi)聚區(qū)域)赞别。這意味著垢粮,在 AOP 中狂打,我們不需要顯式的修改就可以向代碼中添加可執(zhí)行的代碼塊枯冈。這種編程范式假定『橫切關(guān)注點(diǎn)』(cross-cutting concerns华坦,多處代碼中需要的邏輯撮躁,但沒有一個單獨(dú)的類來實(shí)現(xiàn))應(yīng)該只被實(shí)現(xiàn)一次实辑,且能夠多次注入到需要該邏輯的地方捺氢。

代碼注入是 AOP 中的重要部分:它在處理上述提及的橫切整個應(yīng)用的『關(guān)注點(diǎn)』時很有用,例如日志或者性能監(jiān)控剪撬。這種方式摄乒,并不如你所想的應(yīng)用甚少,相反的残黑,每個程序員都可以有使用這種注入代碼能力的場景馍佑,這樣可以避免很多痛苦和無奈。

AOP 是一種已經(jīng)存在了很多年的編程范式梨水。我發(fā)現(xiàn)把它應(yīng)用到 Android 開發(fā)中也很有用拭荤。經(jīng)過一番調(diào)研后,我認(rèn)為我們用它可以獲得很多好處和有用的東西疫诽。

術(shù)語(迷你術(shù)語表)

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

  • Cross-cutting concerns(橫切關(guān)注點(diǎn)): 盡管面向?qū)ο竽P椭写蠖鄶?shù)類會實(shí)現(xiàn)單一特定的功能旦委,但通常也會開放一些通用的附屬功能給其他類。例如雏亚,我們希望在數(shù)據(jù)訪問層中的類中添加日志缨硝,同時也希望當(dāng)UI層中一個線程進(jìn)入或者退出調(diào)用一個方法時添加日志。盡管每個類都有一個區(qū)別于其他類的主要功能评凝,但在代碼里追葡,仍然經(jīng)常需要添加一些相同的附屬功能。

  • Advice(通知): 注入到class文件中的代碼奕短。典型的 Advice 類型有 before宜肉、after 和 around,分別表示在目標(biāo)方法執(zhí)行之前翎碑、執(zhí)行后和完全替代目標(biāo)方法執(zhí)行的代碼谬返。 除了在方法中注入代碼,也可能會對代碼做其他修改日杈,比如在一個class中增加字段或者接口遣铝。

  • Joint point(連接點(diǎn)): 程序中可能作為代碼注入目標(biāo)的特定的點(diǎn),例如一個方法調(diào)用或者方法入口莉擒。

  • Pointcut(切入點(diǎn)): 告訴代碼注入工具酿炸,在何處注入一段特定代碼的表達(dá)式。例如涨冀,在哪些 joint points 應(yīng)用一個特定的 Advice填硕。切入點(diǎn)可以選擇唯一一個,比如執(zhí)行某一個方法鹿鳖,也可以有多個選擇扁眯,比如,標(biāo)記了一個定義成@DebguTrace 的自定義注解的所有方法翅帜。

  • Aspect(切面): Pointcut 和 Advice 的組合看做切面姻檀。例如,我們在應(yīng)用中通過定義一個 pointcut 和給定恰當(dāng)?shù)腶dvice涝滴,添加一個日志切面绣版。

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

下面這張圖簡要總結(jié)了一下上述這些概念歼疮。

那么...我們何時何地應(yīng)用AOP呢杂抽?

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

  • 日志
  • 持久化
  • 性能監(jiān)控
  • 數(shù)據(jù)校驗(yàn)
  • 緩存
  • 其他更多

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

工具和庫

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

  • AspectJ: 一個 JavaTM 語言的面向切面編程的無縫擴(kuò)展(適用Android)腋妙。

  • Javassist for Android: 用于字節(jié)碼操作的知名 java 類庫 Javassist 的 Android 平臺移植版。

  • DexMaker: Dalvik 虛擬機(jī)上讯榕,在編譯期或者運(yùn)行時生成代碼的 Java API骤素。

  • ASMDEX: 一個類似 ASM 的字節(jié)碼操作庫匙睹,運(yùn)行在Android平臺,操作Dex字節(jié)碼济竹。

為什么用 AspectJ痕檬?

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

  • 功能強(qiáng)大
  • 支持編譯期和加載時代碼注入
  • 易于使用

示例

比方說送浊,我們要測量一個方法的性能(執(zhí)行這個方法需要多長時間)梦谜。為此我們用一個 @DebugTrace 的注解標(biāo)記我們的這個方法,并且無需在每個注解過的方法中編寫代碼袭景,就可以通過 logcat 輸出結(jié)果唁桩。我們的方法是使用 AspectJ 達(dá)到這個目的。

我們看下在底層到底發(fā)生了什么:

  • 我們在編譯過程中增加一個新的步驟處理注解耸棒。
  • 注解的方法內(nèi)會生成和注入必要的樣板代碼荒澡。

在此,我必須要提到當(dāng)我研究這些時与殃,發(fā)現(xiàn)了Jake Wharton’s Hugo Library 這個項(xiàng)目单山,支持做同樣的事情。因此幅疼,我重構(gòu)了我的代碼米奸,看上去和它類似。盡管爽篷,我的代碼是一個更加原始和簡化的版本(順便提一下悴晰,通過看這個項(xiàng)目的代碼,我學(xué)到了很多)狼忱。

工程結(jié)構(gòu)

我們會把一個簡單的示例應(yīng)用拆分成兩個 modules膨疏,第一個包含我們的 Android App 代碼,第二個是一個 Android Library 工程钻弄,使用 AspectJ 織入代碼(代碼注入)佃却。

你可能會想知道為什么我們用一個 Android Library 工程,而不是用一個純的 Java Library:原因是為了使 AspectJ 能在 Android 上運(yùn)行窘俺,我們必須在編譯時做一些 hook饲帅。這只能使用 andorid-library gradle 插件完成。(先不要為此擔(dān)心瘤泪,后面我會給出更多細(xì)節(jié)灶泵。)

創(chuàng)建注解

首先我們創(chuàng)建我們的Java注解。這個注解周期聲明在 class 文件上(RetentionPolicy.CLASS)对途,可以注解構(gòu)造函數(shù)和方法(ElementType.CONSTRUCTOR 和 ElementType.METHOD)赦邻。因此,我們的 DebugTrace.java 文件看上是這樣的:

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

我們的性能監(jiān)控計(jì)時類

我已經(jīng)創(chuàng)建了一個簡單的計(jì)時類实檀,包含 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”按声,因?yàn)槲沂紫认氲降氖窍?android log 中增加更多的實(shí)用功能。下面是代碼:

/**
 * 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 類

現(xiàn)在是時候創(chuàng)建我們的 Aspect 類(TraceAspect.java)了恬吕。Aspect 類負(fù)責(zé)管理注解的處理和代碼織入签则。

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

幾個在此提到的重點(diǎn):

  • 我們聲明了兩個作為 pointcuts 的 public 方法,篩選出所有通過 “org.android10.gintonic.annotation.DebugTrace” 注解的方法和構(gòu)造函數(shù)铐料。
  • 我們使用 “@Around” 注解定義了“weaveJointPoint(ProceedingJoinPoint joinPoint)”方法,使我們的代碼注入在使用"@DebugTrace"注解的地方生效渐裂。
  • “Object result = joinPoint.proceed();”這行代碼是被注解的方法執(zhí)行的地方。因此钠惩,在此之前柒凉,我們啟動我們的計(jì)時類計(jì)時,在這之后妻柒,停止計(jì)時扛拨。
  • 最后,我們構(gòu)造日志信息举塔,用 Android Log 輸出绑警。

使 AspectJ 運(yùn)行在 Anroid 上

現(xiàn)在,所有代碼都可以正常工作了央渣,但是计盒,如果我們編譯我們的例子,我們并沒有看到任何事情發(fā)生芽丹。原因是我們必須使用 AspectJ 的編譯器(ajc北启,一個java編譯器的擴(kuò)展)對所有受 aspect 影響的類進(jìn)行織入。這就是為什么拔第,我之前提到的咕村,我們需要在 gradle 的編譯 task 中增加一些額外配置,使之能正確編譯運(yùn)行蚊俺。

我們的 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 注解。我已經(jīng)在主 Activity 類中增加了一個方法用來測試泳猬∨疲看下代碼:

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

運(yùn)行我們的應(yīng)用

我們用 gradle 命令編譯部署我們的 app 到 android 設(shè)備或者模擬器上:

gradlew clean build installDebug

If we open the logcat and execute our sample, we will see a debug log with:
如果我們打開 logcat,執(zhí)行我們的例子得封,會看到一條 debug 日志:

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

我們的第一個使用 AOP 的 Androd 應(yīng)用可以工作了埋心!
你可以用 Dex Dump 或者任何其他的逆向工具反編譯 apk 文件,看一下生成和注入的代碼忙上。

回顧

回顧總結(jié)如下:

  • 我們已經(jīng)對面向切面編程(AOP)這一范式有了初步體驗(yàn)拷呆。
  • 代碼注入是 AOP 中的重要部分。
  • AspectJ 是在 Android 應(yīng)用中進(jìn)行代碼織入的強(qiáng)大且易用的工具。
  • 我們已經(jīng)使用 AOP 能力創(chuàng)建了一個可以工作的示例茬斧。

結(jié)論

面向切面編程很強(qiáng)大箫柳。通過正確使用,你可以在開發(fā)你的 Android 應(yīng)用時啥供,避免在『cross-cutting concerns』處復(fù)制大量代碼,比如我們在示例中看到的性能監(jiān)控部分库糠。我非常鼓勵你嘗試一下伙狐,你會發(fā)現(xiàn)它非常有用。

我希望你能喜歡這篇文章瞬欧,文章的目的是分享我學(xué)到的東西贷屎,所以,歡迎評論和反饋艘虎,如果能 fork 代碼玩一下就更好了唉侄。

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

源碼

你可以在 https://github.com/android10/Android-AOPExample 下載示例 app 代碼属划。另外我還有一個使用動態(tài)代理的 Java AOP 示例(也可以用在Android上):https://github.com/android10/DynamicProxy_Java_Sample

資源


譯注

  • AOP 中的術(shù)語并沒有統(tǒng)一的中文翻譯候生,翻譯過程中同眯,術(shù)語一節(jié)我選取了用的比較多的中文名稱注釋在括號中幫助理解,正文中其他部分出現(xiàn)的術(shù)語唯鸭,使用原始英文命名须蜗。
  • 這篇文章是2014年發(fā)布的,2015年7月目溉,阿里巴巴剛剛開源了一個強(qiáng)大的 Android 平臺 AOP 框架 Dexposed明肮,該項(xiàng)目基于著名的 Xposed 項(xiàng)目
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末缭付,一起剝皮案震驚了整個濱河市柿估,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌蛉腌,老刑警劉巖官份,帶你破解...
    沈念sama閱讀 206,311評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異烙丛,居然都是意外死亡舅巷,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評論 2 382
  • 文/潘曉璐 我一進(jìn)店門河咽,熙熙樓的掌柜王于貴愁眉苦臉地迎上來钠右,“玉大人,你說我怎么就攤上這事忘蟹§浚” “怎么了搁凸?”我有些...
    開封第一講書人閱讀 152,671評論 0 342
  • 文/不壞的土叔 我叫張陵,是天一觀的道長狠毯。 經(jīng)常有香客問我护糖,道長,這世上最難降的妖魔是什么嚼松? 我笑而不...
    開封第一講書人閱讀 55,252評論 1 279
  • 正文 為了忘掉前任嫡良,我火速辦了婚禮,結(jié)果婚禮上献酗,老公的妹妹穿的比我還像新娘寝受。我一直安慰自己,他們只是感情好罕偎,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,253評論 5 371
  • 文/花漫 我一把揭開白布很澄。 她就那樣靜靜地躺著,像睡著了一般颜及。 火紅的嫁衣襯著肌膚如雪甩苛。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,031評論 1 285
  • 那天俏站,我揣著相機(jī)與錄音浪藻,去河邊找鬼。 笑死乾翔,一個胖子當(dāng)著我的面吹牛爱葵,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播反浓,決...
    沈念sama閱讀 38,340評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼萌丈,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了雷则?” 一聲冷哼從身側(cè)響起辆雾,我...
    開封第一講書人閱讀 36,973評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎月劈,沒想到半個月后度迂,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,466評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡猜揪,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,937評論 2 323
  • 正文 我和宋清朗相戀三年惭墓,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片而姐。...
    茶點(diǎn)故事閱讀 38,039評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡腊凶,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情褐缠,我是刑警寧澤,帶...
    沈念sama閱讀 33,701評論 4 323
  • 正文 年R本政府宣布队魏,位于F島的核電站万搔,受9級特大地震影響器躏,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜蟹略,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,254評論 3 307
  • 文/蒙蒙 一遏佣、第九天 我趴在偏房一處隱蔽的房頂上張望挖炬。 院中可真熱鬧状婶,春花似錦意敛、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,259評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽稍刀。三九已至,卻和暖如春账月,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背局齿。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工抓歼, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留讥此,地道東北人谣妻。 一個月前我還...
    沈念sama閱讀 45,497評論 2 354
  • 正文 我出身青樓,卻偏偏與公主長得像取胎,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子闻蛀,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,786評論 2 345

推薦閱讀更多精彩內(nèi)容