分享关于AOP在Android中应用的一些思考

AOP的基本概念

什么是AOP

AOP(Aspect Oriented Programming),面向切面编程,是OOP(面向对象编程)的延续。

在OOP思想中,我们会把问题划分为各个模块,如语音、表情等。在划分这些模块的过程中,也会出现一些共同特征(如埋点)。它的逻辑被分散到了各个模块,导致了代码代码复杂度提高,可复用性降低。

而AOP,就是将各个模块中的通用逻辑抽离出来。我们将这些逻辑视作Aspect(切面),然后动态地把代码插入到类的指定方法、指定位置中。

Aspect(切面)是AOP 中的一个很重要的概念。切面一种新的模块化机制,用来描述分散在对象、类或函数中的横切关点(crosscutting concern)。

下面两张图说明了使用分别OOP和AOP思想实现上的不同:

AOP中的几个重要概念

Aspect 切面 一个关注点的模块化,这个关注点实现可能横切多个对象。如日志切面、权限切面等。

JPoints 执行点 Joint Points,简称JPoints,表示的是程序运行时的一些执行点(可切入点)。 在一个程序中,一个类的构造,一个方法的执行,一个变量的设置,一个异常的捕获,都可以看成是一个执行点。

Pointcuts 切入点 一个程序可以有很多的Jpoints,一个Jpoints还分为call(调用)和execution(执行)。但不是所有的Jpoints都需要关心。 Pointcuts定义了如何在众多的Jpoints中选择想要的切入点。

Target Object 目标对象 包含JPoints的对象,也被称作被通知或被代理对象。这些对象中已经只剩下干干净净的核心业务逻辑代码了,所有的共有功能等代码则是等待AOP容器的切入。

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虚拟机前动态改变既有类的行为。

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

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。

检查权限

与上面的例子相似,我们首先定义注解

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

之后,定义切点:

@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();
    }
}

最后,在需要进行权限检查的地方加上注解即可:

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

AspectJ存在的问题

无法织入的问题

举个例子,如果我们想要统计所有的Activity的onPause方法的耗时,我们可以这样定义PointCut:

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

但是,这样做会导致两个问题:

  1. 如果我们的Activity没有复写onPause方法,那么将不会织入。这是因为android.app.Activity位于android设备内,不参与打包的过程。如果是support包中的Activity或者Fragment,就不会受到影响。
  2. 如果我们的Activity继承了BaseActivity,BaseActivity又继承了android.app.Activity,那么这两个Activity都会被织入,这就造成了重复统计的问题。

出现问题较难排查

AspectJ是通过修改字节码的方式实现AOP,对上层无感知,因此出现问题的时候较难定位问题原因。在项目中如果使用了AOP,应当尽可能留下文档。

编译时间变长

在Gradle的Transform过程,会遍历所有class文件,查找符合需求的切入点,然后插入字节码。如果项目较大且织入代码较多,会增加编译时间。

解决办法包括: 使用exclude过滤掉不需要执行织入的包名。 如果织入代码在debug环境不需要织入,比如埋点,可以主动关闭AspectJ功能。

作者:dennyz
链接:https://juejin.cn/post/7118644512351584287

猜你喜欢

转载自blog.csdn.net/m0_64420071/article/details/125709570