6

看AspectJ在Android中的强势插入

 3 years ago
source link: https://blog.csdn.net/eclipsexys/article/details/54425414
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

什么是AOP

AOP是Aspect Oriented Programming的缩写,即『面向切面编程』。它和我们平时接触到的OOP都是编程的不同思想,OOP,即『面向对象编程』,它提倡的是将功能模块化,对象化,而AOP的思想,则不太一样,它提倡的是针对同一类问题的统一处理,当然,我们在实际编程过程中,不可能单纯的安装AOP或者OOP的思想来编程,很多时候,可能会混合多种编程思想,大家也不必要纠结该使用哪种思想,取百家之长,才是正道。

那么AOP这种编程思想有什么用呢,一般来说,主要用于不想侵入原有代码的场景中,例如SDK需要无侵入的在宿主中插入一些代码,做日志埋点、性能监控、动态权限控制、甚至是代码调试等等。

AspectJ

AspectJ实际上是对AOP编程思想的一个实践,当然,除了AspectJ以外,还有很多其它的AOP实现,例如ASMDex,但目前最好、最方便的,依然是AspectJ。

在Android项目中使用AspectJ

AOP的用处非常广,从Spring到Android,各个地方都有使用,特别是在后端,Spring中已经使用的非常方便了,而且功能非常强大,但是在Android中,AspectJ的实现是略阉割的版本,并不是所有功能都支持,但对于一般的客户端开发来说,已经完全足够用了。

在Android上集成AspectJ实际上是比较复杂的,不是一句话就能compile,但是,鄙司已经给大家把这个问题解决了,大家现在直接使用这个SDK就可以很方便的在Android Studio中使用AspectJ了。Github地址如下:

https://github.com/HujiangTechnology/gradle_plugin_android_aspectjx

另外一个比较成功的使用AOP的库是Jake大神的Hugo:

https://github.com/JakeWharton/hugo

首先,需要在项目根目录的build.gradle中增加依赖:

classpath 'com.hujiang.aspectjx:gradle-android-plugin-aspectjx:1.0.8'

完整代码如下:

buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:2.3.0-beta2'
        classpath 'com.hujiang.aspectjx:gradle-android-plugin-aspectjx:1.0.8'

        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }
}

然后再主项目或者库的build.gradle中增加AspectJ的依赖:

compile 'org.aspectj:aspectjrt:1.8.9'

同时在build.gradle中加入AspectJX模块:

apply plugin: 'android-aspectjx'

这样就把整个Android Studio中的AspectJ的环境配置完毕了,如果在编译的时候,遇到一些『can’t determine superclass of missing type xxxxx』这样的错误,请参考项目README中关于excludeJarFilter的使用。

aspectjx {
    //includes the libs that you want to weave
    includeJarFilter 'universal-image-loader', 'AspectJX-Demo/library'

    //excludes the libs that you don't want to weave
    excludeJarFilter 'universal-image-loader'
}

AspectJ入门

我们通过一段简单的代码来了解下基本的使用方法和功能,新建一个AspectTest类文件,代码如下:

@Aspect
public class AspectTest {

    private static final String TAG = "xuyisheng";

    @Before("execution(* android.app.Activity.on**(..))")
    public void onActivityMethodBefore(JoinPoint joinPoint) throws Throwable {
        String key = joinPoint.getSignature().toString();
        Log.d(TAG, "onActivityMethodBefore: " + key);
    }
}

在类的最开始,我们使用@Aspect注解来定义这样一个AspectJ文件,编译器在编译的时候,就会自动去解析,并不需要主动去调用AspectJ类里面的代码。

我的原始代码很简单:

