likes
comments
collection
share

NativeBridge:实现原理解析

作者站长头像
站长
· 阅读数 60

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第2天,点击查看活动详情

系列(一):NativeBridge:基于webivew_flutter的JSBridge插件 系列(二):NativeBridge:实现原理解析 系列(三):App实现JSBridge的最佳方案 系列(四):NativeBridge:我在Pub上发布的第一个插件

Tip: 目前 NativeBridge 插件已迭代更新多个版本,该文章源码为 Tag: v0.0.1

前沿

上一篇文章《NativeBridge:基于webivew_flutter的JSBridge插件》有讲到 NativeBridge 的集成和使用。趁热打铁,我们今天解析下 NativeBridge实现原理超时检测机制

webview_flutter 插件分析

在说 NativeBridge 之前,我们先了解下 webview_flutter,毕竟 NativeBridge 是对其能力的拓展。

webview_flutter 是提供一个 WebView Widget 的 Flutter 插件。在 iOS 上,WebView Widget 由 WKWebView 支持;在 Android 上,WebView Widget 由 WebView 支持。官方原文:

A Flutter plugin that provides a WebView widget.

On iOS the WebView widget is backed by a WKWebView; On Android the WebView widget is backed by a WebView.

JSBridge 的调用。其通过 javascriptChannels 实现对 H5 端 JSBridge 调用的支持。代码如下:

  WebView(
    ...
    javascriptMode: JavascriptMode.unrestricted,
    javascriptChannels: <JavascriptChannel>{
      _toasterJavascriptChannel(context),
    },
  );

它最大的局限在于 App 端与 H5 端之间的通信只支持单向通信,无法直接获取另一端的返回值。如官方 example 中的实现:


// (1)通过 runJavascript 执行H5端方法,向App端发送 userAgent 信息
  Future<void> _onShowUserAgent(
      WebViewController controller, BuildContext context) async {
    // Send a message with the user agent string to the Toaster JavaScript channel we registered
    // with the WebView.
    await controller.runJavascript(
        'Toaster.postMessage("User Agent: " + navigator.userAgent);');
  }
  
// (2)接收H5端的消息
  JavascriptChannel _toasterJavascriptChannel(BuildContext context) {
    return JavascriptChannel(
        name: 'Toaster',
        onMessageReceived: (JavascriptMessage message) {
          ScaffoldMessenger.of(context).showSnackBar(
            SnackBar(content: Text(message.message)),
          );
        });
  }
  
  

NativeBridge 的源码分析

NativeBridge 本质上是对 webview_flutter 的单向通信能力进行扩展封装

内部由 NativeBridgeImplNativeBridgeHelperNativeBridge 三个类以及一个实体类 Message 构成,结构比较简单。

NativeBridge:实现原理解析

Message实体类。主要定义了需要调用的方法 api、数据 data 和用于执行返回操作的标识 callbackId。

  Message({
    required this.api,  // 方法API
    this.data,		// 发送的数据
    this.callbackId,	// 返回操作标识
  });

NativeBridgeImpl。是一个抽象类,定义了一套需要用户实现的协议。包括:javascript 的 name、支持的 callMethodMap 集合和执行 JS 的 runJavascript() 方法。

abstract class NativeBridgeImpl {
  NativeBridgeImpl({required this.name, required this.callMethodMap});

  /// javascript channel name
  final String name;
  
  /// call method map
  final Map<String, Function?> callMethodMap;

  /// 执行JS
  void runJavascript(String javaScriptString);

}

NativeBridge。实现 WebView 的 JavascriptChannel 类用于 JSBridge 的初始化,其中 name 和 onMessageReceived 都是通过 NativeBridgeImpl 代理实现。

class NativeBridge implements JavascriptChannel {
  NativeBridge({required this.controller});

  final NativeBridgeImpl controller;

  @override
  String get name => controller.name;

  @override
  JavascriptMessageHandler get onMessageReceived => (message) async {
        ...
        controller.runJavascript("receiveMessage($json)");
        }
      };
}

