ARouter系列文章:
“终于懂了” 系列:组件化框架 ARouter 完全解析(一)原理全解
“终于懂了” 系列:组件化框架 ARouter 完全解析(二)APT—帮助类生成
“终于懂了” 系列:组件化框架 ARouter 完全解析(三)AGP/Transform—动态代码注入
在本系列前两篇中,介绍了ARouter的核心原理、ARouter中使用的APT技术,了解了帮助类的作用和编译期生成帮助类的原理。其中原理篇中提到了AGP方式加载路由——在编译时进行扫描并动态在LogisticsCenter#loadRouterMap()中插入了代码。本篇就来介绍ARouter是如何搜集帮助类信息、如何动态注入代码,以及涉及到的AGP、Trasform、ASM 。
一、Arouter中的核心技术
1.1 Gradle
先来了解Android开发必定要接触到的Gradle:
- 一个自动化构建框架
- 在Android开发中,用来编译打包输出apk,还可以用来添加三方依赖
Gradle官网中的介绍:
Gradle是一种开源构建自动化工具,足够灵活,可以构建几乎任何类型的软件。Gradle对您要构建什么或如何构建它几乎没有任何限制。
也就是说,Gradle这个构建工具,可以被用来构建任何东西,具体如何构建也是由使用者制定。 在Android领域,AndroidStudio中的项目默认会引入Gradle,就是被用来构建Android应用包——apk文件。
那么Google是如何制定Apk构建流程的呢? 通过上面了解知道,AndroidStudio中只有Gradle还不够,还得告诉它如何构建。我们知道Apk打包流程包括很多环节:源码文件经过JavaCompiler转为class文件、class经过dex操作变为dex文件、dex和资源等打包成apk、签名 等等一系列步骤,这就需要Google使用Gradle把这些操作都串联起来。
在项目根目录的build.gradle文件中的这一句,就会引入「Google使用Gradle制定的这一系列打包流程」,也就是Google自定义的各种Gradle 的任务。每个任务负责某个具体的事情,按照依赖关系排好执行顺序,先后执行,最后输出Apk文件。
在AndroidStudio的Terminal中,输入打包命令:./gradlew app:assembleDebug --console=plain
,输出如下:
:app:preBuild UP-TO-DATE
...
:app:compileDebugJavaWithJavac
...
:app:transformClassesWithDexBuilderForDebug
...
:app:packageDebug
:app:assembleDebug
这些就是Google官方使用Gradle定义的相互有依赖关系的各种任务,这些任务执行完就完成了Apk的打包。
这些任务是如何被创建的呢?在app模块和arouter-api模块的build.gradle文件中,第一句分别是
//app的build.gradle
apply plugin: 'com.android.application'
//arouter-api的build.gradle
apply plugin: 'com.android.library'
意思是,app引入了名为com.android.application
的gradle插件、arouter-api引入的是名为com.android.library
的gradle插件。上面介绍的各种任务就是在这两种插件中创建的,引入了插件,也就引入了插件对应的任务。
定义了各种gradle任务,为啥还要定义gradle插件呢?一是可以对任务进行管理,例如排好任务的依赖和执行顺序;二是可以复用,模块引入某个插件,就拥有了对应的能力。即gradle插件就是 将构建逻辑的可重用部分封装起来,可以应用到不同的项目和构建中。
gradle中,除了任务(Task)、插件(Plugin),还有一个概念——Project,共同组成了gradle的核心内容:
- Project:Gradle工程,Project包含一个构建脚本,通常名为
build.gradle
。 构建脚本为该
Project定义Task、依赖项、plugin和其他配置。一个Android工程以及每个Android module都是一个Project (都有一个build.gradle文件),一个build.gradle文件编译后就是一个Project对象 - Task:Gradle任务,包含执行某些工作的逻辑,Task本身包括:
- 动作(Actions):具体某中的操作,例如复制文件或编译源代码
- 输入(Inputs):操作使用的值、文件、目录
- 输出(Outputs):操作修改或生成的文件、目录
- Plugin:Gradle插件,用于创建和封装Task,介入编译构建过重要达到扩展功能目的。Plugin使得相同逻辑和配置可以被多个Project复用
再来看下Gradle的生命周期,有三个阶段:
- 初始化:确定哪些Project将参与构建。即读取settings.gradle文件中的include的所有模块
- 配置:确认参与构建的所有任务及其顺序。读取参与构建的Project的build.gradle文件,确定需要运行哪些Task以及确定Task间依赖关系和执行顺序(包含直接在build.gradle中定义的Task以及引入的Plugin中定义的Task)
- 执行:运行在配置阶段确定的Task,这里是真正执行所有的打包操作
以上对Gradle的基本认知非常重要,足以理解本篇内容。至于Gradle脚本编写、Task和Plugin详细用法这种固定知识点可自行到Gradle官网学习即可,这里不再搬运。
1.2 Android Gradle Plugin
AGP(Android Gradle Plugin),即 Android Gradle 插件,是Android开发中使用的Gradle插件。
上面提到的,在项目根目录的build.gradle文件中引入了com.android.tools.build:gradle:4.0.0
,也就引入了 Google官方写好的两个Gradle插件:com.android.application
、com.android.library
,这两个插件内部会创建各种Task用于完成Apk编译打包流程。
来看一个形象的比喻:Gradle编译打包流程和工厂通过自动化流水线来生产流程:
工厂(玩具工厂等) | 开发工具(AndroidStudio等) |
---|---|
厂长 | 程序员 |
流水线 | gradle |
流水线中的一个机器 | gradle中的一个插件 |
原材料 | java/kotln文件、资源文件等 |
各工作台的操作:清洗、组装、包装 | 打包流程:javac、APT、转dex、打包apk |
流水线新加入一个机器:给原材料贴标签 | gradle中新增一个APG:遍历dex并动态插入代码 |
Gradle就像是工厂的流水线一样,负责定义流程和规则,而具体的工作都是通过插件(及其包含的Task)实现的。如果想在流水线上增加一个操作,例如给原材料贴标签,那么就在最开头的位置增加一个贴标签的机器即可。对应的,想要在编译过程中动态插入代码,我们就需要新增一个AGP。
原本只有Google官方定义好的AGP,现在需要做的就是我们自定义一个插件,并且插在编译过程中合适的位置上。这就是本篇中要介绍的重点内容——ARouter用来在编译阶段搜集帮助类信息/动态注入代码的AGP是如何实现的。
1.3 Transform
搜集帮助类信息、动态注入代码,要在编译过程的什么时机执行呢? 这个时机要能够搜集到所有帮助类(包括依赖的三方AAR中的)并且还可以向目标文件中插入代码。
在官方定义的AGP中提供了这样一个时机,如下打包流程图中箭头位置:
在class文件转为dex文件之前,这个节点可以拿到参与构建的所有class文件,包括本项目java文件/kotln文件经javac生产的class文件和依赖的第三方库中的class文件。
代码上如何使用拿到这个时机呢?Android官方的AGP中提供了一个API——Transform:
我们需要自定义Transform,在Transform的transform
方法中就可以获取到所有的class文件。
transform
方法被调用的时机就是在class被打进dex文件之前- 需要在创建的gradle插件中注册自定义的Transform
- 事实上每添加一个Transform,在Grade的配置阶段就会创建一个gradle Task
显然,在Transform的transform
方法中,就可以拿到所有的class文件,我们关心的是所有的帮助类和LogisticsCenter
类。
1.4 ASM
在Transform的中拿到所有class文件后,如何识别帮助类和LogisticsCenter
类呢?,识别后如何在LogisticsCenter
类中插入代码呢? 如下图这样:
上图是class文件反编译后的样子,我们可以看懂。 但如果使用文本软件直接打开class文件,基本就是看不懂的十六进制内容:
如果想要识别出是哪个类、或是要向中间某个位置插入一段代码,这是基本不可能了。我们知道class文件又称为字节码文件,可以使用javap命令把class文件转为大概能看懂的字节码文件(或者直接使用AndroidStudio插件ASM Bytecode Viewer
来查看):
右侧就是字节码指令以及Class文件版本号、常量池、访问标识、变量表、指令代码行号表等信息(可以参考字节码,查看每个指令的含义),这里我们肉眼可以识别类名和方法名。
在Transform的中拿到了class文件后,想要识别类信息并且还能修改内容,这个在代码上如何实现先呢?方法就是ASM。
ASM:字节码操作框架,是一种基于java字节码层面的代码分析和修改工具,ASM的目标是生成、转换、分析已编译的java class文件,可使用ASM工具读、写、转换JVM指令集。通俗点讲就是来处理javac编译之后的class文件。
ASM通过访问者模式依次遍历class字节码中的各个部分(属性、属性、注解等),并不断回调到上层(这有点像SAX解析xml的过程),上层可在业务关心的某个访问点,修改原有逻辑。之所以可以这么做,是因为java字节码是按照严格的JVM规范生成的二进制字节流,ASM是照这个规范对java字节码的一次解释,将晦涩难懂的字节码背后对应的JVM指令一条条的转换成ASM API。其中核心类如下:
- ClassReader:用于读取已经编译好的class文件,读取到内容(属性/方法等)后会调用ClassWriter对应的方法
- ClassWriter:是ClassVisitor的子类,主要负责将ClassVisitor处理好后的class转换成byte数组,有了byte数组就能通过OutputStream写入文件,也就完成了对class文件的修改
- ClassVisitor:class文件访问者,可用于修改类型西,如修改类名、属性、方法,也可生成类的class文件。对于字节码文件中不同的区域有不同的Visitor:用于访问方法的 MethodVisitor、用于访问类属性的FieldVisitor、用于访问注解的AnnotationVisitor等
- MethodVisitor:是ASM中最为重要的类,它是对帧栈中「操作数栈」进行操作的核心类,无论实现什么功能都离不开对操作数栈的操作
使用ASM识别一个类,具体怎么操作呢?
/**
* 扫描class
* @param inputStream class文件的输入流(从transform中获取的)
*/
static void scanClass(InputStream inputStream) {
ClassReader cr = new ClassReader(inputStream)
ClassWriter cw = new ClassWriter(cr, 0)
ScanClassVisitor cv = new ScanClassVisitor(Opcodes.ASM5, cw)
cr.accept(cv, ClassReader.EXPAND_FRAMES)
inputStream.close()
}
static class ScanClassVisitor extends ClassVisitor {
...
//该方法主要提供对java版本,类权限访问符、全限定名、泛型、父类、接口相关信息
void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
super.visit(version, access, name, signature, superName, interfaces)
...
}
}
以上是读取/扫描 class文件的固定写法,重点是自定的ClassVisitor——ScanClassVisitor类,在它的visit()方法中可以获取到 类名、类实现的接口等等。通过对比 类名、接口等信息,就能确认是否是我们要找的帮助类了。
使用ASM在类方法插入代码,具体怎么操作呢?
/**
* 修改class方法
* @param inputStream class文件的输入流(从transform中获取的)
*/
private byte[