大家对AOP应该都不陌生, 就算没有用过也肯定听说过. 用过或了解过Java AOP的同学应该都知道AspectJ的大名. 因为AspectJ与java程序完全兼容,几乎是无缝关联, 所以只需要做一些简单的AJC适配就可以用在Android开发中. AspectJ用法简单容易上手, 不像之前说过的ASM那样有陡峭的学习曲线, 需要了解Java的字节码指令才能做代码注入. 同时AspectJ也很强大, 在AOP方面AspectJ完全可以满足开发需求. 这篇博客会介绍AspectJ的基本术语和用法, 在Android中适配AJC和简单AOP应用, 以及AJC编织之后的反向分析. 之后有机会的话会写一些结合Android和AOP实际用法例如日志管理, 权限管理, 性能检测, 异常管理或统计埋点的库.
代码地址 https://github.com/HiJesse/Android-AOP
AspectJ基础概念
-
Aspect 切面
切面是切入点和通知的集合.
-
PointCut 切入点
切入点是指那些通过使用一些特定的表达式过滤出来的想要切入Advice的连接点.
表达式类型 描述 execution 过滤出方法执行时的连接点 within 过滤出制定类型内方法 this 过滤当前AOP对象的执行时方法 target 过滤目标对象的执行时方法 args 过滤出方法执行时参数匹配args的方法 @within 过滤出持有指定注解类型内的方法 @target 过滤目标对象持有指定注解类型的方法 @args 过滤当前执行的传入的参数持有指定注解的方法 @annotation 过滤当前执行的持有指定注解的方法 匹配语法 描述 * 匹配任何数量字符 … 匹配任何数量字符
匹配任何数量子包
匹配任何数量参数+ 匹配指定类型的子类型 -
Advice 通知
通知是向切点中注入的代码实现方法, 即下面例子中的两个Triggered方法. 根据注入的位置和运行注入代码的时机分为五种Advice.
通知类型 描述 Before 前置通知, 在目标执行之前执行通知 After 后置通知, 目标执行后执行通知 Around 环绕通知, 在目标执行中执行通知,
控制目标执行时机AfterReturning 后置返回通知, 目标返回时执行通知 AfterThrowing 异常通知, 目标抛出异常时执行通知 -
Joint Point 连接点
所有的目标方法都是连接点.
-
Weaving 编织
主要是在编译期使用AJC将切面的代码注入到目标中, 并生成出代码混合过的.class的过程.
Android 适配AJC
AJC是可以编译AspectJ代码的编译器, 不确定全称是AspectJ compiler还是 AOP Java Compiler. 前面提到的Weaving, 就是使用AJC编译实现的. 想要在Android编译期间使用AJC还需要一些Gradle配置. 为了方便大家使用, 我就实现了一个基于AspectJ 1.8.9版本的Gradle插件. Gradle插件的实现之前有讲过-Gradle插件实现传送门. 之后将写好的插件部署到mavenCentral上了, 可以直接引用.
在根项目的build.gradle
引入插件依赖.
buildscript {
repositories {
...
mavenCentral()
...
}
dependencies {
...
classpath 'com.github.hijesse:android-aop:1.0.0'
...
}
}
在需要使用AOP编译的Application或Library模块build.gradle
文件中引入AOP插件
apply plugin: 'android-aop'
插件中实现接入AJC时首先判断一下当前apply插件的模块是否是Application或Library, 否则没有使用AJC的意义.
def hasApp = project.plugins.hasPlugin('com.android.application')
def hasLib = project.plugins.hasPlugin('com.android.library')
if (!hasApp && !hasLib) {
throw new IllegalStateException("'android' or 'android-library' plugin required.")
}
根据不同的module获取对应的variants, 然后遍历.
final def log = project.logger
final def variants
if (hasApp) {
variants = project.android.applicationVariants
} else {
variants = project.android.libraryVariants
}
variants.all { variant ->
...
}
遍历Variant的过程中, 获取当前variant的Java编译环境. 获取到编译器的各个属性放在一个数组中.
JavaCompile javaCompile = variant.javaCompile
javaCompile.doLast {
String[] args = ["-showWeaveInfo",
"-source", javaCompile.sourceCompatibility,
"-target", javaCompile.targetCompatibility,
"-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)
初始化AJC的MessageHandler用来处理所有的消息, 并结合之前拿到的JavaCompile信息启动AJC.
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;
}
}
Android 中AOP 实现
Android中其实有很多可以使用AOP的场景, 例如日志管理, 权限管理, 性能检测, 异常管理和统计埋点等. 这篇文章只是简单得介绍AspectJ在Android中的用法, 在Activity的onCreate方法前后以及onResume方法之前打印日志, 之后可以发挥想象力玩出各种花样.
-
声明Aspect
首先新建一个类, 在头部使用AspectJ注解声明这个类为切面.
@Aspect public class ActivityAOP { ... }
-
声明PointCut
在切面内部使用PointCut注解和上面章节讲到的语法声明两个切点.分别是Activity.onCreate切点.
/** * activity onCreate point cut */ @Pointcut("execution(* android.app.Activity.onCreate(..))") public void activityOnCreate() { //empty method body }
和Activity.onResume切点.
/** * activity onResume point cut */ @Pointcut("execution(* android.app.Activity.onResume())") public void activityOnResume() { //empty method body }
-
实现Advice
根据AspectJ基础概念中对Advice的介绍, 根据自己不同的需求选择不同的时机去调用对应的advice.像要在onCreate方法前后都打印日志, 那么我们就选择使用Around注解.
根据参数中的ProceedingJointPoint获取到当前被切入类的类名和被切入方法的方法签名信息, 加上before注释打印. 调用JointPoint对象的proceed方法执行被切入方法. 执行完业务方法之后再打印带有after注释的日志.
@Around("activityOnCreate()") public void activityOnCreateTriggered(ProceedingJoinPoint joinPoint) throws Throwable{ String targetClassName = joinPoint.getTarget().getClass().getName(); String signatureName = joinPoint.getSignature().getName(); Log.d(TAG, targetClassName + " " + signatureName + " before"); joinPoint.proceed(); Log.d(TAG, targetClassName + " " + signatureName + " after"); }
而对onResume方法的处理只需要使用Before注解, advice的实现同上根据JoinPoint 获取到当前被切入类的类名和被切入方法的方法名, 加上before注释并打印出来.
@Before("activityOnResume()") public void activityOnResumeTriggered(JoinPoint joinPoint) { String targetClassName = joinPoint.getTarget().getClass().getName(); String signatureName = joinPoint.getSignature().getName(); Log.d(TAG, targetClassName + " " + signatureName + " before"); }
根据logcat的日志输出可以看到AOP实现了我们想要的结果, 在onCreate方法前后和onResume方法之前加入了日志.
AOP Weaving
经过上面一节的三个步骤我们已经在Android中实现了AOP. 但是AJC将切面的代码织入到目标中之后是如何修改目标行为的? 猜测应该是AspectJ在编译的过程中根据切面切点和Advice增加目标的行为, 简单理解就是根据声明插入一些AspectJ生成的和Advice代码到目标当中, 以达到新增目标行为的目的. 具体是什么样子的我们还是反向一下代码.
先反编译一下声明的切面类看看有什么变化. 这里为了简洁就只贴出跟Weaving相关的并且是AspectJ自己新增的代码. 整理之后发现新增了一个类的静态实例, 通过静态块在类加载的时候调用postClinit方法对类的静态实例进行初始化. 同时提供了一个对外暴露的aspectOf方法, 该方法会返回切面对象的实例, 如果对象实例为null 则抛出异常.
根据这些新增的代码不难猜出AspectJ应该是要让外部能够通过aspectOf方法获取当前切面的实例, 然后就可以调用我们编写的advice.
public class ActivityAOP {
public static final ActivityAOP ajc$perSingletonInstance;
private static void ajc$postClinit() {
ajc$perSingletonInstance = new ActivityAOP();
}
public static ActivityAOP aspectOf() {
if(ajc$perSingletonInstance == null)
throw new NoAspectBoundException("cn.jesse.aop.ActivityAOP", ajc$initFailureCause);
else
return ajc$perSingletonInstance;
}
static {
try {
ajc$postClinit();
}
catch(Throwable throwable) {
ajc$initFailureCause = throwable;
}
}
}
然后反向切面的目标类MainActivity, 先分析一下共同的东西. 类中新增了两个StaticPart静态对象, 并且在类装载的时候调用preClinit方法, 通过AspectJRT中的工厂方法和一些静态的方法签名信息对着两个StaticPart对象进行初始化. 从方法的静态签名信息中可以看出这两个静态属性对应我们在AOP中使用的到两个JointPoint中的一些信息.
public class MainActivity extends AppCompatActivity {
...
private static final org.aspectj.lang.JoinPoint.StaticPart ajc$tjp_0;
private static final org.aspectj.lang.JoinPoint.StaticPart ajc$tjp_1;
private static void ajc$preClinit() {
Factory factory = new Factory("MainActivity.java", cn/jesse/aop/MainActivity);
ajc$tjp_0 = factory.makeSJP("method-execution", factory.makeMethodSig("4", "onCreate", "cn.jesse.aop.MainActivity", "android.os.Bundle", "savedInstanceState", "", "void"), 12);
ajc$tjp_1 = factory.makeSJP("method-execution", factory.makeMethodSig("4", "onResume", "cn.jesse.aop.MainActivity", "", "", "", "void"), 19);
}
static {
ajc$preClinit();
}
...
}
接下来看被切入的onResume方法有什么变化. 从结果上看来这种Before和After类型Advice对目标的侵入还是比较简单的, 只是在原来的目标实现前面或后面加了两行代码. 还是使用工厂方法根据初始化过方法签名的StaticPart对象tjp_1和目标对象的引用创建出连接点. 再通过上面反向过的切面对象中的aspectOf方法将JoinPoint对象传递给onResume切点的Advice实现中. 最终达到先执行Before类型的Advice, 然后再执行onResume代码的效果.
public class MainActivity extends AppCompatActivity {
...
protected void onResume() {
JoinPoint joinpoint = Factory.makeJP(ajc$tjp_1, this, this);
ActivityAOP.aspectOf().activityOnResumeTriggered(joinpoint);
super.onResume();
Log.d(TAG, "onResume");
}
...
}
而使用Around Advice的侵入就比较复杂了. 修改了onCreate方法后不仅多出了一个静态方法, 还多了一个继承自AroundClosure的内部类. 还是一步一步分析, 先看新增的静态方法. 从代码实现可以看出该方法的实现就是侵入之前onCreate方法中的实现. 看来是把onCreate方法的代码给转移出来, 方便JoinPoint调用.
public class MainActivity extends AppCompatActivity {
...
static final void onCreate_aroundBody0(MainActivity mainactivity, Bundle bundle, JoinPoint joinpoint) {
mainactivity.AppCompatActivity.onCreate(bundle);
mainactivity.setContentView(0x7f04001b);
Log.d(TAG, "onCreate");
}
...
}
看到新增的内部类就明白了, 在内部类的run方法中调用了上面的静态方法. 从AspectJRT的源码中可以了解到, 在调用JointPoint的proceed方法时其实就是调用的这个内部类对象的run方法, 从而执行onCreate方法中原本的代码.
public class MainActivity extends AppCompatActivity {
...
private class AjcClosure1 extends AroundClosure {
public Object run(Object aobj[]) {
aobj = super.state;
MainActivity.onCreate_aroundBody0((MainActivity)aobj[0], (Bundle)aobj[1], (JoinPoint)aobj[2]);
return null;
}
public AjcClosure1(Object aobj[]) {
super(aobj);
}
}
...
}
回到onCreate方法, 方法的实现已经被改的面目全非. 原本的实现已经被转移到了对应的静态方法中, 而新的实现就是将AjcClosure1内部类和JoinPoint联系起来构建出ProceedingJoinPoint对象, 并且调用onCreate方法的Advice实现方法. 在Advice中决定何时调用onCreate方法原本的实现.
public class MainActivity extends AppCompatActivity {
...
protected void onCreate(Bundle bundle) {
JoinPoint joinpoint = Factory.makeJP(ajc$tjp_0, this, this, bundle);
ActivityAOP.aspectOf().activityOnCreateTriggered((new AjcClosure1(new Object[] {
this, bundle, joinpoint
})).linkClosureAndJoinPoint(0x11010));
}
...
}
转载请注明出处:http://blog.csdn.net/l2show/article/details/63684383