Flutter切面的应用与扩展

更新日期: 2022-02-22阅读: 985标签: Flutter

背景

作为一款国民级二手交易App,每天有大量提测任务到质量团队,如何准确地衡量影响范围以及确保提测代码不存在漏测变得尤为重要。因此闲鱼质量尝试研发客户端的精准化测试与用例推荐,遇到的第一个问题——如何实现代码染色和用例关联。对于Native开发,无论是iOS和Android都有比较成熟的技术和方案,但是对于Flutter来说,这一块方案是实质缺失的,通过调研,我们发现Aspectd能够初步达到我们想要的效果。

Aspectd是一个闲鱼技术团队开发的针对Dart的AOP编程框架AspectD,其原理介绍见 重磅开源|AOP for Flutter开发利器——AspectD

问题

我们尝试使用Aspectd切面上报测试时App执行的代码信息(模块、类、函数等),发现该方案虽然可行,但是依旧存在以下问题:

  1. Inject切面无法取得切入点信息。

  2. Inject切面不支持正则匹配插桩,即无法批量插入代码片段。

  3. Aspectd无法支持if、while等语句级别的切面。

  4. 需要提前学习被hook对象的代码,对不熟悉工程的测试同学而言,比较复杂。

基于以上问题,我们在Aspectd的基础上进行了一定的扩展以支持我们的代码染色和精准化测试。

技术方案


如何快速获取切入点信息

在代码染色中,我们除了需要获取某个功能调用了哪些函数,还需要了解代码具体走到了哪一个分支上。Inject操作能够在不改变调用逻辑的基础上,在原有函数体中注入代码片段。 Aspectd支持注入代码片段中使用原函数体中的变量信息,但是需要开发人员知道原函数变量命名。另外在使用Inject操作时,我们无法拿到具体切入点信息,例如当前执行函数的Library、class等信息。 为了解决这个问题,编译时通过修改AST生成了预置变量,这样开发者在使用Inject操作时可以直接根据固定变量获取对应切入点的相关信息,预置变量生成伪代码示例如下:

Procedure procedure = methodNode;
Class methodClass = procedure.parent;
/**
* 预置变量生成:函数相关信息
*/
final List<MapLiteralEntry> entries = <MapLiteralEntry>[];
entries.add(MapLiteralEntry(StringLiteral("library"), StringLiteral(library.importUri.toString()))); //添加模块
entries.add(MapLiteralEntry(StringLiteral("class"), StringLiteral(methodClass.name))); // 添加类
entries.add(MapLiteralEntry(StringLiteral("method"), StringLiteral(procedure.name.text))); // 添加方法名
entries.add(MapLiteralEntry(StringLiteral("args"), StringLiteral(procedure.function.positionalParameters.toString()))); // 参数列表
final MapLiteral methodLiteral = MapLiteral(entries);
final VariableDeclaration methodInfo = VariableDeclaration("methodInfo", initializer: methodLiteral);
tmpStatements.add(methodInfo);




/**
* 预置变量生成:参数相关信息
*/
Library core = _libraryMap['dart:core'];
final List<MapLiteralEntry> paramsEntries = <MapLiteralEntry>[];
final MapLiteral paramsLiteral = MapLiteral(paramsEntries);
final VariableDeclaration paramsInfo = VariableDeclaration("params", initializer: paramsLiteral);
tmpStatements.add(paramsInfo);


final List<DartType> positionalParameters = <DartType>[];
positionalParameters.add(DynamicType());
positionalParameters.add(DynamicType());
FunctionType functionType = FunctionType(positionalParameters, VoidType(), Nullability.legacy);
for (VariableDeclaration variable in procedure.function.namedParameters) {
final List<Expression> positional = <Expression>[];
positional.add(StringLiteral(variable.name));
positional.add(VariableGet(variable));
InstanceInvocation instanceInvocation = InstanceInvocation.byReference(InstanceAccessKind.Instance, VariableGet(paramsInfo), Name("[]="),Arguments(positional), interfaceTargetReference: core.classes[95].procedures[14].reference, functionType: functionType);
final VariableDeclaration p = VariableDeclaration(variable.name, initializer: VariableGet(variable));
ExpressionStatement newExpressionStatement = ExpressionStatement(instanceInvocation);
instanceInvocation.parent = newExpressionStatement;
instanceInvocation.parent = statement.parent;
tmpStatements.add(newExpressionStatement);
}