NativeBridgeHelper。顾名思义是 NativeBridge 的帮助类,主要实现发送消息和接受消息的处理,并根据 callbackId 标识进行 Future 的回调。

  /// 发送消息
  static Completer sendMessage(Message message, NativeBridgeImpl nativeBridgeImpl) {
    Completer completer = Completer();
    var callbackId = _pushCallback(message.api, completer);
    message.callbackId = callbackId;
    // H5接受消息
    final res = messageToJson(message);
    nativeBridgeImpl.runJavascript("receiveMessage($res)");
    return completer;
  }
  
  /// 接收消息
  static void receiveMessage(String json) {
    var map = jsonDecode(json);
    var callbackId = map["callbackId"];
    var data = map["data"];
    var completer = _popCallback(callbackId);
    completer?.complete(Future.value(data));
  }

NativeBridge 双向通信的实现原理

通过 example 的例子可以看到,我们可以直接通过 NativeBridgeHelper 获取到 H5 端的返回值。那么它是如何实现的呢?答案是 Completer

 var isHome = await NativeBridgeHelper.sendMessage(
                        Message(api: "isHome"), 
                        _nativeBridgeController,
                    ).future;

Completer。简单来说就是生成一个异步的 Future,允许我们在接受到 H5 端的消息后进行异步回调。就像网络请求一样,发送请求后等待服务器响应请求并返回数据。(Tip:我们用到的 dio 网络库也是通过 Completer 来实现异步响应)

用上面的获取 H5 端的 isHome 值为例。我们先发送一条需要获取 isHome 的消息,然后异步等待 H5 端接收消息并进行处理,H5 端接收到消息后,根据收到的 api 回复一条携带返回值的消息给 App 端,App 端再通过 callbackId 查询到前面缓存的 Completer 执行 complete() 方法调用,最后获取到我们想要的值。

执行流程如下:

NativeBridge:实现原理解析

(1)App 端根据发送的消息创建 Completer 并缓存到 Completer 栈,给 H5 端发送消息。

    // native_bridge_helper.dart  sendMessage()
    Completer completer = Completer();
    var callbackId = _pushCallback(message.api, completer);
    message.callbackId = callbackId;
    // H5接受消息
    final res = messageToJson(message);
    nativeBridgeImpl.runJavascript("receiveMessage($res)");

(2)H5 端接收 App 端的消息,根据消息的 api 回复一条消息。

    // jsBridgeHelper.js  receiveMessage()
    if (message.api === 'isHome') {
        this._postMessage(message.api, true.toString(), message.callbackId)
    }

(3)App 端接收到 H5 端的消息后,根据 callbackId 获取到缓存的 Completer 并执行 complete() 完成回调。

    // native_bridge_helper.dart  receiveMessage()
    var map = jsonDecode(json);
    var callbackId = map["callbackId"];
    var data = map["data"];
    var completer = _popCallback(callbackId);
    completer?.complete(Future.value(data));

至此,完成了从 App 端获取 H5 端值的整个流程。

NativeBridge 的超时机制

就像网络请求一样,我们不能让代码执行一直阻塞在获取返回值的位置上。因为单向发送消息是不可靠的,可能存在消息丢失,或者 H5 端不响应消息的情况。因此我们需要类似网络请求一样,新增超时机制

在 NativeBridge 的 sendMessage() 方法中通过设置倒计时,一旦超过倒计时还没有响应时,我们就将当前的 Completer 取出并执行 complete(Future.value(null)) 表示没有获取到对应的值。

    // 增加回调异常容错机制,避免消息丢失导致一直阻塞
    Future.delayed(const Duration(milliseconds: 200), (){
      var completer = _popCallback(callbackId);
      completer?.complete(Future.value(null));
    });

对于 H5 端的实现

对于 H5 端的实现和 App 端同理,只是将 Completer 替换成了 Promise,其余逻辑和处理都相同。具体实现参考 example/assets/test/jsBridgeHelper.js

总结

我们先通过分析 webview_flutter 插件的实现和存在的缺陷,引申出 NativeBridge 的作用和源码构成,再通过 Completer 来实现异步 future 的调用,最后介绍了添加超时机制的理由和具体实现。至此,我们完成了整个插件的实现原理解析。

转载自:https://juejin.cn/post/7172840863234523173
评论
请登录