面向切面編程AspectJ在Android埋點的實踐

在項目開發中,對 App 客戶端重構後,發現用於統計用戶行爲的友盟統計代碼和用戶行爲日誌記錄代碼分散在各業務模塊中,好比在某個模塊,要想實現對用戶的行爲一和行爲二進行統計,所以按照OOP面向對象編程思想,就須要把友盟統計的代碼以強依賴的形式寫入相應的模塊中,這樣會形成項目業務邏輯混亂,而且不利於對外提供SDK。所以,經過研究發現,在Android項目中,可使用AOP面向切面編程思想,把項目中全部的友盟統計代碼,從各個業務模塊提取出來,統一放到一個模塊裏面,這樣就能夠避免咱們提供的SDK中包含用戶不須要的友盟SDK及其相關代碼。html

AOP

面向切面編程(AOP,Aspect-oriented programming):是一種能夠經過預編譯方式和運行期動態代理實如今不修改源代碼的狀況下給程序動態統一添加功能的技術。AOP是OOP的延續,是軟件開發中的一個熱點,是函數式編程的一種衍生範型,將代碼切入到類的指定方法、指定位置上的編程思想。利用AOP能夠對業務邏輯的各個部分進行隔離,從而使得業務邏輯各部分之間的耦合度下降,提升程序的可重用性,同時提升了開發的效率。java

AOP、OOP在字面上雖然很是相似,但倒是面向不一樣領域的兩種設計思想。OOP(面向對象編程)針對業務處理過程的實體及其屬性和行爲進行抽象封裝,以得到更加清晰高效的邏輯單元劃分,而AOP則是針對業務處理過程當中的切面進行提取,它所面對的是處理過程當中的某個步驟或階段,以得到邏輯過程當中各部分之間低耦合性的隔離效果。這兩種設計思想在目標上有着本質的差別。舉個簡單的例子,對於「僱員」這樣一個業務實體進行封裝,天然是OOP/OOD的任務,咱們能夠爲其創建一個「Employee」類,並將「僱員」相關的屬性和行爲封裝其中,若用AOP設計思想對「僱員」進行封裝將無從談起,一樣,對於「權限檢查」這一動做片段進行劃分,則是AOP的目標領域,若經過OOD/OOP對一個動做進行封裝,則有點不三不四。android

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

AOP編程的常見的使用場景:程序員

  • 日誌記錄
  • 持久化
  • 行爲監測
  • 數據驗證
  • 緩存
  • ...

代碼注入時機

代碼注入主要利用了Java的反射和註解機制,根據註解時機的不一樣,主要分爲運行時、加載時和編譯時。github

  • 運行時:你的代碼對加強代碼的需求很明確,好比,必須使用動態代理(這能夠說並非真正的代碼注入)。
  • 加載時:當目標類被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提出的幾個新的概念。bash

##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語言中的類很像,可是方面還封裝了切點、通知以及類型間聲明。

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

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編譯時須要用到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,能夠看一下相關的代碼。

能夠發現,在onCreate執行以前,插入了一些AspectJ的代碼,而且調用了TraceAspect中的 onActivityMethodBefore(JoinPoint joinPoint)方法。

參考:AOP編程之AspectJ實戰實現數據埋點

AspectJ實現Android端非侵入式埋點

美團移動性能監控

相關文章
相關標籤/搜索