CodingMeUp / AboutFE

知识归纳、内容都在issue里
74 stars 14 forks source link

31、跨端通信桥接Bridge、WebView #32

Open CodingMeUp opened 4 years ago

CodingMeUp commented 4 years ago

RNBridge桥接是什么?

RN可以分成JS 和 Native两部分,这两部分都有自己的线程

线程通信是通过Bridge通信,传输的是JSON信息,其中包含了module id,method id和一些需要的数据。这两边并不能直接地相互感知,也不能共享相同的内存。

很多人认为队列是一个很好的解决方案。你发送JSON/XML队列消息,这个消息遵从相应的协议,并且每个服务都知道怎么去读取和解析成对应的数据和行为。这个队列就很像RN里面的Bridge。

path: node_modules/react-native/Libraries/BatchedBridge/MessageQueue.js

import MessageQueue from 'react-native/Libraries/BatchedBridge/MessageQueue'
MessageQueue.spy(true);

js代码和objective-c之间的通信。RN在ios上使用的是内置的JSCore,在Android需要格外编译一个Js引擎 JS引擎知道如何去更加高效地把JS转换成机器语言。

这也是为什么Android App需要更多的时间去加载,因为需要加载JSCore到项目里。不过在0.60.2之后,RN可以使用Hermes,这对于RN来说是更理想的JS引擎。

RN 桥接bridge怎么工作的

关键概念名词解释

  1. UIManager:在Native侧,是在iOS/Android里主要运行的线程。只有它有权限可以修改客户端UI。
  2. JS Thread:运行打包好的main.bundle.js文件,这个文件包含了RN的所有业务逻辑、行为和组件。
  3. Shadow Node/Tree:在Native层的一个组件树,可以帮助监听App内的UI变化,有点像ReactJS里的虚拟Dom和Dom之间的关系。
  4. Yoga:用来计算layout。是Facebook写的一个C引擎,用来把基于Flexbox的布局转换到Native的布局系统。

打开App时,每一步发生了什么:

  1. 用户点击App的图标

  2. UIManager线程:加载所有的Native库和Native组件比如 Text、Button、Image等

  3. 告诉Js线程,Native部分准备好了,Js侧开始加载main.bundle.js,这里面包含了所有的js和react逻辑以及组件。

  4. Js侧通过Bridge发送一条JSON消息到Native侧,告诉Native怎么创建UI。值得一提的是:所有经过Bridge的通信都是异步的,并且是打包发送的。这是为了避免阻塞UI,举个栗子: avatar

  5. Shadow线程最先拿到消息,然后创建UI树

  6. 它使用Yoga布局引擎去获取所有基于flex样式的布局,并且转化成Native的布局,宽、高、间距等。

  7. UIManager执行一些操作并且像这样在屏幕上展示UI

RN目前架构的优缺点

RN基于这个架构有以下优点:

  1. UI不会被阻塞:用户感觉到更加流畅
  2. 不需要写Native侧的代码:使用RN库的话,很多代码可以只写JavaScript的
  3. 性能更加接近Native
  4. 整个流程是完整的。开发者不用去控制并且完全了解它

但是,有优点肯定也会有缺点。下面我们介绍一下能够解决现有缺点的新架构:Fabric 。 当前架构的缺点是:

  1. 有两个不同的领域:JS和Native,他们彼此之间并不能真正互相感知,并且也不能共享相同的内存。
  2. 它们之间的通信是基于Bridge的异步通信。但是这也意味着,并不能保证数据100%并及时地到达另一侧。
  3. 传输大数据非常慢,因为内存不能共享,所有在js和native之间传输的数据都是一次新的复制。
  4. 无法同步更新UI。比方说有个FlatList,当我滑动的时候会加载大量的数据。在某些边界场景,当有用户界面交互,但数据还没有返回,屏幕可能会发生闪烁。
  5. RN代码仓库太大了。导致库更重,开源社区贡献代码或发布修复也更慢。

avatar

avatar

Fabric http://facebook.github.io/react-native/blog/2018/06/14/state-of-react-native-2018 Fabric新的概念:JSI,Fabric,Turbo Modules,和 CodeGen。

  1. JSI(将会替换Bridge) -- 为了让JS和Native能够互相感知。将不再需要通过Bridge传输序列化JSON。将允许Native对象被导出成Js对象,反过来也可以。两侧也会导出可以被同步调用的API。实际上,架构的其他部分都是基于这个之上的(Fabric,Turbo Modules等,这些下面会解释)
  2. Fabric -- UIManager的新名称,将负责Native端渲染。和当前的Bridge不同的是,它可以通过JSI导出自己的Native函数,在JS层可以直接使用这些函数引用,反过来,Native层也可以直接调用JS层。这带来更好更高效的性能和数据传输。
  3. Turbo Modules。记得上面的Native组件吗?Text、Image、View,他们的新名字叫Turbo Modules。组件的作用是相同的,但是实现和行为会不同。第一,他们是懒加载的(只有当App需要的时候加载),而现在是在启动时全部加载。另外,他们也是通过JSI导出的,所以JS可以拿到这些组件的引用,并且在React Natvie JS里使用他们。尤其会在启动的时候带来更好的性能表现。
  4. CodeGen -- 为了让JS侧成为两端通信时的唯一可信来源。它可以让开发者创建JS的静态类,以便Native端(Fabric和Turbo Modules)可以识别它们,并且避免每次都校验数据 => 将会带来更好的性能,并且减少传输数据出错的可能性。
  5. Lean Core -- 是对React Native库架构的变化。目的是减轻lib的负担,并帮助社区更快地解决更多pull request。Facebook正在把库的某些部分拆分出来,通过关注Github就可以看出他们在这方面的行动了。比如这个: avatar

使用新架构,App启动的流程是怎么样的。

  1. 用户点击App的图标
  2. Fabric加载Native侧(没有Native组件, Turbo Modules 懒加载)
  3. 然后通知JS线程Native侧准备好了,JS侧会加载所有的main.bundle.js,里面包含了所有的js和react逻辑+组件
  4. JS通过一个Native函数的引用(JSI API导出的)调用到Fabric,同时Shadow Node创建一个和以前一样的UI树。
  5. Yoga执行布局计算,把基于Flexbox的布局转化成终端的布局。
  6. Fabric执行操作并且显示UI==>

为了完成整个流程,我们几乎做了同样的事情,但是没有了Bridge,现在我们可以有更好的性能,我们可以用同步的方式进行操作,甚至可以对UI上的同步操作进行优先级排序。启动时间也将更快,App也将更小。

CodingMeUp commented 4 years ago

JSBridge 的通信原理

Hybrid 方案是基于 WebView 的,JavaScript 执行在 WebView 的 Webkit 引擎中。因此,Hybrid 方案中 JSBridge 的通信原理会具有一些 Web 特性。

JavaScript 调用 Native

JavaScript 调用 Native 的方式,主要有两种:注入 API拦截 URL SCHEME

