看!闲鱼又开源了一个 Flutter 开发利器

阿里妹导读:随着 Flutter 这一框架的快速发展,有越来越多的业务开始使用 Flutter 来重构或新建其产品。但在我们的实践过程中发现,一方面 Flutter 开发效率高,性能优异,跨平台表现好,另一方面 Flutter 也面临着插件,基础能力,底层框架缺失或者不完善等问题。今天,闲鱼团队的正物带我们解决一个问题:如何解决 AOP for Flutter?

问题背景

我们在实现一个自动化录制回放的过程中发现,需要去修改 Flutter 框架( Dart 层面)的代码才能够满足要求,这就会有了对框架的侵入性。要解决这种侵入性的问题,更好地减少迭代过程中的维护成本,我们考虑的首要方案即面向切面编程。

那么如何解决 AOP for Flutter 这个问题呢?本文将重点介绍一个闲鱼技术团队开发的针对 Dart 的 AOP 编程框架 AspectD。

AspectD:面向 Dart 的 AOP 框架

AOP 能力究竟是运行时还是编译时支持依赖于语言本身的特点。举例来说在 iOS 中,Objective C 本身提供了强大的运行时和动态性使得运行期 AOP 简单易用。在 Android下,Java 语言的特点不仅可以实现类似 AspectJ 这样的基于字节码修改的编译期静态代理,也可以实现 Spring AOP 这样的基于运行时增强的运行期动态代理。那么 Dart 呢?一来 Dart 的反射支持很弱,只支持了检查( Introspection ),不支持修改( Modification );其次 Flutter 为了包大小,健壮性等的原因禁止了反射。

因此,我们设计实现了基于编译期修改的 AOP 方案 AspectD。

1、设计详图

2、典型的 AOP 场景

下列 AspectD 代码说明了一个典型的 AOP 使用场景:

aop.dartimport 'package:example/main.dart' as app;
import 'aop_impl.dart';void main()=> app.main();
aop_impl.dartimport 'package:aspectd/aspectd.dart';@Aspect()
@pragma("vm:entry-point")
class ExecuteDemo {
@pragma("vm:entry-point")
ExecuteDemo();@Execute("package:example/main.dart", "_MyHomePageState", "-_incrementCounter")
@pragma("vm:entry-point")
void _incrementCounter(PointCut pointcut) {pointcut.proceed();
print('KWLM called!');
}
}

3、面向开发者的API设计

PointCut 的设计

@Call("package:app/calculator.dart","Calculator","-getCurTime")

PointCut 需要完备表征以什么样的方式( Call/Execute 等),向哪个 Library,哪个类(Library Method 的时候此项为空),哪个方法来添加 AOP 逻辑。PointCut 的数据结构:

@pragma('vm:entry-point')
class PointCut {
final Map<dynamic, dynamic> sourceInfos;
final Object target;
final String function;
final String stubId;
final List<dynamic> positionalParams;
final Map<dynamic, dynamic> namedParams;@pragma('vm:entry-point')
PointCut(this.sourceInfos, this.target, this.function, this.stubId,this.positionalParams, this.namedParams);@pragma('vm:entry-point')
Object proceed(){
return null;
}
}

其中包含了源代码信息(如库名,文件名,行号等),方法调用对象,函数名,参数信息等。请注意这里的 @pragma('vm:entry-point')注解,其核心逻辑在于 Tree-Shaking 。在 AOT(ahead of time) 编译下,如果不能被应用主入口( main )最终可能调到,那么将被视为无用代码而丢弃。AOP 代码因为其注入逻辑的无侵入性,显然是不会被main 调到的,因此需要此注解告诉编译器不要丢弃这段逻辑。此处的 proceed 方法,类似 AspectJ 中的 ProceedingJoinPoint.proceed()方法,调用 pointcut.proceed()方法即可实现对原始逻辑的调用。原始定义中的 proceed 方法体只是个空壳,其内容将会被在运行时动态生成。

Advice 的设计

@pragma("vm:entry-point")
Future<String> getCurTime(PointCut pointcut) async{
...
return result;
}

此处的 @pragma("vm:entry-point")效果同a中所述,pointCut对象作为参数传入AOP方法,使开发者可以获得源代码调用信息的相关信息,实现自身逻辑或者是通过pointcut.proceed()调用原始逻辑。

Aspect 的设计

@pragma("vm:entry-point")
class ExecuteDemo {
@pragma("vm:entry-point")
ExecuteDemo();
...
}

