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
微信公众号:终端研发部

技术+职场
技术+职场
相关文章
相关标签/搜索