Some thoughts on the application of AOP in Android

Basic Concepts of AOP

What is AOP

AOP (Aspect Oriented Programming), aspect-oriented programming, is the continuation of OOP (object-oriented programming).

In OOP thinking, we divide the problem into various modules, such as voice, expression, etc. In the process of dividing these modules, some common features (such as buried points) will also appear. Its logic is scattered to various modules, resulting in increased code complexity and reduced reusability.

And AOP is to extract the general logic in each module. We regard these logics as Aspects, and then dynamically insert the code into the specified methods and positions of the class.

Aspect is a very important concept in AOP. Aspects A new modular mechanism for describing crosscutting concerns scattered across objects, classes, or functions.

The following two figures illustrate the difference in implementation using OOP and AOP ideas respectively:

image.png image.png

Several important concepts in AOP

Aspect A modularity of concerns whose implementation may cross-cut multiple objects. Such as log aspect, permission aspect, etc.

JPoints Joint Points, or JPoints for short, represent some execution points (pointcuts) when the program is running. In a program, the construction of a class, the execution of a method, the setting of a variable, and the capture of an exception can all be regarded as an execution point.

Pointcuts pointcuts A program can have many Jpoints, and a Jpoints is also divided into call (call) and execution (execution). But not all Jpoints need to be concerned. Pointcuts define how to choose the desired pointcut among the many Jpoints.

Target Object The object that contains JPoints, also known as the notified or proxied object. Only the core business logic code is left in these objects, and all the common functions and other codes are waiting for the entry of the AOP container.

Advice 执行时机 Advice简单来说就是hook点,常见的有before、after、around三种类型。

Aspect 切面 Pointcut和Advice的组合可以看做切面,它是一个关注点的模块化,这个关注点可能会横切多个对象。

Weaving 织入 把代码织入到目标对象的过程。

AOP的主要应用场合

在开发中,我们通常会把核心功能划分为一个个模块开发,再把各个核心模块中的通用逻辑抽离出来,使用AOP的思想动态织入业务逻辑中。 所以,根据AOP的特性,AOP更适合与核心业务相关的通用逻辑,如:

  • 权限检查
  • 日志记录
  • 性能监控
  • 埋点操作
  • 异常处理
  • 参数校验

Android下实现AOP的几种工具

无论是OOP还是AOP,它们都是方法论。 在Android中,AOP可以通过预编译,或者在运行期动态代理的实现。

动态代理

实现AOP最基础的方案,可能就是Java语言的反射机制与动态代理机制了。

业务逻辑组件在运行过程中,AOP容器会动态创建一个代理对象供使用者调用,该代理对象已经按程序员的意图将切面成功切入到目标方法的连接点上,从而使切面的功能与业务逻辑的功能都得以执行。 从原理上讲,调用者直接调用的其实是AOP容器动态生成的代理对象,再由代理对象调用目标对象完成原始的业务逻辑处理,而代理对象则已经将切面与业务逻辑方法进行了合成。

动态代理又可细分为JDK动态代理和CGLib动态代理。 JDK动态代理利用接口实现。被代理的对象必须实现业务接口,代理对象必须实现InvocationHanlder接口。代理对象在调用具体方法前将其拦截。 而CGLIB动态代理则利用继承实现。通过ASM(一个开源的字节码修改工具),将代理对象类的class文件加载进来,通过修改其字节码生成子类来处理。

一个简单的JDK动态代理的代码示例如下:

/**
 * 业务接口
 */
public interface Subject {
    void call();
}
/**
 * 业务接口的实现(被代理的类)
 */
public class RealSubjcet implements Subject {
    @Override
    public void call() {
        Log.d("denny", "RealSubject#call");
    }
}
/**
 * 代理类
 */
public class ProxyHandler implements InvocationHandler {

    private final Object realSubject;

    public ProxyHandler(Object realSubject) {
        this.realSubject = realSubject;
    }

    @Override
    public Object invoke(Object o, Method method, Object[] args) throws Throwable {
        Log.d("denny", "before ProxyHandler#invoke");
        Object result = method.invoke(realSubject, args);
        Log.d("denny", "after ProxyHandler#invoke");
        return result;
    }
}
// 通过Proxy创建代理类对象
RealSubjcet realSubjcet = new RealSubjcet();
Subject proxySubject = (Subject) Proxy.newProxyInstance(
        Subject.class.getClassLoader(),
        new Class[]{Subject.class},
        new ProxyHandler(realSubjcet));