最终实现的效果如图1所示,通过断点可以看到,运行时,原始方法中多了两个变量——methodInfo和params,分别存储了函数模块信息和执行期间的参数信息。


图1

为了更直观地看到编译期间做的工作,使用 dart /pkg/vm/bin/dump_kernel.dart 对编译后的app.dill进行输出,如图2所示,在原有代码逻辑基础上,增加了methodInfo、params的定义和赋值逻辑。


图2

用户在自己的切面代码中,可以直接调用这两个预置变量来获取想要的信息。从而解决了inject注入无法获取切面点相关信息的问题。

如何对分支语句插桩

前面提到在进行代码染色时,除了需要知道调用的函数以外,我们还需要知道程序在遇到if/switch/for等分支语句时,具体走到了哪一个分支,以便我们统计代码的覆盖率以及测试的完整性。显然Aspectd目前并不支持基于语句级别的切面。


图3

阅读Aspectd代码后,我们发现在inject注入时,insertStatementsToBody方法会根据需要插入的lineNum找到对应的代码块, 然后插入需要注入的代码片段。如图2所示,分支语句也只是Statement的多个子类,所以基于语句级别的切面相对比较简单,我们只需要遍历原有函数体的代码块,判断其是否为分支语句,如果是分支语句,则插入我们想要注入的代码即可,其伪代码如下:

final List<Statement> statements = body.statements;
final int len = statements.length;
for (int i = 0; i < len; i++) {
final Statement statement = statements[i];
if (statement is IfStatement) {
insertStatementsToIfStatement(aopInsertStatements);
}


if (statement is SwitchStatement) {
insertStatementsToSwitchStatement(aopInsertStatements);
}


if (statement is TryStatement) {
insertStatementsToSwitchStatement(aopInsertStatements);
}
...
}

对于各个分支语句的插入则是单独处理,例如IfStatement,需要分别在then和otherwise两个语句子Statements进行注入代码片段插入。

如何快速批量插桩

在过去一年,客户端同学对闲鱼工程进行了拆包,每个模块都是一个独立的库。测试同学只了解自己负责的业务是什么模块,对于业务代码的细节并不清楚。然而在Aspectd中使用Inject插桩需要明确具体的函数信息,这对于代码染色这种需要批量插桩并不适应。

Aspectd在transform阶段能够拿到所有的library,因此可以通过配置文件显式定义需要切面的模块名称以及对应的切面action就能够直接对指定的模块进行切面,在这个过程中,测试同学只需要实现action函数即可,并不需要知道这个模块有什么类、什么方法。

通过配置模块和语句注入两个扩展,就可以实现对指定的模块进行插入代码片段,进行更加全面的代码染色。

fish_redux:
- type: 'inject'
module: package:fwn_idlefish/CodeExecutionLog.dart
action: CodeExection.Log
lineNum: 0


flutter_boost:
- type: 'call'
module: package:fwn_idlefish/CodeExecutionLog.dart
action: CodeExection.print


fish_test:
- type: 'inject'
module: package:fwn_idlefish/CodeExecutionLog.dart
action: CodeExection.Log
color: true # 代码染色,覆盖fish_test的所有代码分支

其配置样例如上所示,在编写完具体的切面逻辑在切面配置文件中指定切面的模块以及action,代码编译阶段首先会读取配置文件,生成AopItemInfo列表,然后在Transform时通过遍历Library,对需要切面的模块以及起方法进行切面。伪代码如下:

    for (Library library in libraryMapa.values) {
if (library.importUri.toString().contains(itemInfo.module) &&
library.importUri.toString().endsWith('aop.dart') == false &&
library.importUri.toString().contains('CodeExecutionLog') ==
false)
for (Class cls in library.classes) {
for (Constructor constructor in cls.constructors) {
transformConstructor(library,
_uriToSource[library.fileUri], procedure, aopItemInfo)
}
for (Procedure procedure in cls.procedures) {
if (blackList.contains(procedure.name) == false)
transformProcedure(library,
_uriToSource[library.fileUri], procedure, aopItemInfo);
}
}
}