Aspect 的注解可以使得 ExecuteDemo 这样的 AOP 实现类被方便地识别和提取,也可以起到开关的作用,即如果希望禁掉此段 AOP 逻辑,移除 @Aspect 注解即可。

4、AOP 代码的编译

包含原始工程的 main 入口

从上文可以看到,aop.dart 引入 import'package:example/main.dart'as app; 这使得编译 aop.dart 时可包含整个 example 工程的所有代码。

Debug 模式下的编译

在 aop.dart 中引入 import'aop_impl.dart'; 这使得 aop_impl.dart 中内容即便不被aop.dart 显式依赖,也可以在 Debug 模式下被编译进去。

Release 模式下的编译

在 AOT 编译( Release 模式下),Tree-Shaking 逻辑使得当 aop_impl.dart 中的内容没有被 aop 中 main 调用时,其内容将不会编译到 dill 中。通过添加 @pragma("vm:entry-point") 可以避免其影响。

当我们用 AspectD 写出 AOP 代码,透过编译 aop.dart 生成中间产物,使得 dill 中既包含了原始项目代码,也包含了 AOP 代码后,则需要考虑如何对其修改。在 AspectJ 中,修改是通过对 Class 文件进行操作实现的,在 AspectD 中,我们则对 dill 文件进行操作。

5、Dill操作

dill 文件,又称为 Dart Intermediate Language,是 Dart 语言编译中的一个概念,无论是 Script Snapshot 还是 AOT 编译,都需要 dill 作为中间产物。

Dill 的结构

我们可以通过 dart sdk 中的 vm package 提供的 dump_kernel.dart 打印出 dill 的内部结构

Dill 变换

dart 提供了一种 Kernel to Kernel Transform 的方式,可以通过对 dill 文件的递归式AST 遍历,实现对 dill 的变换。

基于开发者编写的 AspectD 注解,AspectD 的变换部分可以提取出是哪些库/类/方法需要添加怎样的 AOP 代码,再在 AST 递归的过程中通过对目标类的操作,实现Call/Execute 这样的功能。

一个典型的 Transform 部分逻辑如下所示:

@override
MethodInvocation visitMethodInvocation(MethodInvocation methodInvocation) {methodInvocation.transformChildren(this);
Node node = methodInvocation.interfaceTargetReference?.node;
String uniqueKeyForMethod = null;
if (node is Procedure) {
Procedure procedure = node;
Class cls = procedure.parent as Class;
String procedureImportUri = cls.reference.canonicalName.parent.name;uniqueKeyForMethod = AspectdItemInfo.uniqueKeyForMethod(procedureImportUri, cls.name, methodInvocation.name.name, false, null);
}
else if(node == null) {
String importUri = methodInvocation?.interfaceTargetReference?.canonicalName?.reference?.canonicalName?.nonRootTop?.name;
String clsName = methodInvocation?.interfaceTargetReference?.canonicalName?.parent?.parent?.name;
String methodName = methodInvocation?.interfaceTargetReference?.canonicalName?.name;uniqueKeyForMethod = AspectdItemInfo.uniqueKeyForMethod(importUri, clsName, methodName, false, null);
}
if(uniqueKeyForMethod != null) {
AspectdItemInfo aspectdItemInfo = _aspectdInfoMap[uniqueKeyForMethod];
if (aspectdItemInfo?.mode == AspectdMode.Call &&
!_transformedInvocationSet.contains(methodInvocation) && AspectdUtils.checkIfSkipAOP(aspectdItemInfo, _curLibrary) == false) {
return transformInstanceMethodInvocation(methodInvocation, aspectdItemInfo);
}
}
return methodInvocation;
}

通过对于 dill 中 AST 对象的遍历(此处的 visitMethodInvocation 函数),结合开发者书写的 AspectD 注解(此处的 aspectdInfoMap 和 aspectdItemInfo ),可以对原始的 AST 对象(此处 methodInvocation )进行变换,从而改变原始的代码逻辑,即Transform 过程。

6、AspectD 支持的语法

不同于 AspectJ 中提供的 BeforeAroundAfter 三种预发,在 AspectD 中,只有一种统一的抽象即 Around。从是否修改原始方法内部而言,有 Call 和 Execute 两种,前者的 PointCut 是调用点,后者的 PointCut 则是执行点。

Call