proxySubject.call();

以及,最终的结果: D/denny: before ProxyHandler#invoke D/denny: RealSubject#call D/denny: after ProxyHandler#invoke

APT

在编译的时候,利用APT生成.java文件。例如Dagger2、ButterKnife、EventBus3等。 APT(Annotation Process Tool)是一种注解处理工具,它对源代码文件进行检测并找出其中的Annotation,使用Annotation进行额外的处理。 使用APT主要有以下几个缺点:

  1. 相关API晦涩难懂,需要一定的编译基础。
  2. APT无法扫描其他module。
  3. 很难把相关代码插入一个带有返回值的方法之后(也就是在return之后)。

我们也可以通过继承AbstractProcessor等方式,实现自己的APT。

AspectJ

在.java编译为.class(java字节码)的时候,进行代码注入。 AspectJ功能强大。语法较多,但是难度不大,掌握几个常用的就能应付大部分场合。

Javassist、ASM等字节码操作类库

这两个工具比较相似,都是对已经编译好的class文件进行操作。相比于AspectJ,这两个工具显得更加强大灵活,可以直接对Java的字节码进行修改,但是难度也较高。 Java的二进制被存储在严格格式定义的.class文件中,这些字节码文件拥有足够的元数据信息来表示类中的所有元素,包括类名称、方法、属性以及Java字节码指令。 ASM可以动态生成类或者增强既有类的功能。使用ASM,可以直接生成二进制.class文件,也可以在类被加载入Java虚拟机前动态改变既有类的行为。

下面这张图说明了各个工具的作用时机。

image.png

AspectJ

AspectJ简介

AspectJ是目前应用最为广泛的AOP实现方案。 使用AspectJ开发有两种打开方式:

  • 使用AspectJ的语言编写.aj文件进行开发。这种语言与Java十分相似,只是多了几个关键字而已
  • 使用AspectJ提供的注解,直接在Java语言上进行开发

但在Android开发中,由于Android Studio并不认识.aj文件,因此建议使用注解进行开发。后续的例子也都使用注解的方式。一般情况下,使用提供的注解和一些简单的语法就可以实现绝大部分功能上的需求了。

Android中集成AspectJ

AspectJ的引入很简单。 首先,我们在项目根目录的gradle中引入hujiang的gradle插件。 当然,如果你愿意,也可以自己写个gradle插件来实现。

Hujiang的插件是一个基于AspectJ并在此基础上扩展出来可应用于Android开发平台的AOP框架,可作用于java源码,class文件及jar包,同时支持kotlin的应用。感兴趣的小伙伴可以看看他们的github

classpath "com.hujiang.aspectjx:gradle-android-plugin-aspectjx:2.0.4"

之后,在app的gradle下添加AspectJ的依赖

apply plugin: 'android-aspectjx'
dependencies {
    compile 'org.aspectj:aspectjrt:1.8.10'
}

先举个栗子

让我们先看一个简单的例子。

@After("execution(protected void com.example.myapplication.MainActivity.onCreate(android.os.Bundle))")
public void logCreate(JoinPoint joinPoint) {
    Log.d("denny", "MainActivity#onCreate");
}

在这个例子中,

  • @After定义了Advice,After的含义是在切入点之后执行。
  • execution(XXX)定义了JPoints的类型,execution的含义是截获方法的执行,并在执行前/后插入切点。
  • protected void com.example.myapplication.MainActivity.onCreate(android.os.Bundle))定义了过滤条件。在这里,我们选择的切入点是call,因此它的过滤条件一定是某个(或某些)函数。
    • protect过滤了Jpoint的访问权限
    • void表示Jpoint无返回值
    • com.example.myapplication.MainActivity是函数的包名,紧跟其后的onCreate是函数名
    • android.os.Bundle同理,指明了函数只有一个参数,参数的类型是位于android.os包下的Bundle

以上这个注解连起来的意思就是,选择一个截获了onCreate方法调用的切入点,在该切入点后执行logCreate方法(打印一条log)。 其中,onCreate方法的过滤条件为:访问类型为protect的方法,该方法位于com.example.myapplication.MainActivity中,方法名为onCreate,参数为android.os.Bundle

