AspectJ

AspectJ 的优势与局限性

最常用的字节码处理框架有 AspectJ、ASM 等等,它们的相同之处在于输入输出都是 Class 文件。并且,它们都是 在 Java 文件编译成 .class 文件之后,生成 Dalvik 字节码之前执行。

AspectJ 作为 Java 中流行的 AOP(aspect-oriented programming) 编程扩展框架,其内部使用的是 BCEL框架 来完成其功能

1、AspectJ 的优势

它的优势有两点:成熟稳定、使用非常简单。

1、成熟稳定

字节码的处理并不简单,特别是 针对于字节码的格式和各种指令规则,如果处理出错,就会导致程序编译或者运行过程中出现问题。
而 AspectJ 作为从 2001 年发展至今的框架,它已经发展地非常成熟,通常不用考虑插入的字节码发生正确性相关的问题。

2、使用非常简单

AspectJ 的使用非常简单,并且它的功能非常强大,我们完全不需要理解任何 Java 字节码相关的知识,就可以在很多情况下对字节码进行操控。例如,它可以在如下五个位置插入自定义的代码:

  • 1)、*在方法(包括构造方法)被调用的位置
  • 2)、在方法体(包括构造方法)的内部
  • 3)、在读写变量的位置
  • 4)、在静态代码块内部
  • 5)、在异常处理的位置的前后
    此外,它也可以 直接将原位置的代码替换为自定义的代码

2、AspectJ 的缺陷

AspectJ 的缺点可以归结为如下 三点:

1、切入点固定

AspectJ 只能在一些固定的切入点来进行操作,如果想要进行更细致的操作则很难实现,它无法针对一些特定规则的字节码序列做操作。

2、正则表达式的局限性

AspectJ 的匹配规则采用了类似正则表达式的规则,比如 匹配 Activity 生命周期的 onXXX 方法,如果有自定义的其他以 on 开头的方法也会匹配到,这样匹配的正确性就无法满足。

3、性能较低

AspectJ 在实现时会包装自己一些特定的类,它并不会直接把 Trace 函数直接插入到代码中,而是经过一系列自己的封装
这样不仅生成的字节码比较大,而且对原函数的性能会有不小的影响。
如果想对 App 中所有的函数都进行插桩,性能影响肯定会比较大。如果你只插桩一小部分函数,那么 AspectJ 带来的性能损耗几乎可以忽略不计。

AspectJ 核心语法简介

AspectJ 其实就是一种 AOP 框架,AOP 是实现程序功能统一维护的一种技术。

利用 AOP 可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合性降低,提高程序的可重用性,同时大大提高了开发效率。

  • 1)、无侵入性。
  • 2)、修改方便。
    此外,AOP 不同于 OOP 将问题划分到单个模块之中,它把 涉及到众多模块的同一类问题进行了统一处理

比如我们可以设计两个切面,一个是用于处理 App 中所有模块的日志输出功能,另外一个则是用于处理 App 中一些特殊函数调用的权限检查。

1、横切关注点

对哪些方法进行拦截,拦截后怎么处理。

2、切面(Aspect)

类是对物体特征的抽象,切面就是对横切关注点的抽象。

3、连接点(JoinPoint)

JPoint 是一个程序的关键执行点,也是我们关注的重点。它就是指被拦截到的点(如方法、字段、构造器等等)。

4、切入点(PointCut)

对 JoinPoint 进行拦截的定义。PointCut 的目的就是提供一种方法使得开发者能够选择自己感兴趣的 JoinPoint。

5、通知(Advice)

切入点仅用于捕捉连接点集合,但是,除了捕捉连接点集合以外什么事情都没有做。事实上实现横切行为我们要使用通知。
它 一般指拦截到 JoinPoint 后要执行的代码,分为 前置、后置、环绕 三种类型。
这里,我们需要 注意 Advice Precedence(优先权) 的情况,比如我们对同一个切面方法同时使用了 @Before 和 @Around 时就会报错,此时会提示需要设置 Advice 的优先级。
切入点和通知动态地影响程序流程,类型间声明则是静态的影响程序的类等级结构,而切面则是对所有这些新结构的封装

Android 平台上要使用 AspectJ 还是有点麻烦的,这里我们可以直接使用沪江的 AspectJX 框架。下面,我们就来使用 AspectJX 进行 AOP 切面编程。

AspectJX 实战

首先,为了在 Android 使用 AOP 埋点需要引入 AspectJX,在项目根目录的 build.gradle 下加入:

    classpath 'com.hujiang.aspectjx:gradle-android-plugin-aspectjx:2.0.0'

然后,在 app 目录下的 build.gradle 下加入:

    apply plugin: 'android-aspectjx'
    implement 'org.aspectj:aspectjrt:1.8.+'