public class MainActivity extends AppCompatActivity {

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

通过这种方式编译后,我们来看下生成的代码是怎样的。AspectJ的原理实际上是在编译的时候,根据一定的规则解析,然后插入一些代码,通过aspectjx生成的代码,会在Build目录下:

通过反编译工具查看下生成内容:

我们可以发现,在onCreate的最前面,插入了一行AspectJ的代码。这个就是AspectJ的主要功能,抛开AOP的思想来说,我们想做的,实际上就是『在不侵入原有代码的基础上,增加新的代码』。

AspectJ之Join Points

Join Points,简称JPoints,是AspectJ的核心思想之一,它就像一把刀,把程序的整个执行过程切成了一段段不同的部分。例如,构造方法调用、调用方法、方法执行、异常等等,这些都是Join Points,实际上,也就是你想把新的代码插在程序的哪个地方,是插在构造方法中,还是插在某个方法调用前,或者是插在某个方法中,这个地方就是Join Points,当然,不是所有地方都能给你插的,只有能插的地方,才叫Join Points。

AspectJ之Pointcuts

Join Points和Pointcuts的区别实际上很难说,我也不敢说我理解的一定对,但这些都是概念上的内容,并不影响我们去使用。

Pointcuts,在我理解,实际上就是在Join Points中通过一定条件选择出我们所需要的Join Points,所以说,Pointcuts,也就是带条件的Join Points,作为我们需要的代码切入点。

AspectJ之Advice

又来一个Advice,Advice其实是最好理解的,也就是我们具体插入的代码,以及如何插入这些代码。我们最开始举的那个例子,里面就是使用的最简单的Advice——Before。类似的还有After、Around,我们后面来讲讲他们的区别。

AspectJ之切点语法

我们以前面的Demo来看下最简单的AspectJ语法:

@Before("execution(* android.app.Activity.on**(..))")
public void onActivityMethodBefore(JoinPoint joinPoint) throws Throwable {
}

这里会分成几个部分,我们依次来看:

  • @Before:Advice,也就是具体的插入点
  • execution:处理Join Point的类型,例如call、execution
  • (* android.app.Activity.on**(..)):这个是最重要的表达式,第一个『*』表示返回值,『*』表示返回值为任意类型,后面这个就是典型的包名路径,其中可以包含『*』来进行通配,几个『*』没区别。同时,这里可以通过『&&、||、!』来进行条件组合。()代表这个方法的参数,你可以指定类型,例如android.os.Bundle,或者(..)这样来代表任意类型、任意个数的参数。
  • public void onActivityMethodBefore:实际切入的代码。

这里还有一些匹配规则,可以作为示例来进行讲解:

表达式含义java.lang.String匹配String类型java.*.String匹配java包下的任何“一级子包”下的String类型,如匹配java.lang.String,但不匹配java.lang.ss.Stringjava..*匹配java包及任何子包下的任何类型,如匹配java.lang.String、java.lang.annotation.Annotationjava.lang.*ing匹配任何java.lang包下的以ing结尾的类型java.lang.Number+匹配java.lang包下的任何Number的自类型,如匹配java.lang.Integer,也匹配java.math.BigInteger
参数含义()表示方法没有任何参数(..)表示匹配接受任意个参数的方法(..,java.lang.String)表示匹配接受java.lang.String类型的参数结束,且其前边可以接受有任意个参数的方法(java.lang.String,..)表示匹配接受java.lang.String类型的参数开始,且其后边可以接受任意个参数的方法(*,java.lang.String)表示匹配接受java.lang.String类型的参数结束,且其前边接受有一个任意类型参数的方法

AspectJ实例

Before、After

这两个Advice应该是使用的最多的,所以,我们先来看下这两个Advice的实例,首先看下Before和After。

@Before("execution(* com.xys.aspectjxdemo.MainActivity.on*(android.os.Bundle))")
public void onActivityMethodBefore(JoinPoint joinPoint) throws Throwable {
    String key = joinPoint.getSignature().toString();
    Log.d(TAG, "onActivityMethodBefore: " + key);
}

@After("execution(* com.xys.aspectjxdemo.MainActivity.on*(android.os.Bundle))")
public void onActivityMethodAfter(JoinPoint joinPoint) throws Throwable {
    String key = joinPoint.getSignature().toString();
    Log.d(TAG, "onActivityMethodAfter: " + key);
}

经过上面的语法解释,现在看这个应该很好理解了,我们来看下编译后的类:

我们可以看见,在原始代码的基础上,增加了Before和After的代码,Log也能被正确的插入并打印出来。

Around

Before和After其实还是很好理解的,也就是在Pointcuts之前和之后,插入代码,那么Around呢,从字面含义上来讲,也就是在方法前后各插入代码,是的,他包含了Before和After的全部功能,代码如下:

@Around("execution(* com.xys.aspectjxdemo.MainActivity.testAOP())")
public void onActivityMethodAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
    String key = proceedingJoinPoint.getSignature().toString();
    Log.d(TAG, "onActivityMethodAroundFirst: " + key);
    proceedingJoinPoint.proceed();
    Log.d(TAG, "onActivityMethodAroundSecond: " + key);
}