AspectJ语法解析

JPoints 执行点

Jpoint是指程序运行中可切入的点。 在Aspect中支持的Jpoints如下:

Jpoints 说明
Method call 方法被调用
Method execution 方法执行
Constructor call 构造函数被调用
Constructor execution 构造函数执行
Field get 读取属性
Field set 写入属性
Pre-initialization 与构造函数有关,很少用到
Initialization 与构造函数有关,很少用到
Static initialization static 块初始化
Handler 异常处理
Advice execution 所有 Advice 执行

execution和call的含义是不同的。

execution截获的是方法真正执行的代码区,使用@Around可以控制原方法执行与否,可以选择执行或者替换; 而call截获的是方法的调用区,它无法控制原来方法的执行与否,只是在方法调用前后插入切点,因此比较适合做一些轻量的监控(方法调用耗时等)

Pointcuts 切入点
概览

Pointcuts定义了如何在众多的Jpoints中选择想要的切入点。

AspectJ中,对于Pointcuts有一套标准的语法。使用这套语法,可以实现许多强大的功能。在实际使用中,我们只需要掌握一些简单的应用就可以了。至于那些高级玩法,等到项目需要的时候再去查询文档也不迟。

PointCuts中最常选择的点与JPoint密切相关,下面的表格给出了二者之间的关系:

Join Point 说明 Pointcuts语法
Method call 方法被调用 call(MethodPattern)
Method execution 方法执行 execution(MethodPattern)
Constructor call 构造函数被调用 call(ConstructorPattern)
Constructor execution 构造函数执行 execution(ConstructorPattern)
Field get 读取属性 get(FieldPattern)
Field set 写入属性 set(FieldPattern)
Pre-initialization 与构造函数有关,很少用到 preinitialization(ConstructorPattern)
Initialization 与构造函数有关,很少用到 initialization(ConstructorPattern)
Static initialization static 块初始化 staticinitialization(TypePattern)
Handler 异常处理 handler(TypePattern)
Advice execution 所有 Advice 执行 adviceexcution()

除了上表中的Jpoint,AspectJ还提供其他一些选择方法,下表列出了一些常用的选择非Jpoint的方法:

Pointcuts synatx 说明
within(TypePattern) 符合 TypePattern 的代码中的 Join Point
withincode(MethodPattern) 在某些方法中的 Join Point
withincode(ConstructorPattern) 在某些构造函数中的 Join Point
cflow(Pointcut) Pointcut 选择出的切入点 P 的控制流中的所有 Join Point,包括 P 本身
cflowbelow(Pointcut) Pointcut 选择出的切入点 P 的控制流中的所有 Join Point,不包括 P 本身
this(Type or Id) Join Point 所属的 this 对象是否 instanceOf Type 或者 Id 的类型
target(Type or Id) Join Point 所在的对象(例如 call 或 execution 操作符应用的对象)是否 instanceOf Type 或者 Id 的类型
args(Type or Id, ...) 方法或构造函数参数的类型
if(BooleanExpression) 满足表达式的 Join Point,表达式只能使用静态属性、Pointcuts 或 Advice 暴露的参数、thisJoinPoint 对象

上面Pointcuts的语法中涉及到一些Pattern,这些Pattern的具体规则如下表所示,[]里的内容是可选的:

Pattern 规则
MethodPattern [!] [@Annotation] [public,protected,private] [static] [final] 返回值类型 [类名.]方法名(参数类型列表) [throws 异常类型]
ConstructorPattern [!] [@Annotation] [public,protected,private] [final] [类名.]new(参数类型列表) [throws 异常类型]
FieldPattern [!] [@Annotation] [public,protected,private] [static] [final] 属性类型 [类名.]属性名
TypePattern 其他 Pattern 涉及到的类型规则也是一样,可以使用 '!'、''、'..'、'+','!' 表示取反,'*' 匹配除 . 外的所有字符串,'*' 单独使用事表示匹配任意类型,'..' 匹配任意字符串,'..' 单独使用时表示匹配任意长度任意类型,'+' 匹配其自身及子类,还有一个 '...'表示不定个数

Pointcut和Pattern都可以使用!、&&、|| 来组合选取,其中的含义和Java一样,这里就不再赘述了。

举例

