Android監測用戶行爲之中AOP編程之AspectJ實戰

文章背景

  • 最近在給某某銀行作項目的時,涉及到了數據埋點,性能監控等問題,那咱們起先想到的有兩種方案:java

    • 方案之一就是藉助第三方,好比友盟、Bugly等,因爲項目是部署在銀行的網絡框架以內的,因此該方案不可行。
    • 另一種方案是就是給每個方法裏面數據打點,而後寫入SD卡,定時上報給本身的服務器,服務器來進行統計分析

    這種方案看上去彷佛可行,但有弊端,不只會給程序員增長巨大工做量、並且最致命的是會嚴重拖累整個APP的性能。android

  • 好多都應無奈之舉放棄了該需求,但數據埋點實現用戶行爲的收集分析和性能監控對於技術部和運營部來講是一件很是有價值的事情,因此做爲程序的我必應尋找解決方案git

  • 慶幸的是咱們除了OOP編程思想外,還有一種編程思想就是AOP編程,這種編程思想能解決此類問題。

文章目標

  • 實現用戶行爲採集
  • 實現方法性能監控
  • 探討AOP編程實戰

看圖簡單解讀Android的AOP實戰程序員

看到沒有,在LinearLayoutTestActivity中除了加載佈局的操做外,我並無幹其餘的什麼,但當我點擊菜單跳轉到該Activity時,onCreate的方法和參數被打印出來,甚至LinearLayoutTestActivity類信息也被打印出來了,幹這個事情的是TraceAspect這個類。到這裏上面所說的用戶的行爲跟蹤就垂手可得得以實現,那麼下面咱們開始來了解一下這種技術。github

什麼是AOP

  • 面向切面編程(AOP,Aspect-oriented programming)須要把程序邏輯分解成『關注點』(concerns,功能的內聚區域)。正則表達式

  • 這意味着,在 AOP 中,咱們不須要顯式的修改就能夠向代碼中添加可執行的代碼塊。這種編程範式假定『橫切關注點』(cross-cutting concerns,多處代碼中須要的邏輯,但沒有一個單獨的類來實現)應該只被實現一次,且可以屢次注入到須要該邏輯的地方。編程

  • 代碼注入是 AOP 中的重要部分:它在處理上述說起的橫切整個應用的『關注點』時頗有用,例如日誌或者性能監控。這種方式,並不如你所想的應用甚少,相反的,每一個程序員均可以有使用這種注入代碼能力的場景,這樣能夠避免不少痛苦和無奈。bash

  • AOP 是一種已經存在了不少年的編程範式。我發現把它應用到 Android 開發中也頗有用。通過一番調研後,我認爲咱們用它能夠得到不少好處和有用的東西。服務器

AspectJ是什麼

  • AspectJ其實是對AOP編程思想的一個實踐,它不是一個新的語言,它就是一個代碼編譯器(ajc)
  • 在Java編譯器的基礎上增長了一些它本身的關鍵字識別和編譯方法。所以,ajc也能夠編譯Java代碼。微信

  • 它在編譯期將開發者編寫的Aspect程序編織到目標程序中,對目標程序做了重構,目的就是創建目標程序與Aspect程序的鏈接(耦合,得到對方的引用(得到的是聲明類型,不是運行時類型)和上下文信息),從而達到AOP的目的(這裏在編譯期仍是修改了原來程序的代碼,可是是ajc替咱們作的)。

  • 固然,除了AspectJ之外,還有不少其它的AOP實現,例如XPosed、DexPosed、ASMDex。

    爲何用 AspectJ?

  • 功能強大:它就是一個編譯器+一個庫,可讓開發者最大限度的發揮,實現形形色色的AOP程序!

  • 非侵入式監控: 能夠在不修監控目標的狀況下監控其運行,截獲某類方法,甚至能夠修改其參數和運行軌跡!

  • 支持編譯期和加載時代碼注入,不影響性能。

  • 易用易學:它就是Java,只要會Java就能夠用它。

    如何Android項目中使用AspectJ

    使用方法有兩種:

  • 插件的方式:網上有人在github上提供了集成的插件gradle-android-aspectj-plugin。這種方式配置簡單方便,但經測試沒法兼容databinding框架,這個問題如今做者依然沒有解決,但願做者可以快速解決。

  • Gradle配置的方式:配置有點麻煩,不過國外一個大牛在build文件中添加了一些腳本,雖然有點難懂,但能夠在AS中使用。文章出處:fernandocejas.com/2014/08/03/…

Step

  • 一、建立一個AS原工程

    Paste_Image.png
    Paste_Image.png

  • 二、再建立一個module(Android Library)

