使用AspectJ来Hook你的Android代码

d9b366ef3669eb8ba660d45f0975e545.png

/   今日科技快讯   /

近日,工信部召开“十四五”信息通信业发展规划新闻发布会。会上,工信部信息通信发展司司长谢存表示,目前,我国已建成5G基站超过115万个,占全球70%以上,是全球规模最大、技术最先进的5G独立组网网络。全国所有地级市城区、超过97%的县城城区和40%的乡镇镇区实现5G网络覆盖;5G终端用户达到4.5亿户,占全球80%以上。

/   作者简介   /

本篇文章来自yangxiaobin的投稿,文章主要分享了AspectJ的相关知识以及对Aop的理解,相信会对大家有所帮助。同时也感谢作者贡献的精彩文章。

yangxiaobin的博客地址:

https://github.com/yangxiaobinhaoshuai

/   理解AOP   /

理解AOP

AOP是个老生常谈的概念了。作为一种编程范式,AOP的使用动机,多用于传统OOP程序设计无法很好的完成的场景。如:动态权限处理、安全策略应用、Trace/Log植入等。

这些场景的逻辑,大多以程序的最小单元进行批处理。OOP中,这个最小单元是Class对象,我们常借助继承、代理等模式来完成统一处理。但如果被处理的Class对象彼此之间没有任何规律和关联,比如,目标是所有的Class对象,通过任何OOP的模式对所有Class对象进行批处理都听起来不太容易。

相同的问题以 AOP 的视角看,情况大有不同。

不妨先来谈一下AOP中Aspect的理解。Aspect字面意思为“方面”,私以为这里可以理解为视角,看待程序的视角,不同视角下,程序的存在形式也不同。在编写源码期间,程序是源文件;在编译期间,可以是任何编译中间产物(AST,SymbolTable,ByteCode, etc);在运行时,可以是Runtime对象。在程序的不同生命周期,程序的形态不一样,我们对程序模块划分的方式也就不一样。

回到问题上,在AOP中,我们把视角切到编译期间,将每一个 “中间文件” 看作程序的最小模块单元,对这些单元进行批处理,插入自己的逻辑来完成需求。更具体些,只需Hook程序构建过程(多数构建工具支持自定义插件能力),找到目标文件,将符合“中间文件”语法的逻辑插入即可。相比OOP对所有Class对象应用处理的case,AOP在编译期间以文件为操作对象,将逻辑集中一处,更符合程序设计。

所以我们表达程序逻辑不仅限于写源码,可以在编译前生成模板代码,可以在编译环节中修改中间代码,可以在运行时代理替换Runtime对象,每种方式各有使用场景。

/   AsepctJ   /

简介

ApsectJ是JVM平台AOP的大名鼎鼎实现工具之一。其功能基本覆盖所有对JVM平台对 AOP 的使用需求,完全兼容Java语言,但不仅限于Java语言。托管于Eclipse,发展至今已10余年,仍在随Java版本更迭。

使用

概念

首先介绍 AspectJ 的几个概念,熟悉Spring的同学应该对这些概念不会感到陌生。首先是最为基本的:

Join Point : 程序流中连接点

可以理解为AspectJ将Java程序执行 看作一系列 程序组成元素 的流,每个连接点是AspectJ可以插入逻辑的地方。这些连接点可以是一个方法调用前后,可以是异常抛出时,也可以是某个成员变量被修改时。所以Join Point可以理解为程序执行时某些特定的时机。

其他AspectJ元素

674ed4a40e0710adf0a0072a53df88fe.png

其中,PointCut和JoinPoint的概念很容易混淆。Stackoveflow有个比喻很恰当,把 JoinPoint比作餐厅菜单中所有的菜品,在用餐时,你可以有机会点任意菜品,但你显然不会把左右菜品都点一遍。当你下单后,你所选择的菜品即是PointCut。所以,PointCut是JoinPoint的特定子集。我个人更喜欢把PointCut当作程序执行的“横截面”来理解,将AspectJ理解为庖丁的解牛刀,程序即为牛整体,解剖的器官截面即为 PointCut。因为在PointCut里可以获取程序变量在当时的值,好比能够一览整个截面一般。