JoinPoint 一般定位在如下位置:

  • 1)、函数调用。
  • 2)、获取、设置变量。
  • 3)、类初始化。

使用 PointCut 对我们指定的连接点进行拦截,通过 Advice,就可以拦截到 JoinPoint 后要执行的代码。Advice 通常有以下 三种类型:

  • 1)、Before:PointCut 之前执行。
  • 2)、After:PointCut 之后执行。
  • 3)、Around:PointCut 之前、之后分别执行。

1、最简单的 AspectJ 示例

    @Before("execution(* android.app.Activity.on**(..))")
    public void onActivityCalled(JoinPoint joinPoint) throws Throwable {
        Log.d(...)
    }

其中,在 execution 中的是一个匹配规则,第一个 * 代表匹配任意的方法返回值,后面的语法代码匹配所有 Activity 中以 on 开头的方法。这样,我们就可以 在 App 中所有 Activity 中以 on 开头的方法中输出一句 log。

上面的 execution 就是处理 Join Point 的类型,通常有如下两种类型:

  • 1)、call:代表调用方法的位置,插入在函数体外面。
  • 2)、execution:代表方法执行的位置,插入在函数体内部。

2、统计 Application 中所有方法的耗时

    @Aspect
    public class ApplicationAop {
    
        @Around("call (* com.json.chao.application.BaseApplication.**(..))")
        public void getTime(ProceedingJoinPoint joinPoint) {
        Signature signature = joinPoint.getSignature();
        String name = signature.toShortString();
        long time = System.currentTimeMillis();
        try {
            joinPoint.proceed();
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
        Log.i(TAG, name + " cost" +     (System.currentTimeMillis() - time));
        }
    }

需要注意的是,当 Action 为 Before、After 时,方法入参为 JoinPoint。当 Action 为 Around 时,方法入参为 ProceedingPoint。

Around 和 Before、After 的最大区别就是 ProceedingPoint 不同于 JoinPoint,其提供了 proceed 方法执行目标方法

3、对 App 中所有的方法进行 Systrace 函数插桩

我们就可以利用它实现对 App 中所有的方法进行 Systrace 函数插桩了,代码如下所示:

    @Aspect
    public class SystraceTraceAspectj {

        private static final String TAG = "SystraceTraceAspectj";

        @Before("execution(* **(..))")
        public void before(JoinPoint joinPoint) {
            TraceCompat.beginSection(joinPoint.getSignature().toString());
        }
    
        @After("execution(* **(..))")
        public void after() {
            TraceCompat.endSection();
        }
    }

使用 AspectJ 打造自己的性能监控框架

将以奇虎360的 ArgusAPM 性能监控框架来全面分析下 AOP 技术在性能监控方面的应用。

监控 OKHttp3 的每一次网络请求

首先,我们看到 OKHttp3 的切面文件,代码如下所示:

    /**
    * OKHTTP3 切面文件
    *
    * @author ArgusAPM Team
    */
    @Aspect
    public class OkHttp3Aspect {

        // 1、定义一个切入点,用于直接调用 OkHttpClient 的 build 方法。
        @Pointcut("call(public okhttp3.OkHttpClient build())")
        public void build() {

        }

        // 2、使用环绕通知在 build 方法执行前添加一个 NetWokrInterceptor。
        @Around("build()")
        public Object aroundBuild(ProceedingJoinPoint joinPoint) throws Throwable {
            Object target = joinPoint.getTarget();

            if (target instanceof OkHttpClient.Builder && Client.isTaskRunning(ApmTask.TASK_NET)) {
                OkHttpClient.Builder builder = (OkHttpClient.Builder) target;
                builder.addInterceptor(new NetWorkInterceptor());
            }

            return joinPoint.proceed();
        }
    }

在注释1、2处,在调用 OkHttpClient 的 build 方法之前添加了一个 NetWokrInterceptor。我们看看它的实现代码,如下所示:

    @Override
    public Response intercept(Chain chain) throws IOException {
        // 1、获取每一个 OkHttp 请求的开始时间
        long startNs = System.currentTimeMillis();

        mOkHttpData = new OkHttpData();
        mOkHttpData.startTime = startNs;

        if (Env.DEBUG) {
            Log.d(TAG, "okhttp request 开始时间:" + mOkHttpData.startTime);
        }

        Request request = chain.request();
        
        // 2、记录当前请求的请求 url 和请求数据大小
        recordRequest(request);

        Response response;

        try {
            response = chain.proceed(request);
        } catch (IOException e) {
            if (Env.DEBUG) {
                e.printStackTrace();
                Log.e(TAG, "HTTP FAILED: " + e);
            }
            throw e;
        }
        
        // 3、记录这次请求花费的时间
        mOkHttpData.costTime = System.currentTimeMillis() - startNs;

        if (Env.DEBUG) {
            Log.d(TAG, "okhttp chain.proceed 耗时:" + mOkHttpData.costTime);
        }
        
        // 4、记录当前请求返回的响应码和响应数据大小
        recordResponse(response);

        if (Env.DEBUG) {
            Log.d(TAG, "okhttp chain.proceed end.");
        }

        // 5、记录 OkHttp 的请求数据
        DataRecordUtils.recordUrlRequest(mOkHttpData);
        return response;
    }

首先,在注释1处,获取了每一个 OkHttp 请求的开始时间。接着,在注释2处,通过 recordRequest 方法记录了当前请求的请求 url 和请求数据大小。然后,注释3处,记录了这次 请求所花费的时间。

接下来,在注释4处,通过 recordResponse 方法记录了当前请求返回的响应码和响应数据大小。最后,在注释5处,调用了 DataRecordUtils 的 recordUrlRequest 方法记录了 mOkHttpData 中保存好的数据。我们继续看到 recordUrlRequest 方法,代码如下所示:

    /**
     * recordUrlRequest
     *
     * @param okHttpData
     */
    public static void recordUrlRequest(OkHttpData okHttpData) {
        if (okHttpData == null || TextUtils.isEmpty(okHttpData.url)) {
            return;
        }

        QOKHttp.recordUrlRequest(okHttpData.url, okHttpData.code, okHttpData.requestSize,
                okHttpData.responseSize, okHttpData.startTime, okHttpData.costTime);

        if (Env.DEBUG) {
            Log.d(Env.TAG, "存储okkHttp请求数据,结束。");
        }
    }

可以看到,这里调用了 QOKHttp 的 recordUrlRequest 方法用于记录网络请求信息。我们再看到 QOKHttp 的 recordUrlRequest 方法,如下所示:

    /**
     * 记录一次网络请求
     *
     * @param url          请求url
     * @param code         状态码
     * @param requestSize  发送的数据大小
     * @param responseSize 接收的数据大小
     * @param startTime    发起时间
     * @param costTime     耗时
     */
    public static void recordUrlRequest(String url, int code, long requestSize, long responseSize,
                                        long startTime, long costTime) {
        NetInfo netInfo = new NetInfo();
        netInfo.setStartTime(startTime);
        netInfo.setURL(url);
        netInfo.setStatusCode(code);
        netInfo.setSendBytes(requestSize);
        netInfo.setRecordTime(System.currentTimeMillis());
        netInfo.setReceivedBytes(responseSize);
        netInfo.setCostTime(costTime);
        netInfo.end();
    }

可以看到,这里 将网络请求信息保存在了 NetInfo 中,并最终调用了 netInfo 的 end 方法,代码如下所示:

    /**
     * 为什存储的操作要写到这里呢?
     * 历史原因
     */
    public void end() {
        if (DEBUG) {
            LogX.d(TAG, SUB_TAG, "end :");
        }
        this.isWifi = SystemUtils.isWifiConnected();
        this.costTime = System.currentTimeMillis() - startTime;
        if (AnalyzeManager.getInstance().isDebugMode()) {
            AnalyzeManager.getInstance().getNetTask().parse(this);
        }
        ITask task = Manager.getInstance().getTaskManager().getTask(ApmTask.TASK_NET);
        if (task != null) {
            // 1
            task.save(this);
        } else {
            if (DEBUG) {
                LogX.d(TAG, SUB_TAG, "task == null");
            }
        }
    }

可以看到,这里 最终还是调用了 NetTask 实例的 save 方法保存网络请求的信息。而 NetTask 肯定是使用了与之对应的 NetStorage 实例将信息保存在了 ContentProvider 中

监控 HttpConnection 和 HttPClient 的每一次网络请求

在 ArgusAPM 中,使用的是 TraceNetTrafficMonitor 这个切面类对 HttpConnection 的每一次网络请求进行监控。关键代码如下所示:

    @Aspect
    public class TraceNetTrafficMonitor {

        // 1
        @Pointcut("(!within(com.argusapm.android.aop.*) && ((!within(com.argusapm.android.**) && (!within(com.argusapm.android.core.job.net.i.*) && (!within(com.argusapm.android.core.job.net.impl.*) && (!within(com.qihoo360.mobilesafe.mms.transaction.MmsHttpClient) && !target(com.qihoo360.mobilesafe.mms.transaction.MmsHttpClient)))))))")
        public void baseCondition() {
        }

        // 2
        @Pointcut("call(org.apache.http.HttpResponse org.apache.http.client.HttpClient.execute(org.apache.http.client.methods.HttpUriRequest)) && (target(httpClient) && (args(request) && baseCondition()))")
        public void httpClientExecuteOne(HttpClient httpClient, HttpUriRequest request) {
        }

        // 3
        @Around("httpClientExecuteOne(httpClient, request)")
        public HttpResponse httpClientExecuteOneAdvice(HttpClient httpClient, HttpUriRequest request) throws IOException {
            return QHC.execute(httpClient, request);
        }

        // 排查一些处理异常的切面代码

        // 4
        @Pointcut("call(java.net.URLConnection openConnection()) && (target(url) && baseCondition())")
        public void URLOpenConnectionOne(URL url) {
        }

        // 5
        @Around("URLOpenConnectionOne(url)")
        public URLConnection URLOpenConnectionOneAdvice(URL url) throws IOException {
            return QURL.openConnection(url);
        }

        // 排查一些处理异常的切面代码
    
    }

TraceNetTrafficMonitor 里面的操作分为 两类,一类是用于切 HttpClient 的 execute 方法,即注释1、2、3处所示的切面代码;一类是用于切 HttpConnection 的 openConnection 方法,对应的切面代码为注释4、5处。我们首先分析 HttpClient 的情况,这里最终 调用了 QHC 的 execute 方法进行处理

    public static HttpResponse execute(HttpClient client, HttpUriRequest request) throws IOException {
        return isTaskRunning()
                ? AopHttpClient.execute(client, request)
                : client.execute(request);
    }

这里又 继续调用了 AopHttpClient 的 execute 方法,代码如下所示:

    public static HttpResponse execute(HttpClient httpClient, HttpUriRequest request) throws IOException {
        NetInfo data = new NetInfo();
        // 1
        HttpResponse response = httpClient.execute(handleRequest(request, data));
        // 2
        handleResponse(response, data);
        return response;
    }

首先,在注释1处,调用了 handleRequest 处理请求数据,如下所示:

    private static HttpUriRequest handleRequest(HttpUriRequest request, NetInfo data) {
        data.setURL(request.getURI().toString());
        if (request instanceof HttpEntityEnclosingRequest) {
            HttpEntityEnclosingRequest entityRequest = (HttpEntityEnclosingRequest) request;
            if (entityRequest.getEntity() != null) {
                // 1、将请求实体使用 AopHttpRequestEntity 进行了封装
                entityRequest.setEntity(new AopHttpRequestEntity(entityRequest.getEntity(), data));
            }
            return (HttpUriRequest) entityRequest;
        }
        return request;
    }

可以看到,在注释1处,使用 AopHttpRequestEntity 对请求实体进行了封装,这里的目的主要是为了 便于使用封装实体中的 NetInfo 进行数据操作。
接着,在注释2处,将得到的响应信息进行了处理,这里的实现很简单,就是 使用 NetInfo 这个实体类将响应信息保存在了 ContentProvider 中。
我们接着分析下 HTTPConnection 的切面部分代码,如下所示:

    // 4
    @Pointcut("call(java.net.URLConnection openConnection()) && (target(url) && baseCondition())")
    public void URLOpenConnectionOne(URL url) {
    }

    // 5
    @Around("URLOpenConnectionOne(url)")
    public URLConnection URLOpenConnectionOneAdvice(URL url) throws IOException {
        return QURL.openConnection(url);
    }

可以看到,这里是 调用了 QURL 的 openConnection 方法进行处理。我们来看看它的实现代码:

    public static URLConnection openConnection(URL url) throws IOException {
        return isNetTaskRunning() ? AopURL.openConnection(url) : url.openConnection();
    }

这里 又调用了 AopURL 的 openConnection 方法,继续 看看它的实现:

    public static URLConnection openConnection(URL url) throws IOException {
        if (url == null) {
            return null;
        }
        return getAopConnection(url.openConnection());
    }
    
    private static URLConnection getAopConnection(URLConnection con) {
        if (con == null) {
            return null;
        }
        if (Env.DEBUG) {
            LogX.d(TAG, "AopURL", "getAopConnection in AopURL");
        }
        
        // 1
        if ((con instanceof HttpsURLConnection)) {
            return new AopHttpsURLConnection((HttpsURLConnection) con);
        }
        
        // 2
        if ((con instanceof HttpURLConnection)) {
            return new AopHttpURLConnection((HttpURLConnection) con);
        }
        return con;
    }

最终,在注释1处,会判断如果是 https 请求,则会使用 AopHttpsURLConnection 封装 con,如果是 http 请求,则使用 AopHttpURLConnection 进行封装。
AopHttpsURLConnection 的实现与它类似,仅仅是多加了 SSL 证书验证的部分。

  • 1)、在回调 getHeaderFields()、getInputStream()、getLastModified() 等一系列方法时会调用 inspectAndInstrumentResponse 方法把响应大小和状态码保存在 NetInfo 中。
  • 在回调 onInputstreamComplete()、onInputstreamError()等方法时,即请求完成或失败时,此时会直接调用 myData 的 end 方法将网络响应信息保存在 ContentProvider 中。

参考

深入探索编译插桩技术AspectJ

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

推荐阅读更多精彩内容