Paste_Image.png
Paste_Image.png

  • 三、在gintonic中添加AspectJ依賴,同時編寫build腳本,添加任務,使得IDE使用ajc做爲編譯器編譯代碼,而後把該Module添加至主工程Module中。

    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;
            }
          }
        }
      }複製代碼
  • 四、在主build.gradle(Module:app)中添加也要添加AspectJ依賴,同時編寫build腳本,添加任務,目的就是爲了創建二者的通訊,使得IDE使用ajc編譯代碼。

    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'
      }複製代碼
  • 五、在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();
        }
      }複製代碼

而後運行咱們的程序看日誌打印效果

上面的代碼片斷中有兩處地方值得注意,一個是把這個類註解爲@Aspect,另外一個是給方法的的註解並加上了相似正則表達式的過濾條件,咱們先按照個人步驟走,後面會一一講解。

根據圖片咱們會驚奇的發現LinearLayoutTestActivity中的onCreate(Bundle savedInstanceState)方法被TraceAspect類赤裸裸的監控了,不只截取到了LinearLayoutTestActivity類信息和方法及方法參數。

那這究竟是怎麼回事呢?咱們可使用反編譯個人apk看一下相關的代碼
[圖片上傳中。。。(6)]

咱們能夠發現,在onCreate執行以前,插入了一些AspectJ的代碼,而且調用了TraceAspect中的 onActivityMethodBefore(JoinPoint joinPoint)方法。這個就是AspectJ的主要功能,拋開AOP的思想來講,咱們想作的,實際上就是『在不侵入原有代碼的基礎上,增長新的代碼』。

監控Activity的下其它被調用的方法

看到沒有咱們僅僅在TraceAspect類中編寫一個方法就能夠監控RelativeLayoutTestActivity中被用戶點擊的方法,這樣就能夠輕輕鬆鬆採集用戶行

  • 代碼:

    @Around("execution(* com.example.myaspectjapplication.activity.RelativeLayoutTestActivity.testAOP())")
       public void onActivityMethodAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
         String key = proceedingJoinPoint.getSignature().toString();
         Log.d(TAG, "onActivityMethodAroundFirst: " + key);
         proceedingJoinPoint.proceed();
         Log.d(TAG, "onActivityMethodAroundSecond: " + key);
       }複製代碼

咱們仍是照樣看來看一下反編譯的代碼
這是在RelativeLayoutTestActivity類中調用testAOP()咱們的源碼:

public class RelativeLayoutTestActivity extends Activity {

  Button btn_test,btn_test2;
  //public static String A="88888";
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_relative_layout_test);
    btn_test=(Button)findViewById(R.id.btn_test);
    btn_test.setOnClickListener(new View.OnClickListener() {
      @Override
      public void onClick(View v) {
        testAOP();
      }
    });
    btn_test2=(Button)findViewById(R.id.btn_test2);
    btn_test2.setOnClickListener(new View.OnClickListener() {
      @Override
      public void onClick(View v) {
          mytestDebug();
      }
    });

  }

  private  void testAOP(){

    int cunt=0;
    for ( int i=0;i<1000;i++){
      cunt++;

    }
    //Log.d("ydc","cunt:"+cunt+"");
  }

  private void method4Call() {
    //System.out.println("in method method4Call");
  }

  @DebugTrace
  private void  mytestDebug(){

  }
}複製代碼

下面是反編譯的代碼,讀者只要關注testAOP()方法便可

public class RelativeLayoutTestActivity extends Activity
{
  private static final JoinPoint.StaticPart ajc$tjp_0;
  private static final JoinPoint.StaticPart ajc$tjp_1;
  private static final JoinPoint.StaticPart ajc$tjp_2;
  Button btn_test;
  Button btn_test2;

  static
  {
    ajc$preClinit();
  }

  private static void ajc$preClinit()
  {
    Factory localFactory = new Factory("RelativeLayoutTestActivity.java", RelativeLayoutTestActivity.class);
    ajc$tjp_0 = localFactory.makeSJP("method-execution", localFactory.makeMethodSig("4", "onCreate", "com.example.myaspectjapplication.activity.RelativeLayoutTestActivity", "android.os.Bundle", "savedInstanceState", "", "void"), 27);
    ajc$tjp_1 = localFactory.makeSJP("method-execution", localFactory.makeMethodSig("2", "testAOP", "com.example.myaspectjapplication.activity.RelativeLayoutTestActivity", "", "", "", "void"), 48);
    ajc$tjp_2 = localFactory.makeSJP("method-execution", localFactory.makeMethodSig("2", "mytestDebug", "com.example.myaspectjapplication.activity.RelativeLayoutTestActivity", "", "", "", "void"), 63);
  }

  private void method4Call()
  {
  }

  @DebugTrace
  private void mytestDebug()
  {
    JoinPoint localJoinPoint = Factory.makeJP(ajc$tjp_2, this, this);
    TraceAspect.aspectOf().weaveJoinPoint(new RelativeLayoutTestActivity.AjcClosure3(new Object[] { this, localJoinPoint }).linkClosureAndJoinPoint(69648));
  }

