看AspectJ在Android中的强势插入

看AspectJ在Android中的强势插入

什么是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.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
参数 含义
() 表示方法没有任何参数
(..) 表示匹配接受任意个参数的方法
(..,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项目陆续开源中,大家可以持续关注~

欢迎关注我的微信公众号:Android群英传

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容

  • 因为工作需求,自己去了解一下aop并做下的记录,当然大部分都是参考他人博客以及官方文档。 目录 [关于 AOP](...
    forip阅读 2,267评论 1 20
  • What? As we all know,在进行项目构建时,追求各模块高内聚,模块间低耦合。然而现实并不总是如此美...
    MasterNeo阅读 1,958评论 0 17
  • 团队开发框架实战—面向切面的编程 AOP 引言 软件开发的目标是要对世界的部分元素或者信息流建立模型,实现软件系统...
    Bobby0322阅读 4,135评论 4 49
  • AOP之AspectJ 前言:这几天一直在学习aop切面编程,以前一直也有听过aop但是实际用的还是比较少,不是很...
    六_六阅读 1,737评论 0 0
  • 2016年9月21日 今天和彩丽几个妈妈做个一个小型的家庭聚会,产品试验,化妆,促进感情,以美的视角建立一个妈妈圈...
    徐晓美阅读 234评论 0 0