context[@"postBridgeMessage"] = ^(NSArray<NSArray > calls) { // Native 逻辑 }; // iOS 的 WKWebView @interface WKWebVIewVC () @implementation WKWebVIewVC

// 前端调用 // uiwebview: window.postBridgeMessage(message); // wkwebview:
window.webkit.messageHandlers.nativeBridge.postMessage(message); // 安卓webview: window.nativeBridge.postMessage(message);


- 拦截 URL SCHEME
先解释一下 URL SCHEME:URL SCHEME是一种类似于url的链接,是为了方便app直接互相调用设计的,形式和普通的 url 近似,主要区别是 protocol 和 host 一般是自定义的,例如: qunarhy://hy/url?url=http://ymfe.tech,protocol 是 qunarhy,host 则是 hy。

拦截 URL SCHEME 的主要流程是:Web 端通过某种方式(例如 iframe.src)发送 URL Scheme 请求,之后 Native 拦截到请求并根据 URL SCHEME(包括所带的参数)进行相关操作。
在时间过程中,这种方式有一定的**缺陷**:
1. 使用 iframe.src 发送 URL SCHEME 会有 url 长度的隐患。
2. 创建请求,需要一定的耗时,比注入 API 的方式调用同样的功能,耗时会较长。

#### Native 调用 JavaScript

相比于 JavaScript 调用 Native, Native 调用 JavaScript 较为简单,毕竟不管是 iOS 的 UIWebView 还是 WKWebView,还是 Android 的 WebView 组件,都以子组件的形式存在于 View/Activity 中,直接调用相应的 API 即可。

Native 调用 JavaScript,其实就是执行拼接 JavaScript 字符串,从外部调用 JavaScript 中的方法,因此 JavaScript 的方法必须在全局的 window 上。(闭包里的方法,JavaScript 自己都调用不了,更不用想让 Native 去调用了)

```js
// iOS 的 UIWebView
result = [uiWebview stringByEvaluatingJavaScriptFromString:javaScriptString];

// iOS 的 WKWebView
[wkWebView evaluateJavaScript:javaScriptString completionHandler:completionHandler];

// Android,在 Kitkat(4.4)之前并没有提供 iOS 类似的调用方式,只能用 loadUrl 一段 JavaScript 代码, 使用 loadUrl 的方式,并不能获取 JavaScript 执行后的结果
webView.loadUrl("javascript:" + javaScriptString);

// 而 Kitkat 之后的版本,也可以用 evaluateJavascript 方法实现
webView.evaluateJavascript(javaScriptString, new ValueCallback<String>() {
    @Override
    public void onReceiveValue(String value) { }
});

通信原理小总结

通信原理是 JSBridge 实现的核心,实现方式可以各种各样,但是万变不离其宗。推荐的实现方式如下:

对于其他方式,诸如 React Native、微信小程序 的通信方式都与上描述的近似,并根据实际情况进行优化。

以 React Native 的 iOS 端举例:JavaScript 运行在 JSCore 中,实际上可以与上面的方式一样,利用注入 API 来实现 JavaScript 调用 Native 功能。不过 React Native 并没有设计成 JavaScript 直接调用 Object-C,而是为了与 Native 开发里事件响应机制一致,设计成 需要在 Object-C 去调 JavaScript 时才通过返回值触发调用。原理基本一样,只是实现方式不同。

JSbridge接口实现(类似DD fusion)

JSBridge 的接口主要功能有两个:

因此,JSBridge 可以设计如下:

window.JSBridge = {
    // 调用 Native
    invoke: function(msg) {
        // 判断环境,获取不同的 nativeBridge
        nativeBridge.postMessage(msg);
    },
    receiveMessage: function(msg) {
        // 处理 msg
    }
};

RPC 中有一个非常重要的环节是 句柄解析调用 ,这点在 JSBridge 中体现为 句柄与功能对应关系。同时,我们将句柄抽象为 桥名(BridgeName),最终演化为一个 BridgeName 对应一个 Native 功能或者一类 Native 消息 基于此点,JSBridge 的实现可以优化为如下

window.JSBridge = {
    // 调用 Native
    invoke: function(bridgeName, data) {
        // 判断环境,获取不同的 nativeBridge
        nativeBridge.postMessage({
            bridgeName: bridgeName,
            data: data || {}
        });
    },
    receiveMessage: function(msg) {
        var bridgeName = msg.bridgeName,
            data = msg.data || {};
        // 具体逻辑
    }
};

JSBridge 大概的雏形出现了。消息都是单向的,那么对于调用 Native 功能时 JSBridge 的Callback 实现 ,其实就是 RPC 框架的回调机制。当然也可以用更简单的 JSONP 机制解释:

当发送 JSONP 请求时,url 参数里会有 callback 参数,其值是 当前页面唯一 的,而同时以此参数值为 key 将回调函数存到 window 上,随后,服务器返回 script 中,也会以此参数值作为句柄,调用相应的回调函数

由此可见,callback 参数这个 唯一标识 是这个回调逻辑的关键。这样,我们可以参照这个逻辑来实现 JSBridge:用一个自增的唯一 id,来标识并存储回调函数,并把此 id 以参数形式传递给 Native,而 Native 也以此 id 作为回溯的标识。这样,即可实现 Callback 回调逻辑。

(function () {
    var id = 0,
        callbacks = {},
        registerFuncs = {};

    window.JSBridge = {
        // 调用 Native
        invoke: function(bridgeName, callback, data) {
            // 判断环境,获取不同的 nativeBridge
            var thisId = id ++; // 获取唯一 id
            callbacks[thisId] = callback; // 存储 Callback
            nativeBridge.postMessage({
                bridgeName: bridgeName,
                data: data || {},
                callbackId: thisId // 传到 Native 端
            });
        },
        receiveMessage: function(msg) {
            var bridgeName = msg.bridgeName,
                data = msg.data || {},
                callbackId = msg.callbackId, // Native 将 callbackId 原封不动传回
                responstId = msg.responstId;
            // 具体逻辑
            // bridgeName 和 callbackId 不会同时存在
            if (callbackId) {
                if (callbacks[callbackId]) { // 找到相应句柄
                    callbacks[callbackId](msg.data); // 执行调用
                }
            } else if (bridgeName) {
                if (registerFuncs[bridgeName]) { // 通过 bridgeName 找到句柄
                    var ret = {},
                        flag = false;
                    registerFuncs[bridgeName].forEach(function(callback) => {
                        callback(data, function(r) {
                            flag = true;
                            ret = Object.assign(ret, r);
                        });
                    });
                    if (flag) {
                        nativeBridge.postMessage({ // 回调 Native
                            responstId: responstId,
                            ret: ret
                        });
                    }
                }
            }
        },
        register: function(bridgeName, callback) {
            if (!registerFuncs[bridgeName])  {
                registerFuncs[bridgeName] = [];
            }
            registerFuncs[bridgeName].push(callback); // 存储回调
        }
    };
})();

对于 Native 端涉及的并不多。在 Native 端配合实现 JSBridge 的 JavaScript 调用 Native 逻辑也很简单,主要的代码逻辑是:

JSBridge 如何引用

  1. 由 Native 端进行注入 注入方式和 Native 调用 JavaScript 类似,直接执行桥的全部代码。
  1. 由 JavaScript 端引用 直接与 JavaScript 一起执行。

与由 Native 端注入正好相反,