  static final void mytestDebug_aroundBody2(RelativeLayoutTestActivity paramRelativeLayoutTestActivity, JoinPoint paramJoinPoint)
  {
  }

  private void testAOP()
  {
    JoinPoint localJoinPoint = Factory.makeJP(ajc$tjp_1, this, this);
    TraceAspect.aspectOf().onActivityMethodAround(new RelativeLayoutTestActivity.AjcClosure1(new Object[] { this, localJoinPoint }).linkClosureAndJoinPoint(69648));
  }

  static final void testAOP_aroundBody0(RelativeLayoutTestActivity paramRelativeLayoutTestActivity, JoinPoint paramJoinPoint)
  {
    int i = 0;
    for (int j = 0; j < 1000; j++)
      i++;
  }

  protected void onCreate(Bundle paramBundle)
  {
    JoinPoint localJoinPoint = Factory.makeJP(ajc$tjp_0, this, this, paramBundle);
    TraceAspect.aspectOf().onActivityMethodBefore(localJoinPoint);
    super.onCreate(paramBundle);
    setContentView(2130903043);
    this.btn_test = ((Button)findViewById(2131230727));
    this.btn_test.setOnClickListener(new View.OnClickListener()
    {
      public void onClick(View paramAnonymousView)
      {
        RelativeLayoutTestActivity.this.testAOP();
      }
    });
    this.btn_test2 = ((Button)findViewById(2131230728));
    this.btn_test2.setOnClickListener(new View.OnClickListener()
    {
      public void onClick(View paramAnonymousView)
      {
        RelativeLayoutTestActivity.this.mytestDebug();
      }
    });
  }
}複製代碼

咱們不難發現咱們的代碼輕鬆被AspectJ重構了,並且這種重構是在不修改原有代碼的狀況下無縫的被插入。

Fragment的中的方法監控

上面我已經演示過Activity中的方法強勢插入,在Fragment中依然可行

@Before("execution(* android.app.Fragment.on**(..))")
  public void onActivityMethodBefore(JoinPoint joinPoint) throws Throwable {
    String key = joinPoint.getSignature().toString();
    Log.d(TAG, "onActivityMethodBefore: " + key+"\n"+joinPoint.getThis());
  }複製代碼

Paste_Image.png
Paste_Image.png

AspectJ原理剖析

  • 一、Join Points(鏈接點)
    Join Points,簡稱JPoints,是AspectJ的核心思想之一,它就像一把刀,把程序的整個執行過程切成了一段段不一樣的部分。例如,構造方法調用、調用方法、方法執行、異常等等,這些都是Join Points,實際上,也就是你想把新的代碼插在程序的哪一個地方,是插在構造方法中,仍是插在某個方法調用前,或者是插在某個方法中,這個地方就是Join Points,固然,不是全部地方都能給你插的,只有能插的地方,才叫Join Points。

  • 二、Pointcuts(切入點)
    告訴代碼注入工具,在何處注入一段特定代碼的表達式。例如,在哪些 joint points 應用一個特定的 Advice。切入點能夠選擇惟一一個,好比執行某一個方法,也能夠有多個選擇,可簡單理解爲帶條件的Join Points,做爲咱們須要的代碼切入點。

  • 三、Advice(通知)
    如何注入到個人class文件中的代碼。典型的 Advice 類型有 before、after 和 around,分別表示在目標方法執行以前、執行後和徹底替代目標方法執行的代碼。 上面的例子中用的就是最簡單的Advice——Before。
  • 四、Aspect(切面): Pointcut 和 Advice 的組合看作切面。例如,咱們在應用中經過定義一個 pointcut 和給定恰當的advice,添加一個日誌切面。
  • 五、Weaving(織入): 注入代碼(advices)到目標位置(joint points)的過程。
    因爲微信對篇幅大小的限制,這裏只是關於部分AOP的部分講解。
    更多參考: blog.csdn.net/xinanheisha…

注意這裏

AOP 應用場景很是多。
只要在咱們想監控的方法上加上 @DebugTrace便可,我在這裏給onMeasure方法上註解,當我進入LinearLayoutTestActivity 類時,運行以下:

項目結果顯示
項目結果顯示

博客地址:

blog.csdn.net/xinanheisha…

最後附上Dome下載地址:

download.csdn.net/download/xi…

提供一個反編譯工具:

apk反編譯工具下載地址
blog.csdn.net/xinanheisha…

相信本身,沒有作不到的,只有想不到的

若是你以爲此文對您有所幫助,歡迎入羣 QQ交流羣 :232203809
微信公衆號:終端研發部

技術+職場
技術+職場
相關文章
相關標籤/搜索