0x00 前言
什么是aspectd?aspectd是闲鱼针对dart的AOP开源框架。https://github.com/alibaba-flutter/aspectd.git
阅读本文你将得到什么?
- 掌握aspectd的环境搭建,并如何在本地成功运行aspectd的demo
- 掌握有关aop的基础概念
- 了解aspectd的基础用法和原理
0x01 准备
1.1 开发环境
aspectd的环境搭建需要flutter源码、aspectd源码和dart源码,并需要在系统中设置相应的全局环境变量。
1.1.1 flutter环境
下载flutter源码:
git clone https://github.com/flutter/flutter.git
1.1.2 aspectd下载
下载aspectd源码:
git clone https://github.com/alibaba-flutter/aspectd.git
1.1.3 环境变量
配置flutter镜像、本地flutter源码地址、flutter bin目录、dart bin目录:
export PUB_HOSTED_URL=https://pub.flutter-io.cn
export FLUTTER_STORAGE_BASE_URL=https://storage.flutter-io.cn
export PATH_TO_FLUTTER_GIT_DIRECTORY=/Users/Ivonhoe/Flutter/flutter
export PATH=$PATH_TO_FLUTTER_GIT_DIRECTORY/bin:$PATH
export PATH=$PATH_TO_FLUTTER_GIT_DIRECTORY/bin/cache/dart-sdk/bin:$PATH
1.2 安装aspectd
aspectd需要
1.切换到flutter的git目录:
cd ${path-for-git-flutter}
2.将aspectd源码中的git patch文件合并到flutter源码工程中,合并git patch:
git apply --3way ~/Github/aspectd/0001-aspectd.patch
3.删除原有的的flutter编译工具:
rm bin/cache/flutter_tools.stamp
4.重新构建新的flutter编译工具:
flutter doctor -v
1.3 运行
到aspectd源码目录的example目录下执行:
flutter run --debug --verbose
如果你能一次运行成功并aspectd生效,请直接跳转到第二章!
1.4 aspectd编译不过或demo没有效果
编译不过或运行demo没有打印出想要的日志是aspectd使用时最常见的问题。aspectd的基本原理实际上是使用了dart对虚拟语法树操作的api,通过对flutter dill文件进行虚拟语法树遍历,完成对dill文件的转换,进而实现对dart的切面操作。所以在aspectd的编译上需要依赖dart源码中的kernal
和front_end
,可通过查看aspectd源码根目录中的pubspec.yaml
查看依赖库和对应的ref。
dependency_overrides:
kernel:
git:
url: https://github.com/dart-lang/sdk.git
ref: 5e39817ec7ab7f56f381c244d105c7e40913a3e0
path: pkg/kernel
front_end:
git:
url: https://github.com/dart-lang/sdk.git
ref: 5e39817ec7ab7f56f381c244d105c7e40913a3e0
path: pkg/front_end
在1.2步骤中,使用git patch命令修改flutter源码引入了aspectd.dart
文件,该文件做的核心操作就包括下载aspectd的依赖库、编译aspectd.dart.snapshot和根据注解内容使用aspect.dart.snapshot执行具体的dill transform操作。所以,aspectd是否生效的两个关键点是aspectd依赖库是否下载成功和aspectd.snapshot文件是否编译成功。
因为aspect使用依赖github源码指定ref的方式依赖kenerl和front_end库,这个过程需要下载github上dart-lang的所有源码(约900M左右),在国内的网络环境下很难做到一次成功,这里分享一个绕过因网络不稳定问题导致aspectd不生效的方法。
手动下载dart源码,
git clone https://github.com/dart-lang/sdk.git
将dart源码切换到aspectd项目中
pubspec.yaml
指定的ref上,如上例中,可执行git checkout 5e39817ec7ab7f56f381c244d105c7e40913a3e0
-
将aspect对github源码的依赖改成对本地源码的依赖
手动编译aspect.dart.snapshot(在aspectd根目录中)
dart --snapshot=snapshot/aspectd.dart.snapshot tool/starter.dart
-
修改flutter源码中的aspectd.dart,强制指定aspect.dart.snapshot的目录。
删除flutter_tools.stamp重新编译运行
flutter run --debug -v
即可生效
1.5 常见问题解决
等待另一个flutter命令释放锁
Waiting for another flutter command to release the startup lock...
解决方法,将bin/cache下的lockfile删除后重新执行命令
rm ${path-for-git-flutter}/bin/cache/lockfile
如何使用命令行编译工程
debug版本:flutter run --debug --verbose
release版本:flutter run --release --verbose
pub命令是什么?
flutter pub get
pub是dart提供的包管理工具,在flutter源码中的flutter/bin/cache/dart-sdk/bin/pub
目录下有pub可执行文件,想要单独执行pub命令可讲该目录加入到系统的环境变量中
相当于android gradle的gradle sync
相当于ios pod中的pod install
相当于js npm中的npm install
0x02 aspectd的注解
2.1 @pragma(‘vm:entry-point’)
在AOT变一下,如果不能被应用主入口(main)最终可能调用到,那么将被视为无用代码而被丢弃掉。AOP代码因为其注入逻辑的无侵入性,所以不会被main调用,因为使用此注解告诉编译器不要丢弃这段逻辑。
2.2 @Aspect
Aspect注解可以使得像asepctd源码example中aop_impl.dart
这样的AOP实现类被方便的识别和提取,也可以起到方便开关的作用,如果想禁用掉这段AOP逻辑,移除@Aspect注解即可
2.3 @Call、@Execute、@Inject
在介绍这几个注解之前需要理解关于AOP的几个概念,aspectd官方介绍文档对aspectd的说明引入了很对对aop设计的说明,比如什么是Advice?什么是Before\Around\After?如果对这些概念没有预先的概念,读aspectd的文档是一头雾水的,至少我是这样!
2.3.1 什么是Joint Point(连接点)
能够插入切面的一个点。这个点可以是类的某个方法调用前、调用后、方法抛出异常后等。切面代码可以利用这些点插入到应用的正常流程之中,并添加行为
2.3.2 什么是Pointcut(切点)
指定一个通知将被引发的一系列连接点的集合。切点是连接点规则的描述。切点和连接点不是一对一的关系,一个切点匹配多个连接点
2.3.3 什么是Target Object(目标对象)
包含连接点的对象
2.3.3 什么是Advice(通知)
在特定的连接点,AOP框架执行的动作。通知有常见的几种类型:
- 前置通知Before:在目标方法被调用之前调用通知功能
- 后置通知After:目标方法完成之后调用通知,无论该方法是否发生异常
- 后置返回通知After-returning:在目标方法成功执行之后调用通知
- 后置异常通知After-throwing:在目标方法抛出异常后调用通知
- 环绕通知Around:通知包裹了被通知的方法,在被通知的方法调用之前和调用之后执行自定义的行为
2.3.4 @Call、@Execute、@Inject
aspectd只有一种统一的通知类型,就是Around。具体分为两种注解,分别是@Call和@Execute,这两种注解表达的PointCut都是通过包装原有方法实现的。差别是,@Call的PointCut是调用的地方,并不会修改原始方法的内部。@Execute会修改原有方法的内部。举个例子,分别使用@Call和@Execute对test
方法执行切面操作
void test(){
print("print hello world!")
}
void main(){
test();
}
@Call表达注解的实际代码会变成这样:
void test(){
print("print hello world!")
}
void invokeCall(){
// to do somethings
test();
// to do somethings
}
void main(){
aop:invokeCall()
}
@Execute表达注解的实际代码会变成这样:
void invokeExecutor(){
// to do somethings
print("print hello world!")
}
void test(){
invokeExecutor();
}
void main(){
test();
}
而@Inject相对于Call/Executor而言,多了一个lineNum的参数,用于指定插入逻辑的具体行号。用于在具体方法中间插入处理逻辑。
AOP 的理解是,在做日志、埋点追踪、安全检查时使用 AOP 可以在不扰乱正常业务代码的情况下添加想要的功能。
另外,在闲鱼团队的介绍文章中也提到,基于 AOP 可以对 flutter 执行非侵入式框架改造,这样可以实现例如自动化录制回放的功能。
框架整体理念
AspectD 通过在编译期操纵 AST 抽象语法树,达到对指定函数、方法添加调用监视和增加额外逻辑的目的。
引入 AspectD 将对项目结构产生一定改变,同时也要修改 flutter_tools 的少量源码。引入完成后,
运行项目的方式及入口与引入前一致。
AspectD 对项目结构的具体影响
假设引入前项目结构是
└─example
│ pubspec.yaml
│
├─android
├─ios
└─lib
main.dart // 这是项目原有的入口
引入 AOP 之后的结构是
└─example
│ pubspec.yaml
│
├─android
├─aop
│ │ pubspec.yaml
│ │
│ ├─android
│ ├─ios
│ └─lib
│ aop.dart
│
├─ios
└─lib
main.dart // 这依然是项目的入口
可以看到,引入 aop 相当于新引入了一个包(package)。AspectD 约定此包名为 aop(也支持自定义),此包的入口约定为 aop.dart。
这样实际上就把 aop 中的 logging 或者其他各种逻辑与业务逻辑不光从逻辑上,而且从物理上隔离开了。
AspectD 对 flutter_tools 的影响
AspectD 主要是通过在 build 环节增加对 AST 的操作所实现,所以在我们使用 flutter 编译项目时需要添加额外步骤。
这通过修改 flutter_tools 包来实现。主要在这个包中的 build 函数中增加了 AspectD 编译相关的调用(hook)。
笔者觉得这样对标准工具的相关的修改算是比较小,可以接受。
小结
闲鱼团队提供的这款工具可以实现 AOP 编程,有利于保持业务逻辑的清晰。
目前框架引入的门槛略高,需要打 git patch 到 flutter_tools 上,相应版本的配合指定得也还不明确。
总体上是一个很实用的框架。