使用AspectJ来Hook你的Android代码

Posted 冬天的毛毛雨

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了使用AspectJ来Hook你的Android代码相关的知识,希望对你有一定的参考价值。

理解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元素

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

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文件进行插桩。

CLI编译简介

安装ajc

brew install aspectj 

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

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

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

留意下-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。

用途

  • 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

以上是关于使用AspectJ来Hook你的Android代码的主要内容,如果未能解决你的问题,请参考以下文章

使用AspectJ来Hook你的Android代码

AOP之AspectJ在android中的解读

AOP之AspectJ在android中的解读

Android AOP编程之AspectJ

Android AOP编程之AspectJ

Android AOP编程之AspectJ