以MethodPattern为例: 一个MethodPattern的完整表达式为:@注解 访问权限 返回值的类型 包名.函数名(参数)

  • @注解和访问权限(public/private/protect,以及static/final)属于可选项。如果不设置它们,则默认都会选择。以访问权限为例,如果没有设置访问权限作为条件,那么public,private,protect及static、final的函数都会进行搜索。

  • 返回值类型就是普通的函数的返回值类型。如果不限定类型的话,就用*通配符表示

  • 包名.函数名用于查找匹配的函数。可以使用通配符,包括*和..以及+号。其中*号用于匹配除.号之外的任意字符,而..则表示任意子package,+号表示子类。比如:

    • java.*.Date:可以表示java.sql.Date,也可以表示java.util.Date
    • Test*:可以表示TestBase,也可以表示TestDervied
    • java..*:表示任意以java开头的包名,如java.a.Test
    • *..Date: 表示任意以Date结尾的包名,如a.b.Date
    • java..Date: 表示任意以java开头,以Date结尾的包名
    • java..*Model+:表示Java任意package中,名字以Model结尾的子类,比如TabelModel,TreeModel 等
  • 最后来看函数的参数。参数匹配比较简单,主要是参数类型,比如:

    • (int, char):表示参数只有两个,并且第一个参数类型是int,第二个参数类型是char
    • (String, ..):表示至少有一个参数。并且第一个参数类型是String,后面参数类型不限。在参数匹配中,..代表任意参数个数和类型
    • (Object ...):表示不定个数的参数,且类型都是Object,这里的...不是通配符,而是Java中代表不定参数的意思
更多

更详细的Pointcuts定义,可以查看官方Pointcus说明

Advice 执行时机

Advice简单来说就是hook点,常见的有before、after、around三种类型。

Advice 说明
@Before 在执行Join Point之前
@After 在执行Join Point之后,包括正常的return和throw异常
@AfterReturning Join Point为方法调用且正常return时
@AfterThrowing Join Point为方法调用且抛出异常时
@Around 替代 Join Point 的代码,如果要执行原来代码的话,要使用 ProceedingJoinPoint.proceed()

@After默认包含了@AfterReturning和@AfterThrowing两种类型。 理论上说,@Before和@After能够实现的,@Around也完全能够实现。@Around的目标是替换原Jpoint。 对于同一个但是在对一个但是在对一个Pointcut声明,@Before和@After可以同时使用,但是在声明@Around后再声明@Before或者@After,则会报错。 它们的区别如下:

  • @Before和@After没有返回值,而@Around的返回值与原Jpoint匹配。
  • @Around可以决定目标方法是否执行,甚至可以替换目标为另一方法。
AspectJ 切面

使用注解的方式开发AspectJ,需要在类的头部声明@AspectJ,如:

@Aspect
public class ActivityLifeCycle {
    // your methods
}

这个类就相当于一个关注面。我们可以再定义一个PermisssionCheckAspect进行权限检查,定义一个PerformancMonitorAspect进行性能检测……所有的关注点的相关代码都挪到一个类型进行控制。

@Pointcut声明

@Pointcuts由org.aspectj.lang.annotation.Pointcut注解修饰的方法声明,方法返回值只能是void。@Pointcutz修饰的方法只能由空的方法实现而且不能有throws语句,方法的参数和pointcut中的参数相对应。 比如,下面这种方式

@Before("execution(* *..MainActivity.onCreate(..))")
public void logCreate(JoinPoint joinPoint) {
    Log.d("denny", "MainActivity#onCreate");
}

也可以用@Pointcut写成:

@Pointcut("execution(* *..MainActivity.onCreate(..))")
public void onCreatePointcut() {

}

@Before("onCreatePointcut()")
public void logCreate(JoinPoint joinPoint) {
    Log.d("denny", "MainActivity#onCreate");
}
call与execution

举个例子,进一步说明call和execution的区别。 首先,定义Animal接口:

public interface Animal {
    void execute();
}

接着,定义Rabbit与Tiger实现Animal接口:

public class Rabbit implements Animal {
    @Override
    public void execute() {
        Log.d("denny", "Rabbit is running");
    }
}
public class Tiger implements Animal {
    @Override
    public void execute() {
        Log.d("denny", "Tiger is running");
    }
}

然后,在MainActivity的onCreate中:

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        execute();
    }

    private void execute() {
        new Rabbit().execute();
        new Tiger().execute();
    }
}

