Android 进阶 AOP的应用 - AspectJ 的使用

在了解AOP直线需要先了解下注释,可以先看下我的另一篇文章
android注解的使用

AOP(Aspect Oriented Programming)是面向切面编程,AOP和我们平时接触的OOP编程是不同的编程思想,OOP是面向对象编程,提倡的是将功能模块化,对象化。而AOP的思想则是提倡针对同一类问题统一处理,当然,我们在实际编程过程中,不可能单纯的AOP或者OOP的思想来编程,很多时候,可能会混合多种编程思想。

AOP的定义

把某一方面的一些功能提取出来与一批对象进行隔离,提取之后我们就可以对某个单方面的功能进行编程。

AOP的套路

把众多方法中的所有共有代码全部抽取出来,放置到某个地方集中管理,然后在具体运行时,再由容器动态织入这些共有代码的话,最起码可以解决两个问题:
1.1 Android程序员在编写具体的业务逻辑处理方法时,只需关心核心的业务逻辑处理,既提高了工作效率,又使代码变更简洁优雅。
1.2 在日后的维护中由于业务逻辑代码与共有代码分开存放,而且共有代码是集中存放的,因此使维护工作变得简单轻松。
1.3 面向切面编程AOP技术就是为解决这个问题而诞生的,切面就是横切面,代表的是一个普遍存在的共有功能,例如,日志切面、权限切面及事务切面等。

AOP的作用

一般来说,主要用于不想侵入原有代码的场景中,例如SDK需要无侵入的在宿主中插入一些代码,做日志埋点、性能监控、动态权限控制、甚至是代码调试等等。
AOP的使用目前在android方面通常都是用eclipse的AspectJ
另外一个比较成功的使用AOP的库是Jake大神的Hugo:
Jake大神的Hugo

[aspectj下载地址](http://www.eclipse.org/downloads/download.php?file=/tools/aspectj/aspectj-1.8.10.jar
在网上还发现一个github的库,据说竭诚了aspectj
https://github.com/HujiangTechnology/gradle_plugin_android_aspectjx

AspectJ 之 Join Points介绍

Join Points在AspectJ中是关键的概念。Join Points可以看做是程序运行时的一个执行点,比如:一个函数的调用可以看做是个Join Points,相当于代码切入点。但在AspectJ中,只有下面几种执行点是认为是Join Points:

Join Points 说明 实例
method call 函数调用 比如调用Log.e(),这是一个个Join Point
method execution 函数执行 比如Log.e()的执行内部,是一处Join Points。注意这里是函数内部
constructor call 构造函数调用 和method call 类似
constructor execution 构造函数执行 和method execution 类似
field get 获取某个变量 比如读取DemoActivity.debug成员
field set 设置某个变量 比如设置DemoActivity.debug成员
pre-initialization Object在构造函数中做的一些工作。 -
initialization Object在构造函数中做的工作。 -
static initialization 类初始化 比如类的static{}
handler 异常处理 比如try catch 中,对应catch内的执行
advice execution 这个是AspectJ 的内容 -

Pointcuts 介绍

一个程序会有多个Join Points,即使同一个函数,也还分为call 和 execution 类型的Join Points,但并不是所有的Join Points 都是我们关心的,Pointcuts 就是提供一种使得开发者能够值选择所需的JoinPoints的方法。

Advice介绍

Advice就是我们插入的代码可以以何种方式插入,有Before 还有 After、Around。
下面看个例子:

    /**
     * 找到处理的切点
     * * *(..)  可以处理所有的方法
     */
    @Pointcut("execution(@com.liuy.architect_day02.CheckNet * *(..))")
    public void checkNetBehavior() {

    }

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

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

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

@(“execution(* com.example.andorid.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()代表执行原始的方法,在这之前、之后,都可以进行各种逻辑处理。

自定义Pointcuts

自定义Pointcuts可以让我们更加精准的切入一个或多个指定的切入点。
首先我们要定义一个注解类

@Target(ElementType.METHOD) // Target 放在哪个位置
@Retention(RetentionPolicy.RUNTIME)// RUNTIME 运行时 xUtils  CLASS 代表编译时期 ButterKnife   SOURCE 代表资源
public @interface CheckNet { // @interface 注解

}

在需要插入代码的地方加入这个注解,例如在MainActivity中加入:

public class MainActivity extends AppCompatActivity {

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

    @CheckNet
    public void click(View view) {
        Intent intent = new Intent(this, MainActivity.class);
        startActivity(intent);
    }

    @CheckNet
    public void postData() {

    }
}

最后创建切入代码,我这里的例子是判断网络是否可用的

@Aspect
public class SectionAspect {

    /**
     * 找到处理的切点
     * * *(..)  可以处理所有的方法
     */
    @Pointcut("execution(@com.darren.architect_day02.CheckNet * *(..))")
    public void checkNetBehavior() {

    }

    /**
     * 处理切面
     */
    @Around("checkNetBehavior()")
    public Object checkNet(ProceedingJoinPoint joinPoint) throws Throwable {
        Log.e("TAG", "checkNet");
        // 做埋点  日志上传  权限检测(我写的,RxPermission , easyPermission) 网络检测
        // 网络检测
        // 1.获取 CheckNet 注解  NDK  图片压缩  C++ 调用Java 方法
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        CheckNet checkNet = signature.getMethod().getAnnotation(CheckNet.class);
        if (checkNet != null) {
            // 2.判断有没有网络  怎么样获取 context?
            Object object = joinPoint.getThis();// View Activity Fragment ; getThis() 当前切点方法所在的类
            Context context = getContext(object);
            if (context != null) {
                if (!isNetworkAvailable(context)) {
                    // 3.没有网络不要往下执行
                    Toast.makeText(context,"请检查您的网络",Toast.LENGTH_LONG).show();
                    return null;
                }
            }
        }
        return joinPoint.proceed();
    }

    /**
     * 通过对象获取上下文
     *
     * @param object
     * @return
     */
    private Context getContext(Object object) {
        if (object instanceof Activity) {
            return (Activity) object;
        } else if (object instanceof Fragment) {
            Fragment fragment = (Fragment) object;
            return fragment.getActivity();
        } else if (object instanceof View) {
            View view = (View) object;
            return view.getContext();
        }
        return null;
    }

    /**
     * 检查当前网络是否可用
     *
     * @return
     */
    private static boolean isNetworkAvailable(Context context) {
        // 获取手机所有连接管理对象(包括对wi-fi,net等连接的管理)
        ConnectivityManager connectivityManager = (ConnectivityManager)
                context.getSystemService(Context.CONNECTIVITY_SERVICE);
        if (connectivityManager != null) {
            // 获取NetworkInfo对象
            NetworkInfo[] networkInfo = connectivityManager.getAllNetworkInfo();

            if (networkInfo != null && networkInfo.length > 0) {
                for (int i = 0; i < networkInfo.length; i++) {
                    // 判断当前网络状态是否为连接状态
                    if (networkInfo[i].getState() == NetworkInfo.State.CONNECTED) {
                        return true;
                    }
                }
            }
        }
        return false;
    }
}

execution语法

语法结构:execution([修饰符] 返回值类型 方法名(参数) [异常模式]) 实例
execution(public .(..)) 所有的public方法
execution(* hello(..)) 所有的hello()方法
execution(String hello(..)) 所有返回值为String的hello方法。
execution(* hello(String)) 所有参数为String类型的hello()
execution(* hello(String..)) 至少有一个参数,且第一个参数类型为String的hello方法
execution(* com.aspect..*(..)) 所有com.aspect包,以及子孙包下的所有方法
execution(* com...Dao.find*(..)) com包下的所有一Dao结尾的类的一find开头的方法

call和execution的区别

call为调用,而execution为执行。

3 public class HelloWorld {
4   public static void main(int i){
5       System.out.println("in the main method  i = " + i);
6   }
7   
8   public static void main(String[] args) {
9       main(5);
10  }

我们拦截了参数为:int的main方法。 这里用到了一内置的对象:thisJoinPoint,他表示当前jionPoint. 跟我们在java中的this 其实是差不多的,如果你不明白,那么你多运行一下,好好体会一下。getSourceLocation()表示源代码的位置:

public aspect HelloAspect {
 
    pointcut HelloWorldPointCut() : call(* main(int));
    
    
    
   before() : HelloWorldPointCut(){
      System.out.println("Entering : " + thisJoinPoint.getSourceLocation());
    }
}

我们运行一下HelloWorld.java。
打印结果为

Entering : HelloWorld.java:9
in the main method  i = 5

9是行号
接下来我们把call 换成execution打印

public aspect HelloAspect {
 
    pointcut HelloWorldPointCut() : execution(* main(int));
    
    
    
   before() : HelloWorldPointCut(){
      System.out.println("Entering : " + thisJoinPoint.getSourceLocation());
    }
}

Entering : HelloWorld.java:4
in the main method  i = 5

结果为4

从上面的结果可以看出,call是调用的地方,execution是执行的地方。
thisJoinPoint.getSourceLocation() 这段代码将会在我们以后的Demo中经常用到。这是一个跟踪调试的好办法。

within 和 withincode

within

还是上面的例子,如果别的类中也有main方法?应该怎么办?你首先想到的肯定是修改pointcut,指定到我们的HelloWorld类。 这当然是可以的,假设:现在还有5个类,也有main方法,也需要拦截。那你这解决办法肯定就不行了。(当然你也可以用 || 来组合他们)。这个时候就用到了我们的within了。代码如下

    pointcut HelloWorldPointCut() : execution(* main(..)) && !within(HelloAspectDemo);
    
   before() : HelloWorldPointCut(){
      System.out.println("Entering : " + thisJoinPoint.getSourceLocation());

withincode

withincode与within相似,不过withcode()接受的signature是方法,而不是类。用法,意思都差不多,只不过是使用场合不同。

public class MainActivity extends AppCompatActivity {
    final String TAG = "MainActivity";

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

    public void registerTest() {
        Log.e(TAG, "execute registerTest");
    }

    public void register1(){
        registerTest();
    }

    public void register2(){
        registerTest();
    }

    public void register3(){
        registerTest();
    }
}

以上三个register方法都调用了registerTest()方法,如果这个时候想调用register3的registerTest()的方法,需要如下操作

@Pointcut("(call(* *..registerTest()))&&withincode(* *..register2())")
public void invokeregisterTestInregister2() {
}

@Before("invokeregisterTestInregister2()")
public void beforeInvokeregisterTestInregister2(JoinPoint joinPoint) throws Throwable {
    Log.e(TAG, "method:" + getMethodName(joinPoint).getName());
}

private MethodSignature getMethodName(JoinPoint joinPoint) {
    if (joinPoint == null) return null;
    return (MethodSignature) joinPoint.getSignature();
}

输出如下

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