AOP:面向切面编程(Aspect-Oriented Programming)。java
若是说,OOP若是是把问题划分到单个模块的话,那么AOP就是把涉及到众多模块的某一类问题进行统一管理。好比有三个模块:登录、转帐和大文件上传,如今须要加入性能检测功能,统计这三个模块每一个方法耗时多少。android
OOP思想作法是设计一个性能检测模块,提供接口供这三个模块调用。这样每一个模块都要调用性能检测模块的接口,若是接口有改动,须要在这三个模块中每次调用的地方修改。面试
AOP的思想作法是:在这些独立的模块间,在特定的切入点进行hook,将共同的逻辑添加到模块中而不影响原有模块的独立性。编程
因此这就是上面所说的:把涉及到众多模块的某一类问题进行统一管理。缓存
安卓AOP三剑客:APT,AspectJ和Javassist。服务器
APT应用:Dagger,butterKnife,组件化方案等等网络
AspectJ:主要用于性能监控,日志埋点等架构
Javassist:热更新(能够在编译后,打包Dex以前干事情,能够突破一下限制)app
今天的主角是AspectJ,主要用于不想侵入原有代码的场景中,例如SDK须要无侵入的在宿主中插入一些代码,作日志埋点、性能监控、动态权限控制、甚至是代码调试等等。ide
接入说明
首先,须要在项目根目录的build.gradle中增长依赖:
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:2.3.0-beta2'
classpath 'com.hujiang.aspectjx:gradle-android-plugin-aspectjx:2.0.6'
}
}
复制代码
而后再主项目或者库的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'
}
复制代码
直接看代码,Activity中:
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
testMethod();
}
private void testMethod() {
Log.e(DemoAspect.TAG, "testMethod-invoke");
}
复制代码
新建DemoAspect类:
@Aspect
public class DemoAspect {
public static final String TAG = "DemoAspect";
@Before("execution(* com.hujiang.library.demo.DemoActivity.test*(..))")
public void testAspectBefore(JoinPoint point) {
Log.e(TAG, point.getSignature().getName()+"-before ");
}
}
复制代码
运行时候打印:
testMethod-before com.hujiang.library.demo E/DemoAspect: testMethod-invoke
复制代码
因此完成插入操做咱们只须要
类上加入注释@Aspect
方法上加入注释@Before
Before里写入要插入的相关信息
是否是很简单,下面就一一详细讲解。
就是说咱们要插入的代码以何种方式插入,就是方法上的注释。
Before和After很好理解,上面的例子已经展现的很清楚了。
AfterReturning
适用于须要获取到返回值的状况好比:
private int AfterReturningTest() {
Log.e(DemoAspect.TAG, "AfterReturning-invoke");
return 10;
}
@AfterReturning(value = "execution(* com.hujiang.library.demo.DemoActivity.AfterReturning*(..))", returning = "num")
public void testAspectAfterReturning(int num) {
Log.e(TAG, "AfterReturning-num:" + num);
}
复制代码
这样就能够在切面方法里获取到返回值了,值得注意的是方法参数必须和注解中的值一致。
【returning = “num”】===【int num】
复制代码
AfterThrowing
适用于收集和监控异常信息。
private void AfterThrowingTest() {
View v = null;
v.setVisibility(View.VISIBLE);
}
@AfterThrowing(value = "execution(* com.hujiang.library.demo.DemoActivity.AfterThrowing*(..))", throwing = "exception")
public void testAspectAfterReturning(Exception exception) {
Log.e(TAG, "AfterThrowing-exception:" + exception.getMessage());
}
复制代码
一样,参数和注解里的值必须一致。这里崩溃一样会发生,不会由于切面操做而直接catch住,只是在抛出异常以前会打印出异常信息而已。
Around
在方法执行先后均可调用,比较灵活。
private void AroundTest() {
Log.e(DemoAspect.TAG, "AroundTest-invoke");
}
@Around("execution(* com.hujiang.library.demo.DemoActivity.AroundTest(..))")
public void testAspectAround(ProceedingJoinPoint point) throws Throwable {
Log.e(TAG, point.getSignature().getName() + "-before ");
point.proceed();
Log.e(TAG, point.getSignature().getName() + "-after ");
}
复制代码
经过执行ProceedingJoinPoint的proceed方法调用原方法,灵活控制。若是你想的话也能够不调用,直接拦截了。
Pointcut
告诉代码注入工具,在何处注入一段特定代码的表达式。也就是例子中的这句话:
@Before("execution(* com.hujiang.library.demo.DemoActivity.test*(..))")
复制代码
咱们分红几个部分依次来看:
@Before:Advice,也就是具体的插入点,咱们已经介绍过
execution:处理Join Point的类型,例如call、execution、withincode
其中call、execution相似,都是插入代码的意思,区别就是execution是在被切入的方法中,call是在调用被切入的方法前或者后。
//对于Call来讲:
Call(Before)
Pointcut{
Pointcut Method
}
Call(After)
//对于Execution来讲:
Pointcut{
execution(Before)
Pointcut Method
execution(After)
}
复制代码
withcode这个语法一般来进行一些切入点条件的过滤,做更加精确的切入控制,好比:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
test1();
test2();
}
public void test() {
Log.e("qiuyunfei", "test");
}
public void test1() {
test();
}
public void test2() {
test();
}
复制代码
假如咱们想要切test方法,可是只但愿在test2中调用test才执行切面方法,就须要用到withincode。
// 在test()方法内
@Pointcut("withincode(* com.hujiang.library.aspect.MainActivity.test2(..))")
public void invoke2() {
}
// 调用test()方法的时候
@Pointcut("call(* com.hujiang.library.aspect.MainActivity.test(..))")
public void invoke() {
}
// 同时知足前面的条件,即在test2()方法内调用test()方法的时候才切入
@Pointcut("invoke() && invoke2()")
public void invokeOnlyIn2() {
}
@Before("invokeOnlyIn2()")
public void beforeInvokeOnlyIn2(JoinPoint joinPoint) {
String key = joinPoint.getSignature().toString();
Log.d(TAG, "beforeInvokeOnlyIn2: " + key);
}
复制代码
MethodPattern:这个是最重要的表达式,大体为:@注解和访问权限,返回值的类型和包名.函数名(参数)
@注解和访问权限(public/private/protect,以及static/final):属于可选项。若是不设置它们,则默认都会选择。以访问权限为例,若是没有设置访问权限做为条件,那么public,private,protect及static、final的函数都会进行搜索。
返回值类型:就是普通的函数的返回值类型。若是不限定类型的话,就用*通配符表示。
包名.函数名:用于查找匹配的函数。可使用通配符,包括和…以及+号。其中号用于匹配除.号以外的任意字符,而…则表示任意子package,+号表示子类。
* com.hujiang.library.demo.DemoActivity.test*(..)
复制代码
第一部分:『』表示返回值,『』表示返回值为任意类型。
第二部分:就是典型的包名路径,其中能够包含『』来进行通配,几个『』没区别。同时,这里能够经过『&&、||、!』来进行条件组合。相似【test*】的写法,表示以test开头为方法名的任意方法。
第三部分:()表明这个方法的参数,你能够指定类型,例如android.os.Bundle,或者(…)这样来表明任意类型、任意个数的参数,也能够混合写法(android.os.Bundle,…)这样来表明第一个参数为bundle,后面随意。
自定义Pointcuts:有时候咱们须要指定哪些方法须要进行AOP操做,目标明确,也能够经过注解的方式来完成。首先声明注解:
@Retention(RetentionPolicy.CLASS)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface AspectAnnotation {
}
复制代码
而后在切面类中定义:
//定义一个使用该注解的Pointcut
@Pointcut("execution(@com.hujiang.library.aspect.AspectAnnotation * *(..))")
public void AspectAnnotation(){
}
@Before("AspectAnnotation()")
public void testAspectAnnotation(JoinPoint point){
Log.e(TAG, point.getSignature().getName() + "-Before ");
}
//在Activity中使用
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
AnnotationTest();
}
@AspectAnnotation
private void AnnotationTest() {
Log.e(DemoAspect.TAG, "AnnotationTest-invoke");
}
复制代码
使用很简单,前面也介绍过MethodPattern,注释@在这里就不能省略了。
实现登陆检查的操做
不少app都有这个需求,在操做前提醒用户注册登陆,跳转到注册或者登陆界面,若是用AspectJ实现就显得很是简洁且无侵入性。
private static final String TAG = "AspectCommonTool";
@Pointcut("execution(@xxx.aspectj.annotation.NeedLogin * *(..))")
public void needLoginMethod() {
}
/** * 在@NeedLogin方法中插入 * 若在非Activity中使用@NeedLogin,参数必须传入Context做为跳转起始页 */
@Around("needLoginMethod()")
public void onNeedLoginAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
Context mContext = null;
//proceedingJoinPoint.getThis()能够获取到调用该方法的对象
if (proceedingJoinPoint.getThis() instanceof Context) {
mContext = (Context) proceedingJoinPoint.getThis();
} else {
//proceedingJoinPoint.getArgs()能够获取到方法的全部参数
for (Object context : proceedingJoinPoint.getArgs()) {
if (context instanceof Context) {
mContext = (Context) context;
break;
}
}
}
if (mContext == null) {
return;
}
if (LoginUtils.isLogin()) {
/** * 若是用户登陆则执行原方法 */
proceedingJoinPoint.proceed();
} else {
/** * 未登陆状况下跳转至登陆注册主界面 */
}
复制代码
使用很方便,毫无侵入性,后期也很好维护。相似的思想也能够实现:检查网络情况、检查权限状态、避免按钮屡次点击、自动完成缓存等状况。
性能监控
AspectJ其实在Android中的应用主要仍是性能监控、日志埋点等,下面以一个简单的例子表示:
咱们监控布局加载耗时,判断布局是否嵌套过多或者过于复制致使Activity启动卡顿,首先咱们知道Activity是经过setContentView方法加载布局的:
布局解析过程,IO过程
建立View为反射过程
这两步均为耗时操做,因此咱们须要监控setContentView。
@Around("execution(* android.app.Activity.setContentView(..))")
public void getContentViewTime(ProceedingJoinPoint point) throws Throwable {
String name = point.getSignature().toShortString();
long time = System.currentTimeMillis();
point.proceed();
Log.e(TAG, name + " cost: " + (System.currentTimeMillis() - time));
}
//Activity
public class DemoActivity extends AppCompatActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}
复制代码
打印日志以下:
DemoAspect: AppCompatActivity.setContentView(..) cost: 76
复制代码
平常开发中,咱们能够将耗时上传到服务器,收集用户信息,找到卡顿Activity作出对应优化。
固然,这只是很是简单的实现,实际开发中还能够监控各类你想监控的位置。 Android学习PDF+架构视频+面试文档+源码笔记