首页
学习
活动
专区
工具
TVP
发布
精选内容/技术社群/优惠产品,尽在小程序
立即前往

最右JS2Flutter框架——通信机制

1、概述

通信包括异步和同步两种方式,异步可根据是否关注返回结果再细分成Request-Reply和One-Way两种。JS2Flutter框架的通信机制也是在不断的迭代中逐步完善,年初发的文章Flutter动态化在最右App中的实践[1]中还是通过对JSWorkThread的阻塞,去实现Client侧到Host侧的单向同步,如今已经完成了Client和Host之间真正意义上的双向同步直连。

2、异步

2.1 One-Way

对于单向无需关注返回结果的调用,实现起来很简单,在最右JS2Flutter框架——开篇[2]中,我们讲述了Hello World实现的例子,这就是一个最简单的One-Way调用,Client和Native之间可以通过建立通信,Native和Host之间可以通过PlatformChannel建立通信,这样就完成Client和Host之间的通信。

2.2 Request-Reply

对于One-Way的调用,很容易就可以实现,假如我们需要一个返回结果呢?

举个例子,开发者想要获取剪切板数据,调用了Clipboard.getData,在运行阶段这个函数是在JS运行的,它必须向Host侧真实的Clipboard.getData发起调用,Host侧在拿到结果之后,通知Client侧,Client侧把结果给调用者。

很容易想到Client侧通过JSCore向Native注册回调函数,Native通过MethodChannel设置MethodCallHandler,当Native收到Host侧的回调时,Native调用Client侧的回调函数。JS2Flutter框架就是这样去实现的,但却又不止于此,仅仅使用回调函数来做异步的话,当遇到大量的异步回调场景时,很可能陷入"回调地狱",其实Dart为了避免类似的问题,引入了Future,JS也引入了Promise。回调函数只作为Native回调Client侧的一个句柄,真正在框架中发挥威力的是通过Future和Completer去实现的callFlutterWithReply。

代码语言:javascript
复制
Map<int, Completer> _completerMap = new Map();

Future<T> callFlutterWithReply<T>(String method, { dynamic key, String action, dynamic data }) async {
  int callbackId = generateGlobalId();
  invokeFlutter(method, _buildBody(key, action, data, callbackId));
  Completer completer = Completer<dynamic>();
  _completerMap[callbackId] = completer;
  T typedResult = await completer.future;
  return typedResult;
}

//当收到Native回调
...
    Completer completer = _completerMap[callbackId];
    if(completer != null) {
      completer.complete(data);
      _completerMap.remove(callbackId);
    }
...

至此,Client侧面向Host侧的异步调用便建立起来了,同理,可建立Host侧面向Client侧的异步调用,这样就完成了双向的异步通信机制。

3、同步

同步的需求场景也很多,我们在Flutter动态化在最右App中的实践[1]中也有列举,比如你要通过TextPainter来测量文字的高度,是需要同步机制才能保证正确性的,当时我们是通过阻塞JSWorkThread去实现Client侧的同步,因为这是一个与UI绘制无关的线程,所以它的阻塞并不影响渲染和事件接收,但后来我们发现同步机制不仅是Client侧需要的,Host侧也有可能需要。

举个例子,我们在实现ListView控件时,如果是通过builder方式去构建,当遇到大量数据的时候,因为每一项都是实时构建,需要先询问Client侧该位置的子树并立即还原,如果是异步去刷新的话,很可能后几项先显示出来,从而造成视觉上的错乱。所以,在Host侧我们也需要建立同步机制。

那在Host侧是否可以采用同样的方案建立同步机制呢?当然也是可行的,但最右没有采用这种方案,试想一下,本来JS到Flutter就需要经历JSWorkThread、MainThread、Flutter UIThread,再来一层循环阻塞,效率肯定是大打折扣。我们采用了更高效的方式,让JS运行在Flutter的UI线程里,构建一套Client和Host之间直连的通信渠道,这样就能实现真正意义上的双向同步。

这个过程中我们需要解决两个问题:

1:如何让代码块儿能在Flutter UI线程执行?

2:怎样才能建立直连通信渠道?

要解决第一个问题,我们需要了解Flutter UI线程Work的方式,这里直接借用Gityuan的博客——Flutter渲染机制——UI线程[3],Engine是通过UITaskRunner提交任务,从而让任务在UI线程执行。理解了这一点,我们就可以扩充FlutterEngine的能力,增加一个postTaskOnUIThread函数,XCJSRuntime在启动时,让JS运行在Flutter UI线程。

第二个问题相对要复杂的多,牵涉到的面也比较广,我们先看看系统是如何建立通信渠道的,不了解的同学可以看看Gityuan的博客——深入理解Flutter的Platform Channel机制[4],理解了系统的流程(博客以Android为例,iOS侧同理)之后,首先要思考的就是我们与系统不一样的地方,Platform Channel是为了在Flutter和Platform之间建立通信渠道,它们在不同的线程,而我们是在同一个线程,所以我们不需要再去转线程。举个例子:

代码语言:javascript
复制
// |PlatformView::Delegate|
void Shell::OnPlatformViewDispatchPlatformMessage(
    fml::RefPtr<PlatformMessage> message) {
  FML_DCHECK(is_setup_);
  FML_DCHECK(task_runners_.GetPlatformTaskRunner()->RunsTasksOnCurrentThread());

  task_runners_.GetUITaskRunner()->PostTask(
      [engine = engine_->GetWeakPtr(), message = std::move(message)] {
        if (engine) {
          engine->DispatchPlatformMessage(std::move(message));
        }
      });
}