其中,proceedingJoinPoint.proceed()代表执行原始的方法,在这之前、之后,都可以进行各种逻辑处理。

原始代码:

public class MainActivity extends AppCompatActivity {

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

    public void testAOP() {
        Log.d("xuyisheng", "testAOP");
    }
}

我们先来看下编译后的代码:

我们可以发现,Around确实实现了Before和After的功能,但是要注意的是,Around和After是不能同时作用在同一个方法上的,会产生重复切入的问题。

自定义Pointcuts

自定义Pointcuts可以让我们更加精确的切入一个或多个指定的切入点。

首先,我们需要自定义一个注解类,例如——DebugTool.java:

/**
 * 自定义AOP注解
 * <p>
 * Created by xuyisheng on 17/1/12.
 */

@Retention(RetentionPolicy.CLASS)
@Target({ElementType.CONSTRUCTOR, ElementType.METHOD})
public @interface DebugTool {
}

然后在需要插入代码的地方使用这个注解:

public class MainActivity extends AppCompatActivity {

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

    @DebugTool
    public void testAOP() {
        Log.d("xuyisheng", "testAOP");
    }
}

最后,我们来创建自己的切入文件。

@Pointcut("execution(@com.xys.aspectjxdemo.DebugTool * *(..))")
public void DebugToolMethod() {
}

@Before("DebugToolMethod()")
public void onDebugToolMethodBefore(JoinPoint joinPoint) throws Throwable {
    String key = joinPoint.getSignature().toString();
    Log.d(TAG, "onDebugToolMethodBefore: " + key);
}

先定义Pointcut,并申明要监控的方法名,最后,在Before或者其它Advice里面添加切入代码,即可完成切入。

编译好的代码如下:

通过这种方式,我们可以非常方便的监控指定的Pointcut,从而增加监控的粒度。

call和execution

在AspectJ的切入点表达式中,我们前面都是使用的execution,实际上,还有一种类型——call,那么这两种语法有什么区别呢,我们来试验下就知道了。

被切代码依然很简单:

public class MainActivity extends AppCompatActivity {

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

    public void testAOP() {
        Log.d("xuyisheng", "testAOP");
    }
}

先来看execution,代码如下:

@Before("execution(* com.xys.aspectjxdemo.MainActivity.testAOP(..))")
public void methodAOPTest(JoinPoint joinPoint) throws Throwable {
    String key = joinPoint.getSignature().toString();
    Log.d(TAG, "methodAOPTest: " + key);
}

编译之后的代码如下所示:

再来看下call,代码如下:

@Before("call(* com.xys.aspectjxdemo.MainActivity.testAOP(..))")
public void methodAOPTest(JoinPoint joinPoint) throws Throwable {
    String key = joinPoint.getSignature().toString();
    Log.d(TAG, "methodAOPTest: " + key);
}

编译之后的代码如下所示:

其实对照起来看就一目了然了,execution是在被切入的方法中,call是在调用被切入的方法前或者后。

对于Call来说:

Call(Before)
Pointcut{
    Pointcut Method
}
Call(After)

对于Execution来说:

Pointcut{
  execution(Before)
    Pointcut Method
  execution(After)
}

切入点过滤与withincode

除了前面提到的call和execution,比较常用的还有一个withincode。这个语法通常来进行一些切入点条件的过滤,作更加精确的切入控制。我们可以参考下面这个例子:

public class MainActivity extends AppCompatActivity {

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

    public void testAOP() {
        Log.d("xuyisheng", "testAOP");
    }

    public void testAOP1() {
        testAOP();
    }

    public void testAOP2() {
        testAOP();
    }
}

testAOP1()和testAOP2()都调用了testAOP()方法,但是,现在想在testAOP2()方法调用testAOP()方法的时候,才切入代码,那么这个时候,就需要使用到Pointcut和withincode组合的方式,来精确定位切入点。

// 在testAOP2()方法内
@Pointcut("withincode(* com.xys.aspectjxdemo.MainActivity.testAOP2(..))")
public void invokeAOP2() {
}

// 调用testAOP()方法的时候
@Pointcut("call(* com.xys.aspectjxdemo.MainActivity.testAOP(..))")
public void invokeAOP() {
}

// 同时满足前面的条件,即在testAOP2()方法内调用testAOP()方法的时候才切入
@Pointcut("invokeAOP() && invokeAOP2()")
public void invokeAOPOnlyInAOP2() {
}

@Before("invokeAOPOnlyInAOP2()")
public void beforeInvokeAOPOnlyInAOP2(JoinPoint joinPoint) {
    String key = joinPoint.getSignature().toString();
    Log.d(TAG, "onDebugToolMethodBefore: " + key);
}

我们再来看下编译后的代码:

我们可以看见,只有在testAOP2()方法中被插入了代码,这就做到了精确条件的插入。

异常处理AfterThrowing

AfterThrowing是一个比较少见的Advice,他用于处理程序中未处理的异常,记住,这点很重要,是未处理的异常,具体原因,我们等会看反编译出来的代码就知道了。我们随手写一个异常,代码如下:

public void testAOP() {
    View view = null;
    view.animate();
}

然后使用AfterThrowing来进行AOP代码的编写:

@AfterThrowing(pointcut = "execution(* com.xys.aspectjxdemo.*.*(..))", throwing = "exception")
public void catchExceptionMethod(Exception exception) {
    String message = exception.toString();
    Log.d(TAG, "catchExceptionMethod: " + message);
}

这段代码很简单,同样是使用我们前面类似的表达式,但是这里是为了处理异常,所以,使用了*.*来进行通配,在异常中,我们执行一行日志,编译好的代码如下:

我们可以看见com.xys.aspectjxdemo包下的所有方法都被加上了try catch,同时,在catch中,被插入了我们切入的代码,但是最后,他依然会throw e,也就是说,这个异常已经会被抛出去,崩溃依旧是会发生的。同时,如果你的原始代码中已经try catch了,那么同样也无法处理,具体原因,我们看一个反编译的代码:

可以看见,实际上,原始代码的catch中,又被套了一层try catch,所以,e.printStackTrace()被try catch,也就不会再有异常发生了,也就无法切入了。

AspectJX使用案例

目前鄙司的很多项目都已经使用了这套AOP方案,例如基于AOP的动态权限管理、基于AOP的业务数据埋点、基于AOP的性能监测系统等等。

现在已经开源了一部分基于AOP的动态权限管理的源码,但由于需要剥离业务代码,所以后面会更加完善这功能代码,大家可以继续关注,github地址如下所示:

https://github.com/firefly1126/android_permission_aspectjx

其它的AOP项目陆续开源中,大家可以持续关注~

欢迎关注我的微信公众号


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK