Android AOP框架粗解

/ 0评 / 0

什么是AOP

AOP是Aspect Oriented Programming的缩写,即『面向切面编程』,根据百度百科的解释是:通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术.将日志记录,性能统计,安全控制,事务处理,异常处理等代码从业务逻辑代码中划分出来,通过对这些行为的分离,我们希望可以将它们独立到非指导业务逻辑的方法中,进而改变这些行为的时候不影响业务逻辑的代码。

通常来说,AOP都是为一些相对基础且固定的需求服务,实际常见的场景大致包括:

1、统计埋点

2、日志打印/打点

3、数据校验

4、行为拦截

5、性能监控

6、动态权限控制

常用AOP框架

1、Aspectjx

注入方式:编译期注入

Github地址:https://github.com/HujiangTechnology/gradle_plugin_android_aspectjx

注入方式:

2、Lancet

注入方式:编译期注入

Github地址:https://github.com/eleme/lancet

3、epic

注入方式:ARTHook

Github地址:https://github.com/tiann/epic

4、SandHook

注入方式:ARTHook

Github地址:https://github.com/ganyao114/SandHook

Aspectjx使用

1、集成方式

查看Github ReadMe文档即可。

2、使用步骤

1、定义一个类并使用@Aspect标记

2、在类中定义一个方法,使用@Pointcut标记为切面

3、使用@Before/@After/@Around/...(统称为Advice类型)在切点前、后、中声明执行切面代码。

3、实例:打印所有Activity#setContentView耗时

@Aspect
public class NetworkCheckAspect {

    private static final String TAG = "@2222";

    @Pointcut("call(* android.support.v7.app.AppCompatActivity.setContentView(int))")
    public void pointSetContentView() {

    }

    /**
     * 当然你也可以直接将pointSetContentView上面的注解写到@Around中
     * <p>
     * //@Around("call(* android.support.v7.app.AppCompatActivity.setContentView(int))")
     */
    @Around("pointSetContentView()")
    public void aspectSetContentView(ProceedingJoinPoint joinPoint) {
        Object target = joinPoint.getTarget();
        Log.i(TAG, "aspectSetContentView: " + target.getClass().getSimpleName());
        try {
            long begin = System.currentTimeMillis();
	    //执行真正的方法
            joinPoint.proceed();
            Log.i(TAG, "cast : " + (System.currentTimeMillis() - begin));
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
    }
}

1、定义Pointcut的步骤

call("") 表示插入点在目标方法被调用的时候

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

execution("")表示插入点在目标函数里面

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

通配符

*  :匹配任何数量字符

.. :匹配任何数量字符的重复,如在类型模式中匹配任何数量子包;而在方法参数模式中匹配任何数量参数。

+ :匹配指定类型的子类型;仅能作为后缀放在类型模式后边。

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类型的参数结束,且其前边接受有一个任意类型参数的方法;
返回值类型匹配规则
必填,可以是任何类型模式;“*”表示所有类型;
注解
可选,方法上持有的注解,如@Deprecated
修饰符 可选,如public、protected

参考链接:https://www.cnblogs.com/wuzhiwei549/p/9113510.html

4、高级实例:使用注解标记Pointcut

在实例三种介绍了如何将具体的方法定义为Pointcut,然后进行代码插入,接下来介绍如何使用自定义注解来手动指定Pointcut并获取注解值

1、定义一个注解

/**
 * 如果使用@Around注解,那么必须设置为@Retention(RetentionPolicy.RUNTIME)
 * 如果使用@Before或者@After 那么设置为@Retention(RetentionPolicy.CLASSS)即可
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface NetCheck {

    String value() default "";

}

2、定义切片

@Aspect
public class NetworkCheckAspect {

    private static final String TAG = "@2222";

    /**
     * 定义所有被NetCheck注解的方法  并且将注解的值传递过来
     *
     * @param check
     */
    @Pointcut("execution(@com.corbin.myapplication.NetCheck * *(..)) && @annotation(check)")
    public void checkNetPoint(NetCheck check) {

    }

    @Before("checkNetPoint(check)")
    public void doSomeThing(JoinPoint joinPoint, NetCheck check) {
        Log.i(TAG, "doSomeThing: " + check.value());
    }
}

5、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);
}

另外我们除了使用&& 还可以使用 || 或者就拿上面的例子来说,我们想插入到testAOP1方法中,我们可以写成

@Pointcut("invokeAOP() && !invokeAOP2()")
public void invokeAOPOnlyInAOP2() {
}

6、一些细节

1、每一个插入点方法的第一个参数为JoinPoint或者其子类

当我们使用@Before/@After的时候,可以通过JoinPoint获取一些信息,比如当前的调用者以及方法的属于谁(下面再讲),当我们使用@Around的时候,我们需要使用ProceedingJoinPoint,然后使用joinPoint.proceed();去执行原始方法,如果不调用,那么原始方法就不会被调用了!!!

2、我们使用注解标记Pointcut的时候,如果需要使用@Around,那么我们的注解必须定义为@Retention(RetentionPolicy.RUNTIME),不然会报错。

3、JoinPoint#getTarget与JoinPoint#getThis的区别

target是切入点代码的所有者实例,this是插入的代码的所有者的实例

全部参考链接:

https://segmentfault.com/a/1190000008130757

https://juejin.im/post/5d7a049af265da03d1557f42

https://www.cnblogs.com/wuzhiwei549/p/9113510.html

http://dijun.me/2018/05/04/AspectJX-的使用/

实例参考

https://github.com/xuexiangjys/XAOP

Lancet的使用

1、集成方式

查看Github ReadMe文档即可

2、使用方式

查看Github ReadMe文档即可

Lancet与Aspectjx的区别

Lancet相对于AspectJ的优点

Lancet轻量级的框架,编译速度快,支持增量编译

Lancet语法简单,易于上手。AspectJ需要学习的语法比较多。

Lancet相对于AspectJ的缺点

Lancet仅支持hook具体的方法,不能像AspectJ一样根据自定义的注解来Hook一个类或者任意的方法。

使用场景建议 如果只是相对特定的函数,aar中函数、项目中的函数、Android系统源码中的函数进行Hook,可以选择使用Lancet。 如果需要使用注解对某一类操作进行Hook时,例如,权限检查、性能检测等函数,可以使用AspectJ。

AOP的缺点

1、由于是编译器插入代码,可能导致打包变慢

2、可能导致异常上报的行数不对

3、AOP语法错误难以排查

发表评论

您的电子邮箱地址不会被公开。