什么?Flutter标准的AOP框架开源了~

206 阅读8分钟

Flutter AOP集成框架已更新,支持flutter全版本: github.com/TDesignOtea…

一、前言

Flutter AOP并不是什么新鲜的技术,但并没有得到很好的支持。

实现AOP一般是两种表现形式:

  1. 通过直接修改AST语法树,见《AST语法树操纵》
  2. 在语法树基础上封装一层注解,通过注解标识的内容进行固定规则的插桩,见「AspectD」

这两种方式后文还会提到,但这里更多是吐槽AOP接入方式的不统一,更像是应对项目情况的“特殊处理”:

  1. 只适配Flutter特殊版本,当升级Flutter时需要重新处理环境....
  2. 对插件来说更不友好,由于缺乏一致的生态基础,难以兼容全部Flutter版本,也无法提供给外部使用....
  3. 业务不能同时使用携带AOP代码的方案,因为执行入口只有一个....
  4. 不好用,FlutterWeb不支持,依赖方式不符合直觉,代码引用爆红....
  5. 期待官方支持遥遥无期.....

为什么不能有统一标准化的方案呢?像Android - gradle transfomer能力一样,提供基础的框架。在统一生态下,让AOP脱离Flutter版本,使Flutter AOP像正常开发一样自然。

这就是AOPRegistry做到的事情,定义了一套简单标准的集成方式。在这套框架下编译环境由pub get/upgrade动态构建,支持Flutter全版本,插件也可以使用的AOP能力。项目链接:github.com/TDesignOtea…

二、AOP方案的现状

Flutter AOP是怎么实现的,为什么接入流程方式存在欠考虑的地方?本节将对这一编码手段进行整体的阐述。

使用AOP一般是以下两种目的:

  1. 触达正常不可编写的代码: 切面是中间编译结果的再处理,可修改源码、三方库。
  2. 利用切面视角建立统一的规则: 在切面角度可以广泛收集信息,对代码进行统一处理,以实现正常编码无法覆盖的能力。

--
这里对切面编程的描述:“中间编译结果的再处理”、“建立统一的规则”刚好可以对应前文 直接AST操作AspectD 两种AOP方式。

前者是后者的基础,而他们都有相同的集成原理,我们分别来看下。

1. AOP是中间编译结果的再处理

与Java字节码插桩一样,Dart同样有自己的中间编译结果AST(抽象语法树)。AST生成以及Transformer变换,本来就是在Dart编译流程中存在的环节,再由此编译出最终的App包。

现在现在只需要使用AOPRegistry即可完成环境处理,进行AOP开发。

AST结构清晰易读,对其操作像是拼图和积木,并无难度,但操作起来相对麻烦。

好处是有最高的自由度,所有代码内容都呈现在其中,AOP不存在任何限制。见《AST语法树操纵》

image.png

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;
  }

这里建立的的规则是:

  1. Call注解标明了想修改的位置。
  2. 将原有的函数调用变更指向到本函数。
  3. PointCut保存了信息及原本的调用方式,以此可以判断或执行原逻辑。

利用这套规则,就可以避免直接操作AST,降低AOP成本。

image.png

3. 集成方案的困境

一切都很好,但在本文之前,AOP集成方式都更像是一种“特殊处理”。

AST是在Dart编译时的中间结果,集成的本质是编译流程的替换,来加入AOP的逻辑。 但是,需要思虑以下两点:

  1. Dart编译所需的依赖繁多(50+),每个Dart版本对应的三方库的选择不同,版本也不同。
  2. 编译流程只会走一次,不同的接入方案不能自然融合在一起。

image.png

更多的是,可能源于官方做法,之前集成方案都默许放弃了pubspec.yaml依赖方式,直接处理package_config.json。

  1. 依赖繁多?建一个仓库拷贝进所有dart依赖,只能在一个Flutter上使用...

  2. AOP工程可能有其他引用?flutter_tools特殊处理下....

  3. 方案只按固定方式接入,不考虑与其他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所需的依赖,并进行代码串联。 image.png

