Flutter AOP集成框架已更新,支持flutter全版本: github.com/TDesignOtea…
一、前言
Flutter AOP并不是什么新鲜的技术,但并没有得到很好的支持。
实现AOP一般是两种表现形式:
- 通过直接修改AST语法树,见《AST语法树操纵》。
- 在语法树基础上封装一层注解,通过注解标识的内容进行固定规则的插桩,见「AspectD」。
这两种方式后文还会提到,但这里更多是吐槽AOP接入方式的不统一,更像是应对项目情况的“特殊处理”:
- 只适配Flutter特殊版本,当升级Flutter时需要重新处理环境....
- 对插件来说更不友好,由于缺乏一致的生态基础,难以兼容全部Flutter版本,也无法提供给外部使用....
- 业务不能同时使用携带AOP代码的方案,因为执行入口只有一个....
- 不好用,FlutterWeb不支持,依赖方式不符合直觉,代码引用爆红....
- 期待官方支持遥遥无期.....
为什么不能有统一标准化的方案呢?像Android - gradle transfomer能力一样,提供基础的框架。在统一生态下,让AOP脱离Flutter版本,使Flutter AOP像正常开发一样自然。
这就是AOPRegistry做到的事情,定义了一套简单标准的集成方式。在这套框架下编译环境由pub get/upgrade动态构建,支持Flutter全版本,插件也可以使用的AOP能力。项目链接:github.com/TDesignOtea…
二、AOP方案的现状
Flutter AOP是怎么实现的,为什么接入流程方式存在欠考虑的地方?本节将对这一编码手段进行整体的阐述。
使用AOP一般是以下两种目的:
- 触达正常不可编写的代码: 切面是中间编译结果的再处理,可修改源码、三方库。
- 利用切面视角建立统一的规则: 在切面角度可以广泛收集信息,对代码进行统一处理,以实现正常编码无法覆盖的能力。
--
这里对切面编程的描述:“中间编译结果的再处理”、“建立统一的规则”刚好可以对应前文 直接AST操作 和 AspectD 两种AOP方式。
前者是后者的基础,而他们都有相同的集成原理,我们分别来看下。
1. AOP是中间编译结果的再处理
与Java字节码插桩一样,Dart同样有自己的中间编译结果AST(抽象语法树)。AST生成以及Transformer变换,本来就是在Dart编译流程中存在的环节,再由此编译出最终的App包。
现在现在只需要使用AOPRegistry即可完成环境处理,进行AOP开发。
AST结构清晰易读,对其操作像是拼图和积木,并无难度,但操作起来相对麻烦。
好处是有最高的自由度,所有代码内容都呈现在其中,AOP不存在任何限制。见《AST语法树操纵》
2. 注解AOP是建立了统一的插桩规则
AspectD是闲鱼团队开发的一套插装方案,是基于AST操纵建立了一套插桩规则。
这里想表述的含义是,这种方案是利用AST建立规则的普通插件,只是这项规则的目的是用于AOP本身。
--
我们以@Call注解为例,来看下它是怎么建立规则的。当想在所有调用hello的位置做处理时,利用AspectD可以这样写
@Call("package:t_aspectd_example/main.dart", "TestDemo", "-hello")
@pragma("vm:entry-point")
dynamic callHello(PointCut pointcut) {
// do something before...
// 执行原逻辑
Object? result = pointcut.proceed();
// do something after...
return result;
}
这里建立的的规则是:
- Call注解标明了想修改的位置。
- 将原有的函数调用变更指向到本函数。
- PointCut保存了信息及原本的调用方式,以此可以判断或执行原逻辑。
利用这套规则,就可以避免直接操作AST,降低AOP成本。
3. 集成方案的困境
一切都很好,但在本文之前,AOP集成方式都更像是一种“特殊处理”。
AST是在Dart编译时的中间结果,集成的本质是编译流程的替换,来加入AOP的逻辑。 但是,需要思虑以下两点:
- Dart编译所需的依赖繁多(50+),每个Dart版本对应的三方库的选择不同,版本也不同。
- 编译流程只会走一次,不同的接入方案不能自然融合在一起。
更多的是,可能源于官方做法,之前集成方案都默许放弃了pubspec.yaml依赖方式,直接处理package_config.json。
-
依赖繁多?建一个仓库拷贝进所有dart依赖,只能在一个Flutter上使用...
-
AOP工程可能有其他引用?flutter_tools特殊处理下....
-
方案只按固定方式接入,不考虑与其他AOP工程兼容,不考虑Flutter多版本支持...
我们利用AOP实现了功能模块,但更多的是像一个"代码孤岛"。存在不同的特殊处理,难以提供给其他团队使用。
为什么不能有统一标准化的方案呢?在标准框架下,项目及插件在Flutter各版本下都能实现AOP操作。
三、打造标准的AOP框架
直接来看看AOPRegistry是怎么统一生态的吧
AOPRegistry的使用方式非常简单:
i. 标准的集成方式。
向FlutterSDK打入补丁,这一步是AOP实现的切入点,无法省略。
// 在FlutterSDK目录下
git apply --3way aop_market/patch_flutter/2.2.0_infinity.patch
// 删除SDK/bin/cache/flutter_tools.stamp,使修改生效。
rm ./bin/cache/flutter_tools.stamp
ii. 嵌入pub get/upgrade,动态处理依赖、串联所有AOP代码
当项目进行pub get/upgrade,就会动态处理该项目AOP所需的依赖,并进行代码串联。
iii. 嵌入compile,执行编译流程的重定向
编译时AOP将生效,同时打印用于调试的信息,支持Android/iOS与Web。
--
对于开发者来说,无论是t_aspectd还是其他携带AOP代码的插件,标准化集成后直接引用就可以生效了。
自身的AOP工程可以正常开发,与普通的开发方式无异。可直接引用插件,代码不再引用爆红,也无需考虑Flutter版本、多插件兼容的问题。
而框架内部发生了什么事情,这里主要介绍两点:“动态处理依赖” 与 “可用性的考量”。
1. 动态处理依赖
如前文所说,每个Dart版本可能存在不同的三方库依赖,如何动态解析并引用是实现“通用性”的必要条件。
而这个前提是可以做到的:
- 确定目标依赖:Dart仓库中DEPS存储了所有依赖信息,也可以从flutter_tools里获取flutter及dart版本。
- 读取pubspec.yaml文件确定AOP工程与配置,复用pubspec.yaml依赖处理方式。
- 以AOPRegistry工程合并依赖,串联多个AOP工程,编译流程以AOPRegistry作为统一入口。
2. 框架的可用性考量
作为统一AOP生态的基础框架,目标是实现AOP完全自然的开发方式,这里有许多考量。
** i.向更底层嵌入编译流程**
即使Flutter版本变化,标准化集成的方式是统一的。这是因为AOPRegistry在更底层的命令分发时进行的拦截:
这样做的好处不仅是兼容性,更是考虑到了AOP生效的全面性,debug模式、热重启也可以生效。
此外,由于所有指向dart的指令都能拦截,框架也可以模仿官方做出更合理的优化。比如环境改变或AOP工程路径变化时,编译前可以自动pub get/upgrade。
** ii.保证依赖逻辑的健全性**
AOPRegistry定义了一套pubspec.yaml写入方式来处理依赖,但更多是利用pub缓存来减少耗时。第一次pub由于远程拉取的原因耗时为几分钟,但后面仅几秒钟就可以了。
对于AOPRegistry来说,一共会处理4种依赖,都需要在环境变化时进行变更:
-
Dart依赖:当前Flutter版本对应的Dart仓库,加入必要的AST钩子代码。当Dart版本变化或AOPRegistry有更新时重新获取。
-
引用的Dart三方库:DEPS定义的50+个三方库,当Dart版本变化时重新获取。
-
串联的AOP工程:项目的AOP工程、插件种使用配置指向的AOP工程。通过路径依赖导入AOPRegistry中,目标项目改变时重新获取。
-
AOP工程使用的dev_dependencies与dependency_overrides的依赖:这两种配置不会进行依赖传递,所以需要拷贝到AOPRegistry中。目标项目改变时重新获取。
AOPRegistry在环境变化时会重新获取对应的依赖,这使得框架在Flutter切换、多个项目场景下也行之有效,完全与日常Flutter开发一致的体验。
iiii.AOPRegistry工程的解耦意义
在标准化集成后,AOPRegistry就已经内置到FlutterSDK中了,将在pub时检查更新,并保存了Dart改造方式及DEPS解析方式。其工程意义除了统一AOP入口以外,更重要的是抽离与Dart耦合的部分,并实时更新处理方式。
也就是说AOPRegistry目前已验证支持Flutter 2.2.0到最新的3.35.x。虽然无法预测以后的版本,但如果Dart迭代中有大的变更,我们也可以通过更新AOPRegistry来进行适配,而用户无需重新集成。
四、结语
AOP确实是“另类”的编码方式,但在某些特殊需求下又是必不可少的手段。
如果业务新接入AOP或者遇到同样的困惑,不妨使用下AOPRegistry,一定可以给你丝滑的开发体验~