Android性能优化神器-AspectJ

一、AOP

AOP:面向切面编程(Aspect-Oriented Programming)。

如果说,OOP如果是把问题划分到单个模块的话,那么AOP就是把涉及到众多模块的某一类问题进行统一管理。

比如有三个模块:登陆、转账、大文件上传,现在需要加入性能检测功能,统计这三个模块每个方法耗时多少。


clipboard.png

OOP思想做法是设计一个性能检测模块,提供接口供这三个模块调用。这样每个模块都要调用性能检测模块的接口,如果接口有改动,需要在这三个模块中每次调用的地方修改。

AOP的思想做法是:在这些独立的模块间,在特定的切入点进行hook,将共同的逻辑添加到模块中而不影响原有模块的独立性。

所以这就是上面所说的:把涉及到众多模块的某一类问题进行统一管理

安卓AOP三剑客:APT,AspectJ,Javassist。

APT应用:Dagger,butterKnife,组件化方案等等

AspectJ:主要用于性能监控,日志埋点等

Javassist:热更新(可以在编译后,打包Dex之前干事情,可以突破一下限制)

clipboard1.png

二、AspectJ

今天的主角是AspectJ,主要用于不想侵入原有代码的场景中,例如SDK需要无侵入的在宿主中插入一些代码,做日志埋点、性能监控、动态权限控制、甚至是代码调试等等。

接入说明

首先,需要在项目根目录的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'
}
1、基本使用

直接看代码

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

所以完成插入操作我们只需要

1)类上加入注释@Aspect

2)方法上加入注释@Before

3)Before里写入要插入的相关信息

是不是很简单,下面就一一详细讲解。

2、Advice

就是说我们要插入的代码以何种方式插入,就是方法上的注释

clipboard2.png

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方法调用原方法,灵活控制。如果你想的话也可以不调用,直接拦截了。

3、Pointcut

告诉代码注入工具,在何处注入一段特定代码的表达式。也就是例子中的这句话:

@Before("execution(* com.hujiang.library.demo.DemoActivity.test*(..))")

我们分成几个部分依次来看:

1)@Before:Advice,也就是具体的插入点,我们已经介绍过

2)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);
}

3)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,后面随意。

4)自定义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,注释@在这里就不能省略了

4、AspectJ实战

1)实现登录检查的操作

很多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 {
        /**
         * 未登录情况下跳转至登录注册主界面
         */
    }
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 {
        /**
         * 未登录情况下跳转至登录注册主界面
         */
    }
}

使用很方便,毫无侵入性,后期也很好维护。
类似的思想也可以实现:检查网络状况、检查权限状态、避免按钮多次点击、自动完成缓存等情况。

2)性能监控

AspectJ其实在Android中的应用主要还是性能监控、日志埋点等,下面以一个简单的例子表示:
我们监控布局加载耗时,判断布局是否嵌套过多或者过于复制导致Activity启动卡顿,首先我们知道Activity是通过setContentView方法加载布局的:
1、布局解析过程,IO过程
2、创建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做出对应优化。
当然,这只是非常简单的实现,实际开发中还可以监控各种你想监控的位置。

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

推荐阅读更多精彩内容