Flutter aspectd入门指引

什么是aspectd?aspectd是闲鱼针对dart的AOP开源框架。https://github.com/alibaba-flutter/aspectd.git
阅读本文你将得到什么?

  1. 掌握aspectd的环境搭建,并如何在本地成功运行aspectd的demo
  2. 掌握有关aop的基础概念
  3. 了解aspectd的基础用法和原理

0x01 准备

1.1 开发环境

aspectd的环境搭建需要flutter源码、aspectd源码和dart源码,并需要在系统中设置相应的全局环境变量。

1.1.1 flutter环境

下载flutter源码:

1
git clone https://github.com/flutter/flutter.git

1.1.2 aspectd下载

下载aspectd源码:

1
git clone https://github.com/alibaba-flutter/aspectd.git

1.1.3 环境变量

配置flutter镜像、本地flutter源码地址、flutter bin目录、dart bin目录:

1
2
3
4
5
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源码中的kernalfront_end,可通过查看aspectd源码根目录中的pubspec.yaml查看依赖库和对应的ref。

1
2
3
4
5
6
7
8
9
10
11
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不生效的方法。

  1. 手动下载dart源码,git clone https://github.com/dart-lang/sdk.git
  2. 将dart源码切换到aspectd项目中pubspec.yaml指定的ref上,如上例中,可执行 git checkout 5e39817ec7ab7f56f381c244d105c7e40913a3e0
  3. 将aspect对github源码的依赖改成对本地源码的依赖
  4. 手动编译aspect.dart.snapshot(在aspectd根目录中)
    dart --snapshot=snapshot/aspectd.dart.snapshot tool/starter.dart
  5. 修改flutter源码中的aspectd.dart,强制指定aspect.dart.snapshot的目录。
  6. 删除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方法执行切面操作

1
2
3
4
5
6
7
void test(){
print("print hello world!")
}

void main(){
test();
}

@Call表达注解的实际代码会变成这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
void test(){
print("print hello world!")
}

void invokeCall(){
// to do somethings
test();
// to do somethings
}

void main(){
aop:invokeCall()
}

@Execute表达注解的实际代码会变成这样:

1
2
3
4
5
6
7
8
9
10
11
12
void invokeExecutor(){
// to do somethings
print("print hello world!")
}

void test(){
invokeExecutor();
}

void main(){
test();
}

而@Inject相对于Call/Executor而言,多了一个lineNum的参数,用于指定插入逻辑的具体行号。用于在具体方法中间插入处理逻辑。

0x03 参考文档

https://github.com/alibaba-flutter/aspectd/issues/26