C语言编译插桩,深度解析编译插桩技术(二)AspectJ

前言

成为一名优秀的Android开发,需要一份完备的知识体系,在这里,让我们一起成长为自己所想的那样~。

现如今,编译插桩技术已经深入 Android 开发中的各个领域,而 AOP 技术正是一种高效实现插桩的模式,它的出现正好给处于黑暗中的我们带来了光明,极大地解决了传统开发过程中的一些痛点,而 AspectJ 作为一套基于 Java 语言面向切面的扩展设计规范,能够赋予我们新的能力。在这篇文章中我们将来学习如何使用 AspectJ 来进行插桩。本篇内容如下所示:

1)、编译插桩技术的分类与应用场景。

2)、AspectJ 的优势与局限性。

3)、AspectJ 核心语法简介。

4)、AspectJX 实战。

5)、使用 AspectJX 打造自己的性能监控框架。

6)、总结。

面向切面的程序设计 (aspect-oriented programming (AOP)) 吸引了很多开发者的目光, 但是如何在编码中有效地实现这一套设计概念却并不简单,幸运的是,早在 2003 年,一套基于 Java 语言面向切面的扩展设计:AspectJ 诞生了。

不同与传统的 OOP 编程,AspectJ (即 AOP) 的独特之处在于 发现那些使用传统编程方法无法处理得很好的问题。 例如一个要在某些应用中实施安全策略的问题。安全性是贯穿于系统所有模块间的问题,而且每一个模块都必须要添加安全性才能保证整个应用的安全性,并且安全性模块自身也需要安全性,很明显这里的 安全策略的实施问题就是一个横切关注点,使用传统的编程解决此问题非常的困难而且容易产生差错,这正是 AOP 发挥作用的时候了。

传统的面向对象编程中,每个单元就是一个类,而 类似于安全性这方面的问题,它们通常不能集中在一个类中处理,因为它们横跨多个类,这就导致了代码无法重用,它们是不可靠和不可继承的,这样的编程方式使得可维护性差而且产生了大量的代码冗余,这是我们所不愿意看到的。

而面向切面编程的出现正好给处于黑暗中的我们带来了光明,它针对于这些横切关注点进行处理,就似面向对象编程处理一般的关注点一样。

在我们继续深入 AOP 编程之前,我们有必要先来看看当前编译插桩技术的分类与应用场景。这样能让我们 从更高的纬度上去理解各个技术点之间的关联与作用。

一、编译插桩技术的分类与应用场景

编译插桩技术具体可以分为两类,如下所示:

1)、APT(Annotation Process Tools) :用于生成 Java 代码。

2)、AOP(Aspect Oriented Programming):用于操作字节码。

下面👇,我们分别来详细介绍下它们的作用。

1、APT(Annotation Process Tools)

总所周知,ButterKnife、Dagger、GreenDao、Protocol Buffers 这些常用的注解生成框架都会在编译过程中生成代码。而 使用 AndroidAnnotation 结合 APT 技术 来生成代码的时机,是在编译最开始的时候介入的。但是 AOP 是在编译完成后生成 dex 文件之前的时候,直接通过修改 .class 文件的方式,来直接添加或者修改代码逻辑的。

使用 APT 技术生成 Java 代码的方式具有如下 两方面 的优势:

1)、隔离了框架复杂的内部实现,使得开发更加地简单高效。

2)、大大减少了手工重复的工作量,降低了开发时出错的机率。

2、AOP(Aspect Oriented Programming)

而对于操作字节码的方式来说,一般都在 代码监控、代码修改、代码分析 这三个场景有着很广泛的应用。

相对于 Java 代码生成的方式,操作字节码的方式有如下 特点:

1)、应用场景更广。

2)、功能更加强大。

3)、使用复杂度较高。

此外,我们不仅可以操作 .class 文件的 Java 字节码,也可以操作 .dex 文件的 Dalvik 字节码。下面我们就来大致了解下在以上三类场景中编译插桩技术具体是如何应用的。

1、代码监控

编译插桩技术除了 不能够实现耗电监控,它能够实现各式各样的性能监控,例如:网络数据监控、耗时方法监控、大图监控、线程监控 等等。