最后,我们来看使用call和execute的区别:

@Before("call(* *.execute(..))")
public void logExecute(JoinPoint joinPoint) {
    Log.d("denny", "aspectj: " + joinPoint.getSourceLocation());
}

D/denny: aspectj: MainActivity.java:15 D/denny: aspectj: MainActivity.java:19 D/denny: Rabbit is running D/denny: aspectj: MainActivity.java:20 D/denny: Tiger is running

@Before("execution(* *.execute(..))")
public void logExecute(JoinPoint joinPoint) {
    Log.d("denny", "aspectj: " + joinPoint.getSourceLocation());
}

打印的结果为: D/denny: aspectj: MainActivity.java:19 D/denny: aspectj: Rabbit.java:8 D/denny: Rabbit is running D/denny: aspectj: Tiger.java:8 D/denny: Tiger is running

结论:call拦截的是方法的调用,所有execute的调用都在MainActivity中。而execution拦截的是方法的执行,分别在MainActivity、Rabbit和Tiger中。

within

within用来选取符合条件的Pointcut。还是使用上面的例子来说明:

@Before("call(* *.execute(..)) && within(*..MainActivity)")
public void executeAOP(JoinPoint joinPoint) {
    Log.d("denny", "aspectj before: " + joinPoint.getSourceLocation());
}

D/denny: aspectj before: MainActivity.java:15 D/denny: aspectj before: MainActivity.java:19 D/denny: Rabbit is running D/denny: aspectj before: MainActivity.java:20 D/denny: Tiger is running 拦截的是MainActivity中的execute方法的调用。

@Before("execution(* *.execute(..)) && within(*..MainActivity)")
public void executeAOP(JoinPoint joinPoint) {
    Log.d("denny", "aspectj before: " + joinPoint.getSourceLocation());
}

D/denny: aspectj before: MainActivity.java:19 2019-09-04 20:39:03.373 17272-D/denny: Rabbit is running 2019-09-04 20:39:03.373 17272-D/denny: Tiger is running 拦截的是MainActivity中的execute方法的执行,MainActivity中只有一个execute方法,因此只拦截到了一处。

target与this

前面说过,AspetcJ是属于静态织入的,但其实AspectJ也有动态织入的部分,而target()与this()就是属于它动态织入的方式。所以target()与this()需要在在运行时才能确定那些被拦截。

先给结论:

  • target是指:我们pointcut所选取的Join point的所有者,直白点说就是: 指明拦截的方法属于那个类。
  • this是指: 我们pointcut所选取的Join point的调用/执行的所有者,就是说:方法是在那个类中被调用/执行的。

下面的例子可以帮助大家理解两者的微妙区别:

@Before("call(* *.execute(..))")
public void executeAOP(JoinPoint joinPoint) {
    Log.d("denny", "aspectj before: " + joinPoint.getSourceLocation()
        + " this: " + joinPoint.getThis().getClass().getSimpleName()
        + " target: " + joinPoint.getTarget().getClass().getSimpleName());
}

打印的结果为: D/denny: aspectj before: MainActivity.java:15 this: MainActivity target: MainActivity D/denny: aspectj before: MainActivity.java:19 this: MainActivity target: Rabbit D/denny: Rabbit is running D/denny: aspectj before: MainActivity.java:20 this: MainActivity target: Tiger D/denny: Tiger is running 可以看到,log中所有的this都是MainActivity,事实上,我们所选择的切入点(execute方法)确实都是在MainActivity中被调用的。而target是指所拦截的方法属于哪个类,因此打印出来的分别是MainActivity、Rabbit和Tiger。

@Before("execution(* *.execute(..))")
public void executeAOP(JoinPoint joinPoint) {
    Log.d("denny", "aspectj before: " + joinPoint.getSourceLocation()
        + " this: " + joinPoint.getThis().getClass().getSimpleName()
        + " target: " + joinPoint.getTarget().getClass().getSimpleName());
    }

打印的结果为: D/denny: aspectj before: MainActivity.java:19 this: MainActivity target: MainActivity D/denny: aspectj before: Rabbit.java:8 this: Rabbit target: Rabbit D/denny: Rabbit is running D/denny: aspectj before: Tiger.java:8 this: Tiger target: Tiger D/denny: Tiger is running 和上一个例子相比,这里使用execution拦截方法的执行。target没有发生变化,但是this发生了变化。对于Rabbit和Tiger来说,它们的execute()方法是在MainActivity中被调用,但是执行当然是在自己的类中啦。