// |PlatformView::Delegate|
void Shell::OnPlatformViewDispatchDirectMessage(
    fml::RefPtr<DirectMessage> message) {
  FML_DCHECK(is_setup_);
  FML_DCHECK(task_runners_.GetUITaskRunner()->RunsTasksOnCurrentThread());

  engine_->DispatchDirectMessage(std::move(message));
}

有了这个基础之后,我们就可以在MethodChannel上拓展一个invokeDirectMethod函数,参照系统的实现方式,修改Dart层和Engine层,修改的路线也基本就是MethodChannel的调用流程。

这里我摘取了_DefaultBinaryMessenger中增加直连的修改,其后续的实现步骤基本上都是跟随着PlatformChannel的流程,增加直连的函数。

代码语言:javascript
复制
class _DefaultBinaryMessenger extends BinaryMessenger {
  const _DefaultBinaryMessenger._();

  // Handlers for incoming messages from platform plugins.
  // This is static so that this class can have a const constructor.
  static final Map<String, MessageHandler> _handlers =
      <String, MessageHandler>{};

  static final Map<String, DirectMessageHandler> _directHandlers =
      <String, DirectMessageHandler>{};    

  // Mock handlers that intercept and respond to outgoing messages.
  // This is static so that this class can have a const constructor.
  static final Map<String, MessageHandler> _mockHandlers =
      <String, MessageHandler>{};

  Future<ByteData> _sendPlatformMessage(String channel, ByteData message) {
    final Completer<ByteData> completer = Completer<ByteData>();
    // ui.window is accessed directly instead of using ServicesBinding.instance.window
    // because this method might be invoked before any binding is initialized.
    // This issue was reported in #27541. It is not ideal to statically access
    // ui.window because the Window may be dependency injected elsewhere with
    // a different instance. However, static access at this location seems to be
    // the least bad option.
    ui.window.sendPlatformMessage(channel, message, (ByteData reply) {
      try {
        completer.complete(reply);
      } catch (exception, stack) {
        FlutterError.reportError(FlutterErrorDetails(
          exception: exception,
          stack: stack,
          library: 'services library',
          context: ErrorDescription('during a platform message response callback'),
        ));
      }
    });
    return completer.future;
  }

  ByteData _sendDirectMessage(String channel, ByteData message) {
    return ui.window.sendDirectMessage(channel, message);
  }


  @override
  Future<void> handlePlatformMessage(
    String channel,
    ByteData data,
    ui.PlatformMessageResponseCallback callback,
  ) async {
    ByteData response;
    try {
      final MessageHandler handler = _handlers[channel];
      if (handler != null) {
        response = await handler(data);
      } else {
        ui.channelBuffers.push(channel, data, callback);
        callback = null;
      }
    } catch (exception, stack) {
      FlutterError.reportError(FlutterErrorDetails(
        exception: exception,
        stack: stack,
        library: 'services library',
        context: ErrorDescription('during a platform message callback'),
      ));
    } finally {
      if (callback != null) {
        callback(response);
      }
    }
  }


  @override
  void handleDirectMessage(
      String channel,
      ByteData data,
      ui.DirectMessageResultCallback callback,
      ) async {
    ByteData result;
    try {
      final DirectMessageHandler handler = _directHandlers[channel];
      if (handler != null)
        result = handler(data);
    } catch (exception, stack) {
      FlutterError.reportError(FlutterErrorDetails(
        exception: exception,
        stack: stack,
        library: 'services library',
        context: ErrorDescription('during a direct message callback'),
      ));
    } finally {
      callback(result);
    }
  }


  @override
  Future<ByteData> send(String channel, ByteData message) {
    final MessageHandler handler = _mockHandlers[channel];
    if (handler != null)
      return handler(message);
    return _sendPlatformMessage(channel, message);
  }

  @override
  ByteData sendDirect(String channel, ByteData message) {
    return _sendDirectMessage(channel, message);
  }

  @override
  void setMessageHandler(String channel, MessageHandler handler) {
    if (handler == null)
      _handlers.remove(channel);
    else
      _handlers[channel] = handler;
    ui.channelBuffers.drain(channel, (ByteData data, ui.PlatformMessageResponseCallback callback) async {
      await handlePlatformMessage(channel, data, callback);
    });
  }

  @override
  void setDirectMessageHandler(String channel, DirectMessageHandler handler) {
    if (handler == null)
      _directHandlers.remove(channel);
    else
      _directHandlers[channel] = handler;
  }

  ...
}

4、结束语

本⽂阐述了最右JS2Flutter框架的通信机制,基本上都是参照了Flutter的实现⽅式,对于同步直连的实现过程,涉及到的环节比较多,也只阐述了思想,对细节实现感兴趣的同学可追溯源码一探究竟。

5、参考文献

[1]:Flutter动态化在最右App中的实践 https://www.infoq.cn/article/MIa5AN2JE51uor4JeiPG

[2]:JS2Flutter框架——开篇 https://xie.infoq.cn/article/acee65b914dc4d0e32a5561a1

[3]:Flutter渲染机制——UI线程 http://gityuan.com/2019/06/15/flutter_ui_draw/

[4]:深入理解Flutter的Platform Channel机制 http://gityuan.com/2019/08/10/flutter_channel/

  • 发表于:
  • 本文为 InfoQ 中文站特供稿件
  • 首发地址https://www.infoq.cn/article/f23e562e3aa7f3c198eb40a83
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。

扫码

添加站长 进交流群

领取专属 10元无门槛券

私享最新 技术干货

扫码加入开发者社群
领券