面向切面编程AspectJ在Android埋点的实践

4,067

在项目开发中,对 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语言中的类很像,但是方面还封装了切点、通知以及类型间声明。

正常情况下,我们会把一个简单的示例应用拆分成两个 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端非侵入式埋点

美团移动性能监控