target()和this()还存在继承关系作用,也就是说:如果你的signature是一个基类,那么这个pointcut同时也会对他的子类也起作用。这里就不举例了,有兴趣的小伙伴可以自行实验一下。

另外,target和this可以获取他们对应的实例,但within不行。看下面这个例子:

@Before("call(* *.execute(..)) && this(mainActivity) && target(tiger)")
public void executeAOP(JoinPoint joinPoint, MainActivity mainActivity, Tiger tiger) {
    Log.d("denny", "aspectj before: " + joinPoint.getSourceLocation()
        + " this: " + mainActivity.getClass().getSimpleName()
        + " target: " + tiger.getClass().getSimpleName());
}
if表达式

在基于AspectJ注解的开发方式中,if(...)表达式的用法与其他的选择操作符不同,在@Pointcut的语句中if表达式只能是if()、if(true)或if(false),而且@Pointcut方法必须为public static boolean,方法体内就是if表达式的内容,可以使用暴露的参数、静态属性、JoinPoint、JoinPoint.StaticPart、JoinPoint.EnclosingStaticPart。

JoinPoint.StaticParts仅包含join point的静态信息。

下面这个例子中,“ImeService#onStartInputView_before”只会在打印五次。

private static int COUNT = 0;

@Pointcut("execution(* *..ImeService.onStartInputViewInternal(..)) && if()")
public static boolean onStartInputView() {
    return COUNT++ < 5;
}

@Before("onStartInputView()")
public void onBeforeStartInputView(JoinPoint.StaticPart joinPoint) {
    Log.d("denny", "ImeService#onStartInputView_before");
}

代码混淆

通过上述方式切入的代码是可以混淆的。 代码是在编译阶段织入,所以混淆是不会有影响,只有在运行时你需要通过类名,方法名去做一些事情的时候才不能混淆,比如用到了反射技术等。任何在编译阶段植入代码的AOP方案混淆都不会受影响,和混淆无关。而在运行时的AOP方案就会受混淆影响。

反编译查看实现

我们可以通过反编译,看看AspectJ是如何在不修改原有代码的情况下,实现无缝插入的。 源码:

public class MainActivity extends AppCompatActivity {
    private void testA() {
        Log.d("denny", "initial method");
    }
}

切面代码:

@Aspect
public class AspectJ {
    @Around("execution(* *..testA(..))")
    public void log(ProceedingJoinPoint point) throws Throwable {
        point.proceed();
        Log.d("denny", "aspectj test");
        Log.d("denny", Log.getStackTraceString(new Throwable()));
    }
}

反编译后,MainActivity的代码如下:

public class MainActivity extends AppCompatActivity {
    private static final /* synthetic */ StaticPart ajc$tjp_0 = null;

    static {
        ajc$preClinit();
    }

    private static /* synthetic */ void ajc$preClinit() {
        Factory factory = new Factory("MainActivity.java", MainActivity.class);
        ajc$tjp_0 = factory.makeSJP(JoinPoint.METHOD_EXECUTION, (Signature) factory.makeMethodSig("2", "testA", "com.example.dany.aspectjapplication.MainActivity", "", "", "", "void"), 61);
    }

    private void testA() {
        JoinPoint makeJP = Factory.makeJP(ajc$tjp_0, this, this);
        testA_aroundBody1$advice(this, makeJP, AspectJ.aspectOf(), (ProceedingJoinPoint) makeJP);
    }

    private static final /* synthetic */ void testA_aroundBody1$advice(MainActivity ajc$this, JoinPoint thisJoinPoint, AspectJ ajc$aspectInstance, ProceedingJoinPoint point) {
        Log.d("denny", "initial method");
        Log.d("denny", "aspectj test");
        Log.d("denny", Log.getStackTraceString(new Throwable()));
    }
}

我们主要关心testA方法即可。不难发现,我们的代码被AspectJ重构了。在不修改源代的前提下,对java字节码进行操作,实现无缝插入。

Best Practice

帮助定位问题