b3d2d1ec7d92ca8913d2fbb5f1c5d1cb.png

JoinPoint与 PointCut 示意图

inter-type delarations比较少用,用于修改类结构,如增加Field,method或者修改继承关系。

就几种AspectJ结构体作用总结来说,PointCut和Advice动态地影响程序执行,而inter-type declarations静态地修改Class结构,Aspect则将几种修改程序的结构体封装,以便更好的复用和聚合切面逻辑。Aspect就像OOP中封装对象行为和属性的Class对象一样,将动态和静态影响程序行为的功能封装在内,对外使用。

API使用

关于API使用这里就简单提一下,详情移步官方API文档 。地址如下:

https://www.eclipse.org/aspectj/doc/released/progguide/starting-aspectj.html#the-dynamic-join-point-model

AspectJ支持两套API ,一套为使用AJC(AspectJ Compile)识别的语法,编写后缀为.aj的文件。拿最常见的Method call PointCut为例。

AJC语法

声明PointCut

call(void Point.setX(int))

以上为一个method call PointCut的声明,call关键字表示这是一个method call类型的JoinPoint,括号中的参数是一个Java方法签名,是寻找特定method call JoinPoint的匹配条件。当然也可以匹配多个方法,甚至可以为这系列的method call joinpoints命名。

// 匹配多个 method 签名需逻辑运算符连接, 逻辑运算法含义与程序中一样
pointcut move():
    call(void FigureElement.setXY(int,int)) ||
    call(void Point.setX(int))              ||
    call(void Point.setY(int))              ||
    call(void Line.setP1(Point))            ||
    // 方法签名匹配同样支持通配符
    call(void Figure.make*(..))             ||
    call(void Line.setP2(Point));

使用PointCut

通过Advice来使用PointCut:

before(): move() {
    System.out.println("about to move");
}

after() returning: move() {
    System.out.println("just successfully moved");
}

相信以上的代码含义是自解释的。

注解语法

// 表示该类包含 aspect constructs;
   @Aspect
   public class Foo {

       // PointCut 声明
       @Pointcut("call(* java.util.List.*(..))") // must qualify
       void listOperation() {}

       @Pointcut("call(* java.util..*(..))")
       void anyUtilityCall() {}

       @After("execution(private * *(..))")
       void doBefore() {}
     }

虽然这是个类声明,但可以理解为一张用Java语法来编写的信息表。就声明PointCut用途来说,用aj语法和注解语法区别无他,但注解语法有两点好处:

  1.  这是个彻头彻尾的Java Pojo,在没有ajc的情况下这个类也可以正常参与编译,方便我们将源代码编译和处理AspectJ元素过程分离。

  2.  对于熟悉Java语法的程序员不会引入新的学习成本。

并且大多数对AspectJ的运用都是以注解的方式,基于这两点优势,后面我们对AspectJ的运用也会选择AspectJ的注解形式。

纵观AspectJ API,其实就是在声明PointCut,而寻找声明的JoinPoint并织入逻辑的功能由AspectJ Compiler(AJC)来完成。

/   AspectJ原理   /

AspectJ详细的织入过程细节不在这里讨论,只是简单介绍一下,为后面自定义工具做铺垫。

总览AspectJ使用,我们不难发现AspectJ和Groovy很像,有自身语法,能够100%支持Java语法。同样,AspectJ的魔法也发生在名为“AJC”的编译器上,在编译环节将我们声明的Aspect construct织入程序执行流程。

跟AJC相关的逻辑体现在aspectjrt.jar中,其中包含可以直接在命令行运行的 org.aspectj.tools.ajc.Main.run()等系列API,该API也是ajc命令的底层实现。Ajc所做的事情大体分三个步骤:

  1. 编译.java源文件,生成对应class;

  2. 对 Aspect constrcut 的字节码进行拓展,增加 “织入”过程所需的方法和属性;

  3. 寻找PointCut,进行织入;

