面向切面程式設計AspectJ在Android埋點的實踐

xiangzhihong發表於2018-07-18

在專案開發中,對 App 客戶端重構後,發現用於統計使用者行為的友盟統計程式碼和使用者行為日誌記錄程式碼分散在各業務模組中,比如在某個模組,要想實現對使用者的行為一和行為二進行統計,因此按照OOP物件導向程式設計思想,就需要把友盟統計的程式碼以強依賴的形式寫入相應的模組中,這樣會造成專案業務邏輯混亂,並且不利於對外提供SDK。因此,通過研究發現,在Android專案中,可以使用AOP面向切面程式設計思想,把專案中所有的友盟統計程式碼,從各個業務模組提取出來,統一放到一個模組裡面,這樣就可以避免我們提供的SDK中包含使用者不需要的友盟SDK及其相關程式碼。

AOP

面向切面程式設計(AOP,Aspect-oriented programming):是一種可以通過預編譯方式和執行期動態代理實現在不修改原始碼的情況下給程式動態統一新增功能的技術。AOP是OOP的延續,是軟體開發中的一個熱點,是函數語言程式設計的一種衍生範型,將程式碼切入到類的指定方法、指定位置上的程式設計思想。利用AOP可以對業務邏輯的各個部分進行隔離,從而使得業務邏輯各部分之間的耦合度降低,提高程式的可重用性,同時提高了開發的效率。

AOP、OOP在字面上雖然非常類似,但卻是面向不同領域的兩種設計思想。OOP(物件導向程式設計)針對業務處理過程的實體及其屬性和行為進行抽象封裝,以獲得更加清晰高效的邏輯單元劃分,而AOP則是針對業務處理過程中的切面進行提取,它所面對的是處理過程中的某個步驟或階段,以獲得邏輯過程中各部分之間低耦合性的隔離效果。這兩種設計思想在目標上有著本質的差異。舉個簡單的例子,對於“僱員”這樣一個業務實體進行封裝,自然是OOP/OOD的任務,我們可以為其建立一個“Employee”類,並將“僱員”相關的屬性和行為封裝其中,若用AOP設計思想對“僱員”進行封裝將無從談起,同樣,對於“許可權檢查”這一動作片斷進行劃分,則是AOP的目標領域,若通過OOD/OOP對一個動作進行封裝,則有點不倫不類。

AOP程式設計的主要用途有:日誌記錄,行為統計,安全控制,事務處理,異常處理,系統統一的認證、許可權管理等。可以使用AOP技術將這些程式碼從業務邏輯程式碼中劃分出來,通過對這些行為的分離,可以將它們獨立到非指導業務邏輯的方法中,進而改變這些行為的時候不影響業務邏輯的程式碼。

AOP程式設計的常見的使用場景:

  • 日誌記錄
  • 持久化
  • 行為監測
  • 資料驗證
  • 快取
  • ...

程式碼注入時機

程式碼注入主要利用了Java的反射和註解機制,根據註解時機的不同,主要分為執行時、載入時和編譯時。

  • 執行時:你的程式碼對增強程式碼的需求很明確,比如,必須使用動態代理(這可以說並不是真正的程式碼注入)。
  • 載入時:當目標類被Dalvik或者ART載入的時候修改才會被執行。這是對Java位元組碼檔案或者Android的dex檔案進行的注入操作。
  • 編譯時:在打包釋出程式之前,通過向編譯過程新增額外的步驟來修改被編譯的類。

常見AOP程式設計庫

在Java中,常見的面向切面程式設計的開源庫有: AspectJ:和Java語言無縫銜接的面向切面的程式設計的擴充套件工具(可用於Android)。 Javassist for Android:一個移植到Android平臺的非常知名的操縱位元組碼的java庫。 DexMaker:用於在Dalvik VM編譯時或執行時生成程式碼的基於java語言的一套API。 ASMDEX:一個位元組碼操作庫(ASM),但它處理Android可執行檔案(DEX位元組碼)。

Aspectj

