Flutter Driver源码分析

2022/4/23 flutter

# Command

首先是最基础的一个概念Command。它定义在lib/src/common/message.dart中。

abstract class Command {
  final Duration? timeout;
  String get kind;
  bool get requiresRootWidgetAttached => true;
  const Command({ this.timeout });
  // serialize deserialize
}
1
2
3
4
5
6
7

timeout是等待Command运行完成的最大等待时间,默认是null,kind用来标记Command的类型,而requiresRootWidgetAttached表示Command是否需要确保Widget树在运行前已经初始化完成。

# Result

Result和Command对应,表示Command的运行结果。

abstract class Result {
  const Result();
 
  static const Result empty = _EmptyResult();

  Map<String, dynamic> toJson();
}
1
2
3
4
5
6
7

通过将构造函数加上const来保证子类也是const,通过重写toJson方法,来将结果序列化。_EmptyResult就是通过重写这个返回一个空map。

# finder

上面提到了command,那么command有一个同样是抽象类的子类CommandWithTarget

abstract class CommandWithTarget extends Command {

  final SerializableFinder finder;

  
  Map<String, String> serialize() =>
      super.serialize()..addAll(finder.serialize());
}
1
2
3
4
5
6
7
8

它在command的基础上加了一个SerializableFinder类型的finder属性。而SerializableFinder长下面这样。

abstract class SerializableFinder {
  const SerializableFinder();

  String get finderType;


  
  Map<String, String> serialize() => <String, String>{
    'finderType': finderType,
  };
}
1
2
3
4
5
6
7
8
9
10
11

它是flutter driver finders的基类,用于描述driver如何寻找元素。通过继承SerializableFinder就可以实现许多特定的finder。flutter driver中的finder有如下几种:

  • ByTooltipMessage 通过工具提示组件定位
  • BySemanticsLabel 通过语义化标签
  • ByText 通过文字定位
  • ByValueKey 通过key进行定位
  • ByType 通过组件类型
  • PageBack 寻找Material或者Cupertino scaffold上的返回按钮
  • Descendant 通过子组件定位
  • Ancestor 通过父组件定位

当然这些finder仅仅是包含了一些定义,和序列化反序列化的方法,但是并没有具体的查找操作。此外,对于finder的构建,有一个专门的工厂mixin DeserializeFinderFactory。通过finderType来反序列化生成对应的finder。