如果我们需要定位一个问题,但是又不便于调试,那么AspectJ或许可以帮助我们。 举一个在实际项目中遇到的例子。 用户反馈了一个问题,通过查看代码推测,可能是网络状态的判断出现了异常。但是,在调试的状态下(更准确地说,是在充电的情况下),网络状态的判断是正常的。 因此,接下来的思路就是,通过log的形式记录当前的网络状态,写入文件帮助确定问题。 对网络状态的判断的相关代码在另一个组件中,无法直接在其中添加代码。此时,我想到了AspectJ:

@Aspect
public class AspectJTest {
    @After("execution(* *..A.B(..))")
    public void logXg(ProceedingJoinPoint point) throws Throwable {
        Log.d("denny", "current state: " + state);
    }
}

类似的例子还有很多,对于一些无法或者不方便修改源码的第三方库,我们都可以借助AspectJ切入。例如,在Bitmap的createBitmap方法之后记录相关信息、在OnClickListener#onClick中记录View的状态等等。

利用AspectJ进行数据统计/埋点

AOP最直观的一个实践,就是在数据统计中了。 下面给出一个统计方法执行时间的例子。 首先,我们定义注解:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.CLASS)
public @interface PerformanceTrace {
}

然后,定义AspectJ:

@Aspect
public class AspectJTest {
    @Around("execution(@*..PerformanceTrace * *(..))")
    public Object logPerformance(ProceedingJoinPoint point) throws Throwable {
        final long start = System.currentTimeMillis();
        final Object result = point.proceed();
        final long end = System.currentTimeMillis();
        Log.d("denny", "time cost: " + (end - start) + "ms"
                + " | method signature: " + point.getSignature());
        return result;
    }
}

最后,只需要在需要进行性能测试的方法前,加上@PerformanceTrace注解就可以了。

jake Wharton大神已经为我们提供了开源工具实现类似的功能:Hugo。其内部实现,正是借用了AspectJ。

检查权限

Similar to the example above, we first define the annotation

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface PermissionAnnotation {
    String[] declaredPermission() default "";
}

After that, define the pointcut:

@Aspect
public class AspectJTest {
    @Around("execution(@*..PermissionAnnotation * *(..)) && @annotation(annotation)")
    public void permissionCheck(ProceedingJoinPoint point, PermissionAnnotation annotation) throws Throwable {
        final String[] declaredPermission = annotation.declaredPermission();
        final Context context = (Context) point.getThis();  // could lead to a null pointer
        AndPermission.with(context)
                .permission(declaredPermission)
                .onGranted(action -> {
                    try {
                        point.proceed();
                    } catch (Throwable throwable) {
                        throwable.printStackTrace();
                    }
                })
                .onDenied(action -> Toast.makeText(context, "Permission Deny", Toast.LENGTH_SHORT).show())
                .start();
    }
}

Finally, add a comment to the place where the permission check is required:

@PermissionAnnotation(declaredPermission = {Manifest.permission.CAMERA, Manifest.permission.READ_CONTACTS})

more examples

For more examples, you can refer to this Demo of Hujiang

Problems with AspectJ

Can't weave problems

For example, if we want to count the time-consuming of all Activity's onPause methods, we can define PointCut like this:

@Around("execution(* android.app.Activity+.onPause(..))")
public void logPerformance(ProceedingJoinPoint point) throws Throwable {
    // do something you want
}

However, doing so causes two problems:

  1. If our Activity does not override the onPause method, it will not be woven. This is because android.app.Activity is located in the android device and does not participate in the packaging process. If it is an Activity or Fragment in the support package, it will not be affected.
  2. If our Activity inherits BaseActivity, and BaseActivity inherits android.app.Activity, then these two activities will be woven, which causes the problem of repeated statistics.

Difficult to troubleshoot

AspectJ implements AOP by modifying the bytecode, and has no perception of the upper layer, so it is difficult to locate the cause of the problem when there is a problem. If AOP is used in a project, documentation should be left as much as possible.

Compile time becomes longer

In Gradle's Transform process, all class files are traversed, looking for entry points that meet the requirements, and then inserting bytecodes. If the project is large and has a lot of woven code, it will increase the compile time.

Solutions include: Use exclude to filter out package names that do not require weaving. If the weaving code does not need to be weaved in the debug environment, such as buried points, you can actively disable the AspectJ function.

Guess you like

Origin juejin.im/post/7118644512351584287