AOP是一個概念,一個規範,本身並沒有設定具體語言的實現,這實際上提供了非常廣闊的擴充套件的能力。AspectJ是AOP的一個很悠久的實現,它能夠和 Java 配合起來使用,除此之外還有ASMDex,不過最出名還是Aspectj。

AspectJ的使用核心就是它的編譯器,它就做了一件事,將AspectJ的程式碼在編譯期插入目標程式當中,執行時跟在其它地方沒什麼兩樣,因此要使用它最關鍵的就是使用它的編譯器去編譯程式碼ajc。ajc會構建目標程式與AspectJ程式碼的聯絡,在編譯期將AspectJ程式碼插入被切出的PointCut中,達到AOP的目的。

要理解AspectJ,就需要理解AspectJ提出的幾個新的概念。

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

  • Cross-cutting concerns:即使在物件導向程式設計中大多數類都是執行一個單一的、特定的功能,它們也有時候需要共享一些通用的輔助功能。
  • Advice:需要被注入到.class位元組碼檔案的程式碼。通常有三種:before,after和around,分別是在目標方法執行前,執行後以及替換目的碼執行。除了程式碼注入外,你還可以做一些別的修改,例如新增成員變數和介面到一個類中。
  • Join point:程式中執行程式碼插入的點,例如方法呼叫時或者方法執行時。
  • Pointcut:告訴程式碼注入工具在哪裡注入特定程式碼的表示式(即需要在哪些Joint point應用特定的Advice)。
  • Aspect: Aspect將pointcut和advice 聯絡在一起。例如,我們通過定義一個pointcut和給出一個準確的advice實現向我們的程式中新增一個列印日誌功能的aspect。

執行的流程:一個連線點是程式流中指定的一點。切點收集特定的連線點集合和在這些點中的值。一個通知是當一個連線點到達時執行的程式碼,這些都是AspectJ的動態部分。其實連線點就好比是程式中的一條一條的語句,而切點就是特定一條語句處設定的一個斷點,它收集了斷點處程式棧的資訊,而通知就是在這個斷點前後想要加入的程式程式碼。AspectJ中也有許多不同種類的型別間宣告,這就允許程式設計師修改程式的靜態結構、名稱、類的成員以及類之間的關係。AspectJ中的方面是橫切關注點的模組單元。它們的行為與Java語言中的類很像,但是方面還封裝了切點、通知以及型別間宣告。

面向切面程式設計AspectJ在Android埋點的實踐

正常情況下,我們會把一個簡單的示例應用拆分成兩個 modules,第一個包含我們的 Android App 程式碼,第二個是一個 Android Library 工程,使用 AspectJ 織入程式碼(程式碼注入)。其工程結構圖如下:

面向切面程式設計AspectJ在Android埋點的實踐

Android整合AspectJ

整合AspectJ主要有兩種方式: 1,外掛的方式:網上有人在github上提供了整合的外掛gradle-android-aspectj-plugin。這種方式配置簡單方便,但經測試無法相容databinding框架。

2,Gradle配置的方式:配置有點麻煩,不過國外一個大牛在build檔案中新增了一些指令碼,雖然有點難懂,但可以在AS中使用。文章出處:https://fernandocejas.com/2014/08/03/aspect-oriented-programming-in-android/

下面講講如何在Android專案中整合AspectJ。 1,首先,新建一個AS原工程,然後再建立一個module(Android Library) 。

面向切面程式設計AspectJ在Android埋點的實踐
由於aspectj編譯時需要用到ajc編譯器,為了使 Aspectj能在Android上執行,將aspect模組的程式碼注入app中,需要使用gradle外掛完成編譯。

2,在gintonic中新增AspectJ依賴,同時編寫build指令碼,新增任務,使得IDE使用ajc作為編譯器編譯程式碼,然後把該Module新增至主工程Module中。

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:2.1.0'
    classpath 'org.aspectj:aspectjtools:1.8.1'
  }
}

apply plugin: 'com.android.library'

repositories {
  mavenCentral()
}

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

android {
  compileSdkVersion 21
  buildToolsVersion '21.1.2'

  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;
      }
    }
  }
}
複製程式碼