mixin DeserializeFinderFactory {
  /// Deserializes the finder from JSON generated by [SerializableFinder.serialize].
  SerializableFinder deserializeFinder(Map<String, String> json) {
    final String? finderType = json['finderType'];
    switch (finderType) {
      case 'ByType': return ByType.deserialize(json);
      case 'ByValueKey': return ByValueKey.deserialize(json);
      case 'ByTooltipMessage': return ByTooltipMessage.deserialize(json);
      case 'BySemanticsLabel': return BySemanticsLabel.deserialize(json);
      case 'ByText': return ByText.deserialize(json);
      case 'PageBack': return const PageBack();
      case 'Descendant': return Descendant.deserialize(json, this);
      case 'Ancestor': return Ancestor.deserialize(json, this);
    }
    throw DriverError('Unsupported search specification type $finderType');
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

此外还有一个CreateFinderFactory的mixin,它用于从SerializableFinder创建Finder。而Finder是flutter_test中的一个抽象类。额,为什么要这样做呢,再往下看看应该就明白了。

# 常用Command/CommandWithTarget

手势相关:

  • Tap
  • Scroll 参数有每次移动的dx和dy,duration以及frequency
  • ScrollIntoView 滚动finder定位的widget的可滚动父组件,直到widget完全可见。

文本相关:

  • GetText
  • EnterText 这是一个Command,不包含finder属性

# CommandHandlerFactory

通过浏览Command相关的源码,可以看到Command只是一个定义,包含了一些属性,以及序列化和反序列化的方法,具体运行的操作,并没有包含其中。实际上运行的具体操作是在CommandHandlerFactory这个mixin中定义的。

可以简单看一下tap的实现。

Future<Result> _tap(Command command, WidgetController prober, CreateFinderFactoryfinderFactory) async {
  final Tap tapCommand = command as Tap;
  final Finder computedFinder = await waitForElement(
    finderFactory.createFinder(tapCommand.finder).hitTestable(),
  );
  await prober.tap(computedFinder);
  return Result.empty;
}
1
2
3
4
5
6
7
8

通过finderFactory创建flutter_test中的Finder,传入waitForElement函数。

  Future<Finder> waitForElement(Finder finder) async {
    if (_frameSync)
      await _waitUntilFrame(() => SchedulerBinding.instance!.transientCallbackCount == 0);

    await _waitUntilFrame(() => finder.evaluate().isNotEmpty);

    if (_frameSync)
      await _waitUntilFrame(() => SchedulerBinding.instance!.transientCallbackCount == 0);

    return finder;
  }
1
2
3
4
5
6
7
8
9
10
11

waitForElement函数里面主要调用了_waitUntilFrame方法。

  Future<void> _waitUntilFrame(bool Function() condition, [ Completer<void>? completer ]) {
    completer ??= Completer<void>();
    if (!condition()) {
      SchedulerBinding.instance!.addPostFrameCallback((Duration timestamp) {
        _waitUntilFrame(condition, completer);
      });
    } else {
      completer.complete();
    }
    return completer.future;
  }
1
2
3
4
5
6
7
8
9
10
11

这里通过使用Completer来生成一个Future,当bool闭包返回true将会完成这个Future,否则将会一直递归执行。

这一系列的操作都是为了确保能在当前Widget树中找到对应的控件。最后调用prober的tap方法来实现点击。有个很有意思的地方,我们前面看了flutter_driver里的手势相关的操作,只有tap和scroll,稍微基础一点的长按手势也没有支持,但是实际使用的是WidgetController里的tap方法,可WidgetController里是提供了longPress、drag等手势的实现。🤔Google一下,大多数人都是用scroll来验证longPress,感觉有一点点怪。此外WidgetControllerflutter_test包的中的类,看来要想知道具体怎么实现模拟点击那些操作,得看看flutter_test的源码。模拟长按的测试代码如下:

test('test button longpress', () async {
  final SerializableFinder btn = find.byValueKey('button');

  await driver.waitFor(btn);
  await driver.scroll(btn, 0, 0, Duration(milliseconds: 500));
  });
1
2
3
4
5
6

# FlutterDriver

FlutterDriver是一个抽象类,它有两个具体的实现WebFlutterDriverVMServiceFlutterDriver。以VMServiceFlutterDriver为例进行分析。

# connectTo

通过创建VmService client来连接到flutter应用。并通过client获取到main isolate。而创建VmService的在_waitAndConnect这个方法中。构造函数如下。

VmService VmService(
  Stream<dynamic> inStream,
  void Function(String) writeMessage, {
  Log? log,
  Future<dynamic> Function()? disposeHandler,
  Future<dynamic>? streamClosed,
})
1
2
3
4
5
6
7

通过WebSocket创建stream。

socket = await WebSocket.connect(webSocketUrl, headers: headers);
final StreamController<dynamic> controller = StreamController<dynamic>();
final Completer<void> streamClosedCompleter = Completer<void>();
socket.listen(
  (dynamic data) => controller.add(data),
  onDone: () => streamClosedCompleter.complete(),
);
final vms.VmService service = vms.VmService(
  controller.stream,
  socket.add,
  disposeHandler: () => socket!.close(),
  streamClosed: streamClosedCompleter.future
);
1
2
3
4
5
6
7
8
9
10
11
12
13

之后便是通过创建的VMService来发送指令。

final Future<Map<String, dynamic>> future = _serviceClient.callServiceExtension(
        _flutterExtensionMethodName,
        isolateId: _appIsolate.id,
        args: serialized,
);
1
2
3
4
5

调用callServiceExtension来调用特定服务的协议拓展,传入的args为序列化之后的Command信息。在调用这个方法之前,需要先在vmservice中注册相关的服务。这也就是为什么需要在main函数里enableFlutterDriverExtension()

# 注册服务

Flutter应用是运行在Dart VM上的,Dart VM内部提供了一套Web服务VMService,通过 JSON-RPC 2.0 (opens new window) 协议来访问Dart VM服务协议的库,使用VMService可以帮助我们获取app中的数据。Flutter中使用registerServiceExtension方法来完成服务注册,之后我们就可以在app 外部通过VMService来调用对应的服务。

void registerServiceExtension({
  required String name,
  required ServiceExtensionCallback callback,
}) {
  final String methodName = 'ext.flutter.$name';
  developer.registerExtension(methodName, (String method, Map<String, String> parameters) async {
    // 代码省略
    late Map<String, dynamic> result;
    try {
      result = await callback(parameters);
    } catch (exception, stack) {
      
    }
    result['type'] = '_extensionType';
    result['method'] = method;
    return developer.ServiceExtensionResponse.result(json.encode(result)); 
  });
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

registerServiceExtension 就是注册方法,接受的入参就是服务名字回调

服务名字:就是 FlutterDart Vm 能够认识的服务标示,方法名字就是 VM 可以调用到的名字。

回调:就是 VM 调用服务名字时,Flutter 做出的反应

这里注意一点,我们传递的名字会被 包装成 ext.flutter.$名字 的形式。

注册会调用 developerregisterExtension 方法。developer 是一个开发者包,里面有一个比较基础的 API。最后调用的是native的一个方法。

external _registerExtension(String method, ServiceExtensionHandler handler);
1

# _DriverBinding

flutter_driver里注册服务是在enableFlutterDriverExtension函数里完成的,里面调用了_DriverBinding的构造函数。

class _DriverBinding extends BindingBase with SchedulerBinding, ServicesBinding, GestureBinding, PaintingBinding, SemanticsBinding, RendererBinding, WidgetsBinding, TestDefaultBinaryMessengerBinding {
  _DriverBinding(this._handler, this._silenceErrors, this._enableTextEntryEmulation, this.finders, this.commands);

  final DataHandler? _handler;
  final bool _silenceErrors;
  final bool _enableTextEntryEmulation;
  final List<FinderExtension>? finders;
  final List<CommandExtension>? commands;

  
  void initServiceExtensions() {
    super.initServiceExtensions();
    final FlutterDriverExtension extension = FlutterDriverExtension(_handler, _silenceErrors, _enableTextEntryEmulation, finders: finders ?? const <FinderExtension>[], commands: commands ?? const <CommandExtension>[]);
    registerServiceExtension(
      name: _extensionMethodName,
      callback: extension.call,
    );
    if (kIsWeb) {
      registerWebServiceExtension(extension.call);
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

那么看代码可能有点迷惑。首先我们从它的父类入手。因为在调用子类的构造函数的时候,会调用父类的构造函数。下面是BindingBase的构造函数,里面

BindingBase() {
  developer.Timeline.startSync('Framework initialization');
  assert(!_debugInitialized);

  initInstances();
  assert(_debugInitialized);
  assert(!_debugServiceExtensionsRegistered);

  initServiceExtensions();

  assert(_debugServiceExtensionsRegistered);
  developer.postEvent('Flutter.FrameworkInitialization', <String, String>{});
  developer.Timeline.finishSync();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

在构造函数中调用了initServiceExtensions方法,这对应了_DriverBinding中重写的initServiceExtensions方法。并在此方法中进行服务注册,将FlutterDriverExtension的call方法作为参数传入。在call方法中,根据params中的Command信息,调用handleCommand方法进行处理。而handleCommand方法是通过混入CommandHandlerFactory得到的,并通过重写添加了对command的一些判断。

  
  Future<Result> handleCommand(Command command, WidgetController prober, CreateFinderFactory finderFactory) {
    final String kind = command.kind;
    if (_commandExtensions.containsKey(kind)) {
      return _commandExtensions[kind]!.call(command, prober, finderFactory, this);
    }

    return super.handleCommand(command, prober, finderFactory);
  }
1
2
3
4
5
6
7
8
9

这里的设计挺好的,可以借鉴一下,_commandExtensions的存在提供了外部添加自定义Command的接口,也就是在FlutterDriverExtension构造函数中的一个可选参数command,通过在此传入一些自定义的Command,可以实现自定义Command和对应处理方法的功能。

最后再提一嘴,在FlutterDriverExtension构造函数中还做了一件事,那就是registerTextInput。有了它才可以输入框中进行输入操作。

# 总结一下

最后大概总结一下flutter_driver的原理。首先,在需要进行UI自动化测试的Flutter应用的main函数运行前,先向main isolate中注册我们的服务,也就是FlutterDriverExtension中的call方法,用于处理Command。然后是通过websocket,创建vmservice的client,连接到我们的flutter应用。之后就是通过这个client来调用服务拓展,将Command传递过去。具体的操作都是flutter 应用结合flutter_test包来完成的。

# 参考文章

Last Updated: 2022/6/8 02:11:24