import 'package:aspectd/aspectd.dart';@Aspect()
@pragma("vm:entry-point")
class CallDemo{
@Call("package:app/calculator.dart","Calculator","-getCurTime")
@pragma("vm:entry-point")
Future<String> getCurTime(PointCut pointcut) async{
print('Aspectd:KWLM02');
print('${pointcut.sourceInfos.toString()}');
Future<String> result = pointcut.proceed();
String test = await result;
print('Aspectd:KWLM03');
print('${test}');
return result;
}
}

Execute

import 'package:aspectd/aspectd.dart';@Aspect()
@pragma("vm:entry-point")
class ExecuteDemo{
@Execute("package:app/calculator.dart","Calculator","-getCurTime")
@pragma("vm:entry-point")
Future<String> getCurTime(PointCut pointcut) async{
print('Aspectd:KWLM12');
print('${pointcut.sourceInfos.toString()}');
Future<String> result = pointcut.proceed();
String test = await result;
print('Aspectd:KWLM13');
print('${test}');
return result;
}

Inject

仅支持 Call 和 Execute,对于 Flutter(Dart) 而言显然很是单薄。一方面 Flutter 禁止了反射,退一步讲,即便 Flutter 开启了反射支持,依然很弱,并不能满足需求。举个典型的场景,如果需要注入的 dart 代码里,x.dart 文件的类 y 定义了一个私有方法 m或者成员变量 p,那么在 aop_impl.dart 中是没有办法对其访问的,更不用说多个连续的私有变量属性获得。另一方面,仅仅对方法整体进行操作可能是不够的,我们可能需要在方法的中间插入处理逻辑。为了解决这一问题,AspectD 设计了一种语法 Inject,参见下面的例子:flutter 库中包含了一下这段手势相关代码:

Widget build(BuildContext context) {
final Map<Type, GestureRecognizerFactory> gestures = <Type, GestureRecognizerFactory>{};if (onTapDown != null || onTapUp != null || onTap != null || onTapCancel != null) {gestures[TapGestureRecognizer] = GestureRecognizerFactoryWithHandlers<TapGestureRecognizer>(
() => TapGestureRecognizer(debugOwner: this),
(TapGestureRecognizer instance) {instance
..onTapDown = onTapDown
..onTapUp = onTapUp
..onTap = onTap
..onTapCancel = onTapCancel;
},
);
}

如果我们想要在 onTapCancel 之后添加一段对于 instance 和 context 的处理逻辑, Call 和 Execute 是不可行的,而使用 Inject 后,只需要简单的几句即可解决:

@Aspect()
@pragma("vm:entry-point")
class InjectDemo{
@Inject("package:flutter/src/widgets/gesture_detector.dart","GestureDetector","-build", lineNum:452)
@pragma("vm:entry-point")
static void onTapBuild() {
Object instance; //Aspectd Ignore
Object context; //Aspectd Ignore
print(instance);
print(context);
print('Aspectd:KWLM25');
}
}

通过上述的处理逻辑,经过编译构建后的 dill 中的 GestureDetector.build 方法如下所示:

此外,Inject 的输入参数相对于 Call/Execute 而言,多了一个 lineNum 的命名参数,可用于指定插入逻辑的具体行号。

7、构建流程支持

虽然我们可以通过编译 aop.dart 达到同时编译原始工程代码和 AspectD 代码到 dill 文件,再通过 Transform 实现 dill 层次的变换实现 AOP,但标准的 flutter 构建(即fluttertools) 并不支持这个过程,所以还是需要对构建过程做细微修改。在 AspectJ 中,这一过程是由非标准 Java 编译器的 Ajc 来实现的。在 AspectD 中,通过对fluttertools 打上应用 Patch,可以实现对于 AspectD 的支持。

kylewong@KyleWongdeMacBook-Pro fluttermaster % git apply --3way /Users/kylewong/Codes/AOP/aspectd/0001-aspectd.patch
kylewong@KyleWongdeMacBook-Pro fluttermaster % rm bin/cache/flutter_tools.stamp
kylewong@KyleWongdeMacBook-Pro fluttermaster % flutter doctor -v
Building flutter tool...

实战与思考

基于 AspectD,我们在实践中成功地移除了所有对于 Flutter 框架的侵入性代码,实现了同有侵入性代码同样的功能,支撑上百个脚本的录制回放与自动化回归稳定可靠运行。

从 AspectD 的角度看,Call/Execute 可以帮助我们便捷实现诸如性能埋点(关键方法的调用时长),日志增强(获取某个方法具体是在什么地方被调用到的详细信息),Doom 录制回放(如随机数序列的生成记录与回放)等功能。Inject 语法则更为强大,可以通过类似源代码诸如的方式,实现逻辑的自由注入,可以支持诸如 App 录制与自动化回归(如用户触摸事件的录制与回放)等复杂场景。

进一步来说,AspectD 的原理基于 Dill 变换,有了 Dill 操作这一利器,开发者可以自由地对 Dart 编译产物进行操作,而且这种变换面向的是近乎源代码级别的 AST 对象,不仅强大而且可靠。无论是做一些逻辑替换,还是是 Json<--> 模型转换等,都提供了一种新的视角与可能。


原文链接
本文为云栖社区原创内容,未经允许不得转载。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/518220.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

这些常见的分布式存储系统,你是否都了解?

来源 | 清平の乐来源 | CSDN博客&#xff0c;责编 | Carol头图 | CSDN 下载自视觉中国一、数据存储类型一般情况下&#xff0c;我们将存储分成了4种类型&#xff0c;基于本机的DAS和网络的NAS存储、SAN存储、对象存储。对象存储是SAN存储和NAS存储结合后的产物&#xff0c;汲取…

给软件工程师、数据科学家和数据工程师的面试指南:该做与不该做

亚马逊这样的公司有 14 项领导原则 。他们不想仅仅雇佣一个数据科学家或软件工程师。对于许多只进行一次或两次面试的面试者来说&#xff0c;这可能没有那么明显&#xff0c;因为你太专注于回答面试的技术部分。但是&#xff0c;在你进行技术面试时&#xff0c;我们希望提供一些…

限制在同一台电脑上只允许有一个用户登录

文章目录1. html 部分2. js部分3. 拦截器部分4. 认证授权部分5. 控制层部分6. 工具类实现流程: 1.从reqest域中获取现在登陆的新sessionId 2.根据登陆的用户名从reqest域中获取已经登陆的老sessionId 3.判断老sessionId是否存在和新旧sessionId是否是否一致 如果一直返回当前用…

FM算法介绍

概述 FM (Factorization Machine) 算法可进行回归和二分类预测&#xff0c;它的特点是考虑了特征之间的相互作用&#xff0c;是一种非线性模型&#xff0c;目前FM算法是推荐领域被验证的效果较好的推荐方案之一&#xff0c;在诸多电商、广告、直播厂商的推荐领域有广泛应用。 …

最新!Vicor 270V-28V DCM5614以96%效率提供1300W功率

近日Vicor 宣布推出隔离式稳压 270V-28V DC-DC 转换器 DCM5614&#xff0c;其采用 5.6 x 1.4 0.3 英寸 VIA™ 封装&#xff0c;额定输出功率为 1300W。据了解DCM5614 重量仅 178g&#xff0c;提供无与伦比的功率密度可达451W/in3 &#xff0c;支持功率密度、重量和效率都至关重…

JavaScript-jQuery事件

参考文档&#xff1a; https://jquery.cuishifeng.cn/ 事件 鼠标事件&#xff0c;键盘事件&#xff0c;其他事件 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><title>Title</title><script src&…

58 集团大规模 Storm 任务平滑迁移至 Flink 的秘密

Flink-Storm 是 Flink 官方提供的用于 Flink 兼容 Storm 程序 beta 工具&#xff0c;并且在 Release 1.8 之后去掉相关代码。本文主要讲述 58 实时计算平台如何优化 Flink-Storm 以及基于 Flink-Storm 实现真实场景下大规模 Storm 任务平滑迁移 Flink。 背景 58 实时计算平台…

前后端敏感数据加密方案及实现_01

文章目录一、组成部分1. html2. js3. 拦截器4. 认证授权5. 控制层6. 工具类一、组成部分 1. html <form id"formId" class"layui-form" action"${ctxPath}/login" method"post"><!-- 用户名 --><div class"layu…

离屏渲染在车载导航中的应用

导读 与手机导航不同&#xff0c;高德地图的车机版&#xff08;AMAP AUTO&#xff09;直接面对各大车厂和众多设备商。这些B端用户采用的硬件参数参差不齐&#xff0c;提出的业务需求涉及到渲染中诸多复杂技术的应用&#xff0c;这对渲染性能提出了极高的要求。 最初车机版沿…

打造大数据和AI能力底座 联通大数据深度参与“新基建”

年初至今&#xff0c;国家关于“新基建”的政策持续发布&#xff0c;引起社会各界广泛关注。目前来看&#xff0c;官方定义的新型基础设施主要包括信息基础设施、融合基础设施、创新基础设施三方面内容。疫情过后&#xff0c;新型基础设施建设将承担起经济复苏的使命&#xff0…

技术人如何通过了解业务,获取晋升机会?

伐薪是阿里巴巴高级技术专家&#xff0c;14年初入阿里时&#xff0c;没有过多地思考业务痛点和了解业务策略。后来&#xff0c;经历过晋升&#xff0c;当晋升评委&#xff0c;主动学习业务&#xff0c;最后&#xff0c;完成了从技术专家向综合性 TL 转变。这一路下来&#xff0…

adb-获取包名/界面名、获取app启动时间、卸载app、退出app、查看所有进程、查看所有包名

获取包名/界面名 Mac/Linux&#xff1a; adb shell dumpsys window windows | grep mFocusedApp adb shell dumpsys window windows | grep mCurrentFocus adb shell dumpsys window | grep mCurrentFocusWindows: adb shell dumpsys window windows | findstr mCurrentFoc…

记一次代码重构

单一职责 功能单一 功能单一是SRP最基本要求&#xff0c;也就是你一个类的功能职责要单一&#xff0c;这样内聚性才高。 比如&#xff0c;下面这个参数类&#xff0c;是用来查询网站Buyer信息的&#xff0c;按照SRP&#xff0c;里面就应该放置查询相关的Field就好了。 Data…

​如何成为一个更好的 React 开发者?

作者 | Siradji Awoual译者 | 苏本如&#xff0c;责编 | 屠敏头图 | CSDN 下载自东方 IC出品 | CSDN&#xff08;ID&#xff1a;CSDNnews&#xff09;大家好&#xff01;希望你们在疫情期间平平安安。今天&#xff0c;我想在这里谈谈不同的内容。我想分享一些React的开发技巧和…

前后端敏感数据加密方案及实现_02

文章目录1. 环境2. vue部分3. 控制层4. 工具类1. 环境 组件版本springboot2.4.0后端框架3.0.0前端框架vue-router4.0.0-0ant-design-vue2.0.0-rc.3vuex4.0.0-0 2. vue部分 <template><a-layout-header class"header"><div class"logo">…

蚂蚁金服 3 个项目进入 CNCF 云原生全景图 | 开源

2019 年 6 月 25 日&#xff0c;全球知名开源组织云原生计算基金会 CNCF 宣布&#xff0c;蚂蚁金服正式成为 CNCF 黄金会员&#xff0c;蚂蚁金服表示将持续加大对开源项目的支持&#xff0c;包括 Kubernetes&#xff0c;ServiceMesh&#xff0c;Serverless&#xff0c;安全容器…

Knative 初体验:CICD 极速入门

Knative 社区很早就在讨论用 Tekton 替换 Build 模块的事宜。Knative Build 官方已经正式说明不再建议使用 Knative Build 了。 如果你知道 Knative Build 是什么相信你理解起 Tekton 就是很容易的一件事了。 Knative Build 对自己的一句话概述是&#xff1a;A Kubernetes-na…

关于 Docker ,你必须了解的核心都在这里了!

来源 | fysuccess来源 | CSDN博客&#xff0c;责编 | Carol头图 | CSDN 下载自视觉中国Docker引擎Docker Engine是具有以下主要组件的客户端-服务器应用程序&#xff1a;服务器是一种长期运行的程序&#xff0c;称为守护程序进程&#xff08; dockerd命令&#xff09;。REST AP…

如何在视频里任意抠图?阿里工程师做到了!

阿里妹导读&#xff1a;现在的我们在手机上花费了越来越多的时间&#xff0c;其中&#xff0c;视频又格外地吸引我们的注意力。有很多好玩的视频&#xff0c;需要把前景物体从视频中分割出来&#xff0c;这需要花费创作者99%以上的时间。今天&#xff0c;阿里资深算法专家任海兵…

启动vue项目失败,报错Failed at the node-sass@4.14.1 postinstall script.

在启动vue项目的时候报错 报错信息如下&#xff1a;npm ERR! code ELIFECYCLE npm ERR! errno 1 npm ERR! node-sass4.14.1 postinstall: node scripts/build.js npm ERR! Exit status 1 npm ERR! npm ERR! Failed at the node-sass4.14.1 postinstall script. npm ERR! This i…