3,然後在主build.gradle(Module:app)中新增也要新增AspectJ依賴,同時編寫build指令碼,新增任務,目的就是為了建立兩者的通訊,使得IDE使用ajc編譯程式碼。

apply plugin: 'com.android.application'
import org.aspectj.bridge.IMessage
import org.aspectj.bridge.MessageHandler
import org.aspectj.tools.ajc.Main

buildscript {
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath 'org.aspectj:aspectjtools:1.8.1'
    }
}
repositories {
    mavenCentral()
}

android {


    compileSdkVersion 21
    buildToolsVersion '21.1.2'

    defaultConfig {
        applicationId 'com.example.myaspectjapplication'
        minSdkVersion 15
        targetSdkVersion 21
    }

    lintOptions {
        abortOnError true
    }
}

final def log = project.logger
final def variants = project.android.applicationVariants

variants.all { variant ->
    if (!variant.buildType.isDebuggable()) {
        log.debug("Skipping non-debuggable build type '${variant.buildType.name}'.")
        return;
    }

    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", project.android.bootClasspath.join(File.pathSeparator)]
        log.debug "ajc args: " + Arrays.toString(args)

        MessageHandler handler = new MessageHandler(true);
        new Main().run(args, handler);
        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:
                    log.warn message.message, message.thrown
                    break;
                case IMessage.INFO:
                    log.info message.message, message.thrown
                    break;
                case IMessage.DEBUG:
                    log.debug message.message, message.thrown
                    break;
            }
        }
    }
}
dependencies {
    compile fileTree(include: ['*.jar'], dir: 'libs')
    androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
        exclude group: 'com.android.support', module: 'support-annotations'
    })
    //compile 'com.android.support:appcompat-v7:25.3.1'
    //compile 'com.android.support.constraint:constraint-layout:1.0.2'
    testCompile 'junit:junit:4.12'
    compile project(':gintonic')
    compile 'org.aspectj:aspectjrt:1.8.1'
}
複製程式碼

App主模組與其他庫工程中的groovy構建語句唯一的差別是獲取"-bootclasspath"的方法不同,主模組中配置是project.android.bootClasspath.join(File.pathSeparator),而在庫工程中則是plugin.getAndroidBuilder().getBootClasspath(true).join(File.pathSeparator)

需要注意的是,由於不同版本的gradle在獲取編譯時獲取類的路徑等資訊Api不同,所以以上groovy配置語句僅在Gradle Version高於3.3的版本上生效。

4,在Module(gintonic)中新建一個名為”TraceAspect”類,用於進行測試。

@Aspect
public class TraceAspect {

  //ydc start
  private static final String TAG = "ydc";
  @Before("execution(* android.app.Activity.on**(..))")
  public void onActivityMethodBefore(JoinPoint joinPoint) throws Throwable {
    String key = joinPoint.getSignature().toString();
    Log.d(TAG, "onActivityMethodBefore: " + key+"\n"+joinPoint.getThis());
  }
 }
複製程式碼

然後我們新建一個測試頁面LinearLayoutTestActivity類,程式碼如下:

public class LinearLayoutTestActivity extends Activity {

  private LinearLayout myLinearLayout;

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_linear_layout_test);

    myLinearLayout = (LinearLayout) findViewById(R.id.linearLayoutOne);
    myLinearLayout.invalidate();
  }
}
複製程式碼

然後我們執行專案,很神奇的事情出現了,LinearLayoutTestActivity中的onCreate(Bundle savedInstanceState)方法被TraceAspect類監控了,不僅擷取到了LinearLayoutTestActivity類資訊和方法及方法引數。通過反編譯apk,可以看一下相關的程式碼。

面向切面程式設計AspectJ在Android埋點的實踐

可以發現,在onCreate執行之前,插入了一些AspectJ的程式碼,並且呼叫了TraceAspect中的 onActivityMethodBefore(JoinPoint joinPoint)方法。

參考:AOP程式設計之AspectJ實戰實現資料埋點

AspectJ實現Android端非侵入式埋點

美團移動效能監控

相關文章