总结

以上是我们在Aspectd实际应用中的一些思考和探索,目前该方案已经应用在闲鱼客户端精准化测试项目中,能够完整支持上报测试同学在执行用例时具体执行了哪些函数以及相关的环境信息,以实现业务代码和业务测试用例的关联,从而实现用例推荐,后续我们将完整介绍该方案。

除了精准化测试,该方案也会用于客户端代码实时染色系统,实时收集客户端的运行数据,计算出对应的代码行,确认代码执行情况,辅助代码走读,定位问题,完成覆盖率测试等。

同时后续我们也会继续尝试基于Aspectd来实现基于端侧的“流量回放”,用于验证代码变动后,逻辑是否受到影响。

作者:小匠
来源:https://mp.weixin.qq.com/s/SLhPezpoADPENzo2UNrlHQ


链接: https://www.fly63.com/article/detial/11215

移动跨平台框架Flutter介绍和学习线路

Flutter是一款移动应用程序SDK,一份代码可以同时生成iOS和Android两个高性能、高保真的应用程序。Flutter目标是使开发人员能够交付在不同平台上都感觉自然流畅的高性能应用程序。我们兼容滚动行为、排版、图标等方面的差异。

初识flutter

关注flutter已经好久,因为没有发正式版,所以一直也不想过早的躺浑水,但是最近无意中看到几篇文章,再加上美团和咸鱼等app也一直在做灰度测试,所以上周开始看了一下官方文档,地址:https://flutter.io/docs/get-started/install,然后在此做一下总结。

深入理解Flutter多线程

Flutter默认是单线程任务处理的,如果不开启新的线程,任务默认在主线程中处理。和iOS应用很像,在Dart的线程中也存在事件循环和消息队列的概念,但在Dart中线程叫做isolate。

Flutter1.5 开始,将成为全平台 UI 框架!

Flutter 1.5 的发布,同期也宣布发布 Flutter for Web 的 Preview 版本,正式开启了 Flutter 的全平台 UI 框架之路。早在年初发布的 Flutter 2019 Roadmap 中,就有提到,会在今年支持移动设备之外的平台,对 Web 的支持,算是完成了一个新的里程碑吧。

Flutter支持Web开发了!

Flutter作为一个可移植的UI框架,已经支持现代Web应用开发了!我们很开心已经发布了SDK预览版,这样你可以在Web浏览器里直接运行你的Flutter UI代码。

Flutter 混合开发 (交互通信)

Flutter 与原生之间的通信依赖灵活的消息传递方式:1,Flutter 部分通过平台通道将消息发送到其应用程序的所在的宿主环境(原生应用)。2,宿主环境通过监听平台通道,接收消息。

Flutter 局部路由实现

Flutter是借鉴React的开发思想实现的,在子组件的插槽上,React有this.props.children,Vue有<slot></slot>。当然Flutter也有类似的Widget,那就是Navigator,不过是以router的形式实现(像<router-view></router-view>)。

Flutter Kotlin 到底该如何选择?

这两个技术在当下如何选择,我之前在公众号上的回复是:如果你已经处于一个比较满意的公司,并考虑长期发展,公司并未使用这两个技术,你可以专心钻研公司当下使用的,或者未来将要使用的,这些才能助你在公司步步高升。

Flutter 与 iOS 原生 WebView 对比

本文对比的是 UIWebView、WKWebView、flutter_webview_plugin(在 iOS 中使用的是 WKWebView)的加载速度,内存使用情况。测试网页打开的速度,只需要获取 WebView 在开始加载网页和网页加载完成时的时间戳

Flutter For Web

用来构建漂亮、定制化应用的跨平台的 UI 框架 Flutter 现在已经支持 Web 开发了。我们很高兴推出了一个预览版的 SDK 可以让开发者直接使用 Flutter UI 和业务逻辑代码构建 web 应用

点击更多...

内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!