面向对象编程(Object Oriented Programming,OOP,面向对象程序设计)是都是计算机编程架构。OOP在开发中,更多的是用抽象思维将一切事物都抽象为对象,学生类,订单类甚至是图形类,所谓的面向切面编程其实是对业务逻辑又进行了进一步的抽取,将多种业务逻辑中的公用部分抽取出来做成一种服务(比如日志记录,性能统计,安全验证等),从而实现代码复用。
而这些抽象的基础就是这些属性或者操作是固定的,如学生的事务中的登录,查询成绩等等,可是这些真的是固定的么?

我们将一切对象都可以定义成类,相同的可以聚合成一个父类,不同的逻辑可以交给多态或者继承。
而有些逻辑这很难用这OOP去解决,如打点问题,假设我们项目分为多个功能模块或者业务模块,而这些这些模块依赖基础模块。
假如哪一天PM要求我们给各个业务模块打点(如点击事件打点),那么我们的思路肯定在每个模块下一个个打点,针对不同的模块,不同的view进行打点,主要是这些打点逻辑还是重复,和业务实际上关联性很弱,也就是说没它程序也没跑起来,只是在相同或相似时机(如打点模块的点击)去触发打点逻辑。如图所示:

这样机械性的工作,是不是可以考虑AOP呢?

那么什么是AOP呢?

AOP

AOP是Aspect Oriented Programming的缩写,中译文为面向切向编程,他是对OOP的补充。
如上面说的打点的例子,就可以通过AOP很好实现,每个功能模块或者业务模块不需要改动,只需要将打点的逻辑上做一个切面,通过织入需要的代码即可,若你不理解可以看下面的例子。

对函数做切面

我们写个简单的demo,有个类,里面只有onClick和onCreate方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class MainActivity extends AppCompatActivity implements View.OnClickListener {

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

textView = findViewById(R.id.tvjj);

textView.setOnClickListener(this);
}

@Override
public void onClick(View v) {
Log.d("Aop", "onClick");
}
}

我们给这个类加切面:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Aspect
public class AspectTest {
final String TAG = "AspectTest";

@Before("execution(* *..MainActivity+.on**(..))")
public void method(JoinPoint joinPoint) throws Throwable {
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
String className = joinPoint.getThis().getClass().getSimpleName();

Log.e(TAG, "class:" + className);
Log.e(TAG, "method:" + methodSignature.getName());
}
}

运行后的日志:

1
2
3
4
5
6
7
2019-06-23 18:56:21.661 3091-3091/cc.cyning.aspectjx E/AspectTest: -----
2019-06-23 18:56:21.661 3091-3091/cc.cyning.aspectjx E/AspectTest: class:MainActivity
2019-06-23 18:56:21.661 3091-3091/cc.cyning.aspectjx E/AspectTest: method:onCreate
2019-06-23 18:56:24.819 3091-3091/cc.cyning.aspectjx E/AspectTest: -----
2019-06-23 18:56:24.819 3091-3091/cc.cyning.aspectjx E/AspectTest: class:MainActivity
2019-06-23 18:56:24.819 3091-3091/cc.cyning.aspectjx E/AspectTest: method:onClick
2019-06-23 18:56:24.819 3091-3091/cc.cyning.aspectjx E/AspectTest: args 1: AppCompatButton

MainActivity里我们什么也没做,只是在AspectTest里写了加了一个切面,在执行MainActivity里面的on**(里面只有onCreateonClick)前加入了打印当前类和方法的,就可以在onCreateonClick执行时打印出相关信息,是不是很神奇。

那么怎么怎么做切面,切面逻辑怎么写?下面就是我们的主角AspectJ

AspectJ

AspectJ是一个代码织入技术(code injection),提供了一套全新的语法实现,完全兼容Java(其实跟Java之间的区别,只是多了一些关键词而已)。同时,还提供了纯Java语言的实现,通过注解的方式,完成代码编织的功能。因此我们在使用AspectJ的时候有以下两种方式:

  1. 使用AspectJ的语言进行开发
  2. 通过AspectJ提供的注解在Java语言上开发

在了解AspectJ的具体使用之前,先了解一下其中的一些基本的术语概念

常用术语

下面的一些常用术语是我们开发中必须知道的,我们自己做切面时这些术语

Join Points

程序中可能作为代码注入目标的特定的点。join point 可以包含其它 join point。例如,一个方法调用可能在它返回之前引起其它方法调用。那么,Pointcut 就是一种语言构造,这种构造根据已定义的标准挑选一组 join point。

其常用的如下:

JoinPoints 说明 示例
method call 函数调用 比如调用Log.e(),这是一处Joint point
method execution 函数执行 如实例中的onCreate的执行时,其内部代码
constructor call 构造函数调 与方法的调用类型
constructor executor 构造函数执行
与方法的执行执行
field get 获取某个变量
field set 设置某个变量
static initialization 类初始化
initialization object在构造函数中做的一些工作
handler 异常处理 对应try-catch()中,对应的catch块内的执行

Pointcuts

代码注入的位置,这个注意是个点或者多个调用点。