譬如 网络数据监控 的实现,就是在 网络层通过 hook 网络库方法 和 自动化注入拦截器的形式,实现网络请求的全过程监控,包括获取握手时长,首包时间,DNS 耗时,网络耗时等各个网络阶段的信息。

实现了对网络请求过程的监控之后,我们便可以 对整个网络过程的数据表现进行详细地分析,找到网络层面性能的问题点,并做出针对性地优化措施。例如针对于 网络错误率偏高 的问题,我们可以采取以下几方面的措施,如下所示:

1)、使用 HttpDNS。

2)、将错误日志同步 CDN。

3)、CDN 调度链路优化。

2、代码修改

用编译插桩技术来实现代码修改的场景非常之多,而使用最为频繁的场景具体可细分为为如下四种:

1)、实现无痕埋点:**如 网易HubbleData之Android无埋点实践、[51 信用卡 Android 自动埋点实践

2)、统一处理点击抖动:编译阶段统一 hook android.view.View.OnClickListener#onClick() 方法,来实现一个快速点击无效的防抖动效果,这样便能高效、无侵入性地统一解决客户端快速点击多次导致频繁响应的问题。

3)、第三方 SDK 的容灾处理:我们可以在上线前临时修改或者 hook 第三方 SDK 的方法,做到快速容灾上线。

4)、实现热修复框架:我们可以在 Gradle 进行自动化构建的时候,即在 Java 源码编译完成之后,生成 dex 文件之前进行插桩,而插桩的作用是在每个方法执行时先去根据自己方法的签名寻找是否有自己对应的 patch 方法,如果有,执行 patch 方法;如果没有,则执行自己原有的逻辑。

3、代码分析

例如 Findbugs 等三方的代码检查工具里面的 自定义代码检查 也使用了编译插桩技术,利用它我们可以找出 不合理的 Hanlder 使用、new Thread 调用、敏感权限调用 等等一系列编码问题。

二、AspectJ 的优势与局限性

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

而 AspectJ 作为 Java 中流行的 AOP(aspect-oriented programming) 编程扩展框架,其内部使用的是 BCEL框架 来完成其功能。下面,我们就来了解下 AspectJ 具备哪些优势。

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 可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合性降低,提高程序的可重用性,同时大大提高了开发效率。因此 AOP 的优势可总结为如下 两点:

1)、无侵入性。

2)、修改方便。

此外,AOP 不同于 OOP 将问题划分到单个模块之中,它把 涉及到众多模块的同一类问题进行了统一处理。比如我们可以设计两个切面,一个是用于处理 App 中所有模块的日志输出功能,另外一个则是用于处理 App 中一些特殊函数调用的权限检查。

下面👇,我们就来看看要掌握 AspectJ 的使用,我们需要了解的一些 核心概念。

1、横切关注点

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

2、切面(Aspect)

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

3、连接点(JoinPoint)

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

4、切入点(PointCut)

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

5、通知(Advice)

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

AspectJ 作为一种基于 Java 语言实现的一套面向切面程序设计规范。它向 Java 中加入了 连接点(Join Point) 这个新概念,其实它也只是现存的一个 Java 概 念的名称而已。它向 Java 语言中加入了少许新结构,譬如 切入点(pointcut)、通知(Advice)、类型间声明(Inter-type declaration) 和 切面(Aspect)。切入点和通知动态地影响程序流程,类型间声明则是静态的影响程序的类等级结构,而切面则是对所有这些新结构的封装。

对于 AsepctJ 中的各个核心概念来说,其 连接点就恰如程序流中适当的一点。而切入点收集特定的连接点集合和在这些点中的值。一个通知则是当一个连接点到达时执行的代码,这些都是 AspectJ 的动态部分。其实连接点就好比是 程序中那一条一条的语句,而切入点就是特定一条语句处设置的一个断点,它收集了断点处程序栈的信息,而通知就是在这个断点前后想要加入的程序代码。

此外,AspectJ 中也有许多不同种类的类型间声明,这就允许程序员修改程序的静态结构、名称、类的成员以及类之间的关系。 AspectJ 中的切面是横切关注点的模块单元。它们的行为与 Java 语言中的类很象,但是切面 还封装了切入点、通知以及类型间声明。

在 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 中所有方法的耗时

那么,我们如何利用它统计 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));

}

}

复制代码

需要注意的是࿰

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值