源文件编译由org.aspectj.org.eclipse.jdt.internal.compiler.Compiler来完成,同javac类似,其中包括对classFile进行注解处理,解析,构建AST,等系列编译操作,最终生成字节码。对Aspect construts的增强处理,可以有两处选择:

  1. 在compile之前处理源文件形式的aspect constructs;

  2. 在compile之后处理class形式的aspect constructs。

其实处理过程无非是生成一些模板属性和方法。支持多种形式aspect constructs处理大大丰富了AspectJ的使用方式。最后的织入处理,AspectJ采用ClassVisitor处理模式,操作字节码对class文件进行插桩。

9f20ef641bf78a251aac038be9dee3f1.png

CLI编译简介

安装ajc

brew install aspectj

AJC的使用方式和javac命令很像,只是有部分参数不一样,详情移步官方文档,地址如下:

https://www.eclipse.org/aspectj/doc/released/devguide/ajc-ref.html

这里我们选择常用参数了解下:

0aa1f642397ee90451ca3fb7c285d04f.png

留意下-inpath这是Ajc post-compile的关键,后面我们会利用它。

比如:| - me/yangxiaobin/demo/KtAspect.kt | - me/yangxiaobin/demo/Main.kt

ajc -cp java/rt.jar:kotlinstd.jar:aspectjrt.jar  -sourceroots . -d outout/dist/ -1.8 .

亦或者:

// 编译源文件成 classes 到 dist
kotlinc . -d output/dist
// 只用 classes 做织入行为
ajc -cp jre/rt.jar:kotlinstd.jar:aspectjrt.jar -inpath output/dist -1.8 -d output/dist .

/   AspectJ In Java Android   /

为了利用AJC的Post-Compile的优势,我们选择借助AspectJ的Compiler API ,其包含在aspectj-tools工件中,将编译后的Classes做织入处理。

以Gradle Plugin工具为例,伪逻辑如下:

// 通常会是 KotlinCompile or JavaCompile 
val compiles = gradleTaskGraph.findInstance<AbstractCompile> 
compiles.foreach { c -> c.doLast ( doAjc ) }

fun doAjc () {
  val args = arrayof( "-1.8, -cp, jre/rt.jar:kotlinstd.jar:aspectjrt.jar, -sourceroot, src/main/java" ...)
  org.aspectj.tools.ajc.Main.main(args)
}

值得注意的点:

  1. 不同的Compile Task的dist目录不一样,需要把不同目录的Classes汇总作为输入

  2. Android平台可以借助Transform这个Task,在transform input目录中获取所有 Classes和jars

  3. 如果需要织入三方依赖,需要提供能够编译该Jar的完整classpath,仅仅compile classpath可能不够

比如:项目依赖了google的constraintlayout。而Project source code的compile classpath中仅包含constraintlayout-api.jar。但如果想对constraintlayout进行织入,需要能够编译constraintlayout整个工件的classpath。显然只有api.jar是不够的,还需要在runtimeclasspath中找到runtime.jar。

16f196fba7d228ca1280e1c7c9141328.png

/   用途   /

  • Log

  • Trace

  • 自动化工具,如:自动页面pv, 自动控件点击

经常做性能优化和自动工具同学应该对项目AOP的能力比较依赖。相比其他流行AOP工具,如ASM、Javasist,AspectJ学习成本最低,又能以编辑源码的方式来完成切面代码,Ajc post-compile又支持所有JVM语言,综上,AspectJ为AOP工具不二之选。

Demo详见androidapp,地址为:

https://github.com/yangxiaobinhaoshuai/gradle-plugin-template/blob/master/androidapp

推荐阅读:

我的新书,《第一行代码 第3版》已出版!

Activity Result API详解

边学边玩,来搞个2048小游戏吧

欢迎关注我的公众号

学习技术或投稿

fa4425cfe8cc4cceee431b9964824d96.png

d6295926341f2d9a1e35145264330f94.png

长按上图,识别图中二维码即可关注

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值