Advice

其实就是注入到class文件中的代码片,其他告知注入的位置,如是在方法前(Before),还是在方法后(After),或者是在体制替换整个方法(Around)

切点语法表达式

可以拿刚才的例子来学习;

1
2
3
@Before("execution(* *..MainActivity+.on**(..))")
public void method(JoinPoint joinPoint) throws Throwable {
}

可以知道是

  1. 切点语法表达式 = Advice(Before属于Advice)+ Pointcuts
    1
    2
    3
    4
    @Around("onClickPointcuts() || onClickInXmlPointcuts() || onClickInButterKnifePointcuts()")
    public void throttleClick(ProceedingJoinPoint joinPoint) throws Throwable {

    }

onClickPointcuts就是一个Pointcut

  1. Pointcuts = 可能是多个Pointcut
    Pointcut = Join Point的关键字(call,execution) + 函数,变量或者其他切入点
1
2
3
4
5
6
7
8
9
@Pointcut("execution(* android.view.View.OnClickListener.onClick(..))")
public void onClickPointcuts() {
}



@Pointcut( "execution(* android.support.v7.app.AppCompatViewInflater.DeclaredOnClickListener.onClick(..))")
public void onClickInXmlPointcuts() {
}

那么这些*究竟代表啥呢?
若是之间接触过正则表达式,可能就很容易了。
|表达式 |含义|
|—|—|
|java.lang.String |匹配String类型|
|java..String | 匹配java包下的任何“一级子包”下的String类型,如匹配java.lang.String,但不匹配java.lang.ss.String|
|java..
| 匹配java包及任何子包下的任何类型,如匹配java.lang.String、java.lang.annotation.Annotation
|java.lang.*ing| 匹配任何java.lang包下的以ing结尾的类型|
|java.lang.Number+ |匹配java.lang包下的任何Number的自类型,如匹配java.lang.Integer,也匹配java.math.BigInteger|

常用的AspectJ 切点语法表达式

call和execution

call表示的外接的调用,而execution则是函数内部。

MainActivity.class

测试execution

1
2
3
4
5
6
7
·@Before("execution(* cc.cyning.aspectjx.MainActivity.onTest(..))")
public void method(JoinPoint joinPoint) throws Throwable {
// 代码的行号
Log.e(TAG , joinPoint.getSourceLocation().getLine() + "");


}

返回的结果:

1
2
3
4
8828-8828/cc.cyning.aspectjx D/Aop: onClick before
8828-8828/cc.cyning.aspectjx E/AspectTest: 42
8828-8828/cc.cyning.aspectjx D/Aop: onTest
8828-8828/cc.cyning.aspectjx D/Aop: onClick after

测试call

1
2
3
4
5
6
7
8
9
@Before("call(* cc.cyning.aspectjx.MainActivity.onTest(..))")
public void method(JoinPoint joinPoint) throws Throwable {
String key = joinPoint.getSignature().toString();
// 代码的行号
Log.e(TAG , joinPoint.getSourceLocation().getLine() + "");

Log.e(TAG, "method:" + key);

}

运行结果:

1
2
3
4
9003-9003/cc.cyning.aspectjx D/Aop: onClick before
09003-9003/cc.cyning.aspectjx E/AspectTest: 35
9003-9003/cc.cyning.aspectjx D/Aop: onTest
9003-9003/cc.cyning.aspectjx D/Aop: onClick after

可以知道call上是运行的35行,而execution是运行在onTest的内部的.

对于更多的ASpectJ的内容,可以查看我文章末尾的参考文章。

还是回归到打点的问题。

view的点击打点

具体思路是想可以参考网易HubbleData之Android无埋点实践

那么我们可以先使用ASpectJ来处理点击view的打点。

对于view我们最简单的是在View.OnClickListener.onClick切面上加入打点逻辑,当时现在很多点击时间还真的不再这样实现。

问题:

  1. 在xml的中设置点击事件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <Button
    android:id="@+id/button"
    android:onClick="onClick"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Hello World!"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toRightOf="parent"
    app:layout_constraintTop_toTopOf="parent"/>
  2. ButterKnife注释实现点击事件

    1
    2
    @OnClick(R.id.button)
    public onClickView(View view)
  3. lambda的表达式实现点击事件

1
button.addClickListener(clickEvent -> doSth());

xml中点击事件

对于这个问题,最简单的就是看view的代码,不多说,看下源码:

1
2
3
4
5
6
final TypedArray a = context.obtainStyledAttributes(attrs, sOnClickAttrs);
final String handlerName = a.getString(0);
if (handlerName != null) {
view.setOnClickListener(new DeclaredOnClickListener(view, handlerName));
}
a.recycle();

可以在DeclaredOnClickListener做动作吧。

ButterKnife

不多说,可以看代码解决。
可以去拦截butterknife.OnClick下面的监听事件

参考

Android AOP学习之:AspectJ实践

看AspectJ在Android中的强势插入

AOP:利用Aspectj注入代码,无侵入实现各种功能,比如一个注解请求权限

网易HubbleData之Android无埋点实践