iii. 嵌入compile,执行编译流程的重定向

编译时AOP将生效,同时打印用于调试的信息,支持Android/iOS与Web。 image.png

--
对于开发者来说,无论是t_aspectd还是其他携带AOP代码的插件,标准化集成后直接引用就可以生效了。

自身的AOP工程可以正常开发,与普通的开发方式无异。可直接引用插件,代码不再引用爆红,也无需考虑Flutter版本、多插件兼容的问题。

而框架内部发生了什么事情,这里主要介绍两点:“动态处理依赖”“可用性的考量”

1. 动态处理依赖

如前文所说,每个Dart版本可能存在不同的三方库依赖,如何动态解析并引用是实现“通用性”的必要条件。

而这个前提是可以做到的:

  1. 确定目标依赖:Dart仓库中DEPS存储了所有依赖信息,也可以从flutter_tools里获取flutter及dart版本。
  2. 读取pubspec.yaml文件确定AOP工程与配置,复用pubspec.yaml依赖处理方式。
  3. 以AOPRegistry工程合并依赖,串联多个AOP工程,编译流程以AOPRegistry作为统一入口。

image.png

2. 框架的可用性考量

作为统一AOP生态的基础框架,目标是实现AOP完全自然的开发方式,这里有许多考量。

** i.向更底层嵌入编译流程**

即使Flutter版本变化,标准化集成的方式是统一的。这是因为AOPRegistry在更底层的命令分发时进行的拦截:

image.png

这样做的好处不仅是兼容性,更是考虑到了AOP生效的全面性,debug模式、热重启也可以生效。

此外,由于所有指向dart的指令都能拦截,框架也可以模仿官方做出更合理的优化。比如环境改变或AOP工程路径变化时,编译前可以自动pub get/upgrade。

** ii.保证依赖逻辑的健全性**

AOPRegistry定义了一套pubspec.yaml写入方式来处理依赖,但更多是利用pub缓存来减少耗时。第一次pub由于远程拉取的原因耗时为几分钟,但后面仅几秒钟就可以了。

对于AOPRegistry来说,一共会处理4种依赖,都需要在环境变化时进行变更:

  1. Dart依赖:当前Flutter版本对应的Dart仓库,加入必要的AST钩子代码。当Dart版本变化或AOPRegistry有更新时重新获取。

  2. 引用的Dart三方库:DEPS定义的50+个三方库,当Dart版本变化时重新获取。

  3. 串联的AOP工程:项目的AOP工程、插件种使用配置指向的AOP工程。通过路径依赖导入AOPRegistry中,目标项目改变时重新获取。

  4. AOP工程使用的dev_dependencies与dependency_overrides的依赖:这两种配置不会进行依赖传递,所以需要拷贝到AOPRegistry中。目标项目改变时重新获取。

AOPRegistry在环境变化时会重新获取对应的依赖,这使得框架在Flutter切换、多个项目场景下也行之有效,完全与日常Flutter开发一致的体验。

image.png

iiii.AOPRegistry工程的解耦意义

在标准化集成后,AOPRegistry就已经内置到FlutterSDK中了,将在pub时检查更新,并保存了Dart改造方式及DEPS解析方式。其工程意义除了统一AOP入口以外,更重要的是抽离与Dart耦合的部分,并实时更新处理方式。

也就是说AOPRegistry目前已验证支持Flutter 2.2.0到最新的3.35.x。虽然无法预测以后的版本,但如果Dart迭代中有大的变更,我们也可以通过更新AOPRegistry来进行适配,而用户无需重新集成。

image.png

四、结语

AOP确实是“另类”的编码方式,但在某些特殊需求下又是必不可少的手段。

如果业务新接入AOP或者遇到同样的困惑,不妨使用下AOPRegistry,一定可以给你丝滑的开发体验~