AboutFE icon indicating copy to clipboard operation
AboutFE copied to clipboard

31、跨端通信桥接Bridge、WebView

Open CodingMeUp opened this issue 5 years ago • 1 comments

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 avatar Dec 30 '19 07:12 CodingMeUp

JSBridge 的通信原理

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

JavaScript 调用 Native

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

  • 注入API 注入 API 方式的主要原理是,通过 WebView 提供的接口,向 JavaScript 的 Context(window)中注入对象或者方法,让 JavaScript 调用时,直接执行相应的 Native 代码逻辑,达到 JavaScript 调用 Native 的目的。
// 对于 iOS 的 UIWebView,实例如下:
JSContext *context = [uiWebView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];

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

- (void)viewDidLoad {
    [super viewDidLoad];

    WKWebViewConfiguration* configuration = [[WKWebViewConfiguration alloc] init];
    configuration.userContentController = [[WKUserContentController alloc] init];
    WKUserContentController *userCC = configuration.userContentController;
    // 注入对象,前端调用其方法时,Native 可以捕获到
    [userCC addScriptMessageHandler:self name:@"nativeBridge"];

    WKWebView wkWebView = [[WKWebView alloc] initWithFrame:self.view.frame configuration:configuration];

    // TODO 显示 WebView
}

- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {
    if ([message.name isEqualToString:@"nativeBridge"]) {
        NSLog(@"前端传递的数据 %@: ",message.body);
        // Native 逻辑
    }
}
// 安卓
public class JavaScriptInterfaceDemoActivity extends Activity {
	private WebView Wv;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        Wv = (WebView)findViewById(R.id.webView);     
        final JavaScriptInterface myJavaScriptInterface = new JavaScriptInterface(this);    	 

        Wv.getSettings().setJavaScriptEnabled(true);
        Wv.addJavascriptInterface(myJavaScriptInterface, "nativeBridge");

        // TODO 显示 WebView

    }

    public class JavaScriptInterface {
         Context mContext;

         JavaScriptInterface(Context c) {
             mContext = c;
         }

         public void postMessage(String webMessage){	    	
             // Native 逻辑
         }
     }
}

// 前端调用
// 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 去调用了)

// 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 实现的核心,实现方式可以各种各样,但是万变不离其宗。推荐的实现方式如下:

  • JavaScript 调用 Native 推荐使用 注入 API 的方式(iOS6 忽略,Android 4.2以下使用 WebViewClient 的 onJsPrompt 方式)
  • Native 调用 JavaScript 则直接执行拼接好的 JavaScript 代码即可

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

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

JSbridge接口实现(类似DD fusion)

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

  • 调用 Native(给 Native 发消息)
  • 接被 Native 调用(接收 Native 消息)

因此,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 逻辑也很简单,主要的代码逻辑是:

  • 接收到 JavaScript 消息 => 解析参数,拿到 bridgeName、data 和 callbackId => 根据 bridgeName 找到功能方法,以 data 为参数执行 => 执行返回值和 callbackId 一起回传前端。
  • Native 调用 JavaScript 也同样简单,直接自动生成一个唯一的 ResponseId,并存储句柄,然后和 data 一起发送给前端即可。

JSBridge 如何引用

  1. 由 Native 端进行注入 注入方式和 Native 调用 JavaScript 类似,直接执行桥的全部代码。
  • 优点:桥的版本很容易与 Native 保持一致,Native 端不用对不同版本的 JSBridge 进行兼容;
  • 缺点:注入时机不确定,需要实现注入失败后重试的机制,保证注入的成功率,同时 JavaScript 端在调用接口时,需要优先判断 JSBridge 是否已经注入成功。
  1. 由 JavaScript 端引用 直接与 JavaScript 一起执行。

与由 Native 端注入正好相反,

  • 优点:JavaScript 端可以确定 JSBridge 的存在,直接调用即可
  • 缺点:如果桥的实现方式有更改,JSBridge 需要兼容多版本的 Native Bridge 或者 Native Bridge 兼容多版本的 JSBridge。

CodingMeUp avatar May 24 '20 14:05 CodingMeUp