JSBridge原理解析——以WebviewJavascriptBridge实现方式为例

标签: | 发表时间:2021-06-21 22:23 | 作者:
出处:https://mp.weixin.qq.com

一、什么是 JSBridge?

JSBridge 是一种 webview 侧和 native 侧进行通信的手段,webview 可以通过 jsb 调用 native 的能力,native 也可以通过 jsb 在 webview 上执行一些逻辑。

二、JSB 的实现方式

在比较流行的 JSBridge 中,主要是通过拦截 URL 请求来达到 native 端和 webview 端相互通信的效果的。

这里我们以比较火的 WebviewJavascriptBridge 为例,来解析一下它的实现方式。

源码地址:https://github.com/marcuswestin/WebViewJavascriptBridge

2-1、在 native 端和 webview 端注册 Bridge

注册的时候,需要在 webview 侧和 native 侧分别注册 bridge,其实就是用一个对象把所有函数储存起来。

    function registerHandler(handlerName, handler) {      
    messageHandlers[handlerName] = handler;
}
    - (void)registerHandler:(NSString *)handlerName handler:(WVJBHandler)handler {      
    _base.messageHandlers[handlerName] = [handler copy];
}

2-2、在 webview 里面注入初始化代码

    function setupWebViewJavascriptBridge(callback) {      
       if (window.WebViewJavascriptBridge) { return callback(WebViewJavascriptBridge); }
       if (window.WVJBCallbacks) { return window.WVJBCallbacks.push(callback); }
       window.WVJBCallbacks = [callback];
       var WVJBIframe = document.createElement('iframe');
       WVJBIframe.style.display = 'none';
       WVJBIframe.src = 'https://__bridge_loaded__';
       document.documentElement.appendChild(WVJBIframe);
       setTimeout(function() { document.documentElement.removeChild(WVJBIframe) }, 0)
}

这段代码主要做了以下几件事:

(1)创建一个名为 WVJBCallbacks 的数组,将传入的 callback 参数放到数组内

(2)创建一个 iframe,设置不可见,设置 src 为 https://__bridge_loaded__

(3)设置定时器移除这个 iframe

2-3、在 native 端监听 URL 请求

iOS 中有两种 webview,一种是 UIWebview,另一种是 WKWebview,这里以 WKWebview 为例:

    - (void)webView:(WKWebView *)webView decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler {      
    if (webView != _webView) { return; }

    __strong typeof(_webViewDelegate) strongDelegate = _webViewDelegate;
    if (strongDelegate && [strongDelegate respondsToSelector:@selector(webView:decidePolicyForNavigationResponse:decisionHandler:)]) {
        [strongDelegate webView:webView decidePolicyForNavigationResponse:navigationResponse decisionHandler:decisionHandler];
    }
    else {
        decisionHandler(WKNavigationResponsePolicyAllow);
    }
}

这段代码主要做了以下几件事:

(1)拦截了所有的 URL 请求并拿到 url

(2)首先判断 isWebViewJavascriptBridgeURL,判断这个 url 是不是 webview 的 iframe 触发的,具体可以通过 host 去判断。

(3)继续判断,如果是 isBridgeLoadedURL,那么会执行 injectJavascriptFile方法,会向 webview 中再次注入一些逻辑,其中最重要的逻辑就是,在 window 对象上挂载一些全局变量和 WebViewJavascriptBridge属性,具体值如下:

    window.WebViewJavascriptBridge = {      
        registerHandler: registerHandler,
        callHandler: callHandler,
        disableJavscriptAlertBoxSafetyTimeout: disableJavscriptAlertBoxSafetyTimeout,
        _fetchQueue: _fetchQueue,
        _handleMessageFromObjC: _handleMessageFromObjC
};

var sendMessageQueue = [];
var messageHandlers = {};

var responseCallbacks = {};
var uniqueId = 1;

(4)继续判断,如果是 isQueueMessageURL,那么这就是个处理消息的回调,需要执行一些消息处理的方法(第四步会详细讲)

2-4、webview 调用 native 能力

当 native 和 webview 都注册好了 Bridge 之后,双方就可以互相调用了,这里先介绍 webview 调用 native 能力的过程。

2-4-1、webview 侧 callHandler

当 webview 调用 native 时,会调用 callHandler 方法,这个方法具体逻辑如下:

    bridge.callHandler('ObjC Echo', {'key':'value'}, function responseCallback(responseData) {      
       console.log("JS received response:", responseData)
})

function callHandler(handlerName, data, responseCallback) {
        if (arguments.length == 2 && typeof data == 'function') {
                responseCallback = data;
                data = null;
         }
        _doSend({ handlerName:handlerName, data:data }, responseCallback);
}

function _doSend(message, responseCallback) {
        if (responseCallback) {
               var callbackId = 'cb_'+(uniqueId++)+'_'+new Date().getTime();
               responseCallbacks[callbackId] = responseCallback;
               message['callbackId'] = callbackId;
         }
         sendMessageQueue.push(message);
         messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
}

实际上就是先生成一个 message,然后 push 到 sendMessageQueue 里,然后更改 iframe 的 src。

2-4-2、native 侧 flushMessageQueue

然后,当 native 端检测到 iframe src 的变化时,会走到 isQueueMessageURL 的判断逻辑,然后执行 WKFlushMessageQueue 函数,获取到 JS 侧的 sendMessageQueue 中的所有 message。

    - (void)WKFlushMessageQueue {      
    [_webView evaluateJavaScript:[_base webViewJavascriptFetchQueyCommand] completionHandler:^(NSString* result, NSError* error) {
        if (error != nil) {
            NSLog(@"WebViewJavascriptBridge: WARNING: Error when trying to fetch data from WKWebView: %@", error);
        }
        [_base flushMessageQueue:result];
    }];
}

- (void)flushMessageQueue:(NSString *)messageQueueString{
    if (messageQueueString == nil || messageQueueString.length == 0) {
        NSLog(@"WebViewJavascriptBridge: WARNING: ObjC got nil while fetching the message queue JSON from webview. This can happen if the WebViewJavascriptBridge JS is not currently present in the webview, e.g if the webview just loaded a new page.");
        return;
    }

    id messages = [self _deserializeMessageJSON:messageQueueString];
    for (WVJBMessage* message in messages) {
        if (![message isKindOfClass:[WVJBMessage class]]) {
            NSLog(@"WebViewJavascriptBridge: WARNING: Invalid %@ received: %@", [message class], message);
            continue;
        }
        [self _log:@"RCVD" json:message];

        NSString* responseId = message[@"responseId"];
        if (responseId) {
            WVJBResponseCallback responseCallback = _responseCallbacks[responseId];
            responseCallback(message[@"responseData"]);
            [self.responseCallbacks removeObjectForKey:responseId];
        } else {
            WVJBResponseCallback responseCallback = NULL;
            NSString* callbackId = message[@"callbackId"];
            if (callbackId) {
                responseCallback = ^(id responseData) {
                    if (responseData == nil) {
                        responseData = [NSNull null];
                    }

                    WVJBMessage* msg = @{ @"responseId":callbackId, @"responseData":responseData };
                    [self _queueMessage:msg];
                };
            } else {
                responseCallback = ^(id ignoreResponseData) {
                    // Do nothing
                };
            }

            WVJBHandler handler = self.messageHandlers[message[@"handlerName"]];

            if (!handler) {
                NSLog(@"WVJBNoHandlerException, No handler for message from JS: %@", message);
                continue;
            }

            handler(message[@"data"], responseCallback);
        }
    }
}

当一个 message 结构存在 responseId 的时候说明这个 message 是执行 bridge 后传回的。取不到 responseId 说明是第一次调用 bridge 传过来的,这个时候会生成一个返回给调用方的 message,其 reponseId 是传过来的 message 的 callbackId,当 native 执行 responseCallback 时,会触发_dispatchMessage 方法执行 webview 环境的的 js 逻辑,将生成的包含 responseId 的 message 返回给 webview。

2-4-3、webview 侧 handleMessageFromObjC

    function _handleMessageFromObjC(messageJSON) {      
    _dispatchMessageFromObjC(messageJSON);
}

function _dispatchMessageFromObjC(messageJSON) {
       if (dispatchMessagesWithTimeoutSafety) {
             setTimeout(_doDispatchMessageFromObjC);
 } else {
             _doDispatchMessageFromObjC();
 }

 function _doDispatchMessageFromObjC() {
        var message = JSON.parse(messageJSON);
        var messageHandler;
        var responseCallback;
        if (message.responseId) {
                responseCallback = responseCallbacks[message.responseId];
                if (!responseCallback) {
                          return;
                }
                responseCallback(message.responseData);
                delete responseCallbacks[message.responseId];
         } else {
               if (message.callbackId) {
                       var callbackResponseId = message.callbackId;
                       responseCallback = function(responseData) {
                             _doSend({ handlerName:message.handlerName, responseId:callbackResponseId, responseData:responseData });
                        };
                }

                var handler = messageHandlers[message.handlerName];
                if (!handler) {
                        console.log("WebViewJavascriptBridge: WARNING: no handler for message from ObjC:", message);
                } else {
                        handler(message.data, responseCallback);
                }
          }
     }
}

如果从 native 获取到的 message 中有 responseId,说明这个 message 是 JS 调 Native 之后回调接收的 message,所以从一开始 sendData 中添加的 responseCallbacks 中根据 responseId(一开始存的时候是用的 callbackId,两个值是相同的)取出这个回调函数并执行,这样就完成了一次 JS 调用 Native 的流程。

2-4-4、过程总结

过程如下图

1、native 端注册 jsb

2、webview 侧创建 iframe,设置 src 为 __bridge_load__

3、native 端捕获请求,注入 jsb 初始化代码,在 window 上挂载相关对象和方法

4、webview 侧调用 callHandler方法,并在 responseCallback上添加 callbackId: responseCallback,并修改 iframe 的 src,触发捕获

5、native 收到 message,生成一个 responseCallback,并执行 native 侧注册好的方法

6、native 执行完毕后,通过 webview 执行 _handleMessageFromObjC方法,取出 callback 函数,并执行

2-5、native 调用 webview 能力

native 调用 webview 注册的 jsb 的逻辑是相似的,不过就不是通过触发 iframe 的 src 触发执行的了,因为 Native 可以自己主动调用 JS 侧的方法。其具体过程如下图:

1、native 侧调用 callHandler方法,并在 responseCallback上添加 callbackId: responseCallback

2、native 侧主动调用 _handleMessageFromObjC方法,在 webview 中执行对应的逻辑

3、webview 侧执行结束后,生成带有 responseId的 message,添加到 sendMessageQueue中,并修改 iframe 的 src 为 __wvjb_queue_message__

4、native 端拦截到 url 变化,调用 webview 的逻辑获取到 message,拿到 responseId,并执行对应的 callback 函数

相关 [jsbridge 原理 解析] 推荐:

JSBridge原理解析——以WebviewJavascriptBridge实现方式为例

- -
JSBridge 是一种 webview 侧和 native 侧进行通信的手段,webview 可以通过 jsb 调用 native 的能力,native 也可以通过 jsb 在 webview 上执行一些逻辑. 在比较流行的 JSBridge 中,主要是通过拦截 URL 请求来达到 native 端和 webview 端相互通信的效果的.

H5与Native交互之JSBridge技术

- - SegmentFault 最新的文章
做过混合开发的很多人都知道Ionic和PhoneGap之类的框架,这些框架在web基础上包了一层Native,然后通过Bridge技术使得js可以调用视频、位置、音频等功能. 本文就是介绍这层Bridge的交互原理,通过阅读本文你可以了解到js与ios及android底层的通讯原理及JSBridge的封装技术及调试方法.

jsBridge 以及 Web 和 APP 交互通信方式

- - 掘金 前端
jsBridge 到底是什么. jsBridge 是一种技术,主要用于解决 Web 前端和原生应用间的通信问题. 这一技术在混合开发(Hybrid App Development)和一些原生应用内嵌 H5 页面的场景中被广泛应用. 通过 jsBridge,开发者可以直接在 JavaScript 中调用原生代码,如获取设备信息、调用系统功能等,极大的提高了开发效率.

Dubbo原理解析-监控

- - zzm
1.  监控中心启动,我们先看下dubbo的属性文件. 相比于provider, consumer的启动注册中心多了registry, jetty容器启动. 它们都是基于dubbo的spi扩展机制的. SpringContainer容器启动就是加载classpath*:META-INF/spring/ *.xml spring的配置文件.

SL4A 之实现原理解析

- vento - python.cn(jobs, news)
关于SL4A的简介和在Android系统的安装及使用,请参考我的上一篇博文《Android 脚本设计之 SL4A》,本篇来分析其内部的实现机制. SL4A架构实现了本地脚本和原生态Android程序的内部消息通信,所以任何本地脚本语言,只要实现了这套兼容的JSON RPC通信接口,就可以呼叫SL4A的RPC Server端程序.

Quartz集群实战及原理解析

- - CSDN博客推荐文章
  选Quartz的团队基本上是冲着Quartz本身实现的集群去的, 不然JDK自带Timer就可以实现相同的功能, 而Timer存在的单点故障是生产环境上所不能容忍的. 在自己造个有负载均衡和支持集群(高可用、伸缩性)的调度框架又影响项目的进度, 所以大多数团队都直接使用了Quartz来作为调度框架.

DNS原理及其解析过程

- - 操作系统 - ITeye博客
 网络通讯大部分是基于TCP/IP的,而TCP/IP是基于IP地址的,所以计算机在网络上进行通讯时只能识别如“202.96.134.133”之类的IP地址,而不能认识域名. 我们无法记住10个以上IP地址的网站,所以我们访问网站时,更多的是在浏览器地址栏中输入域名,就能看到所需要的页面,这是因为有一个叫“DNS服务器”的计算机自动把我们的域名“翻译”成了相应的IP地址,然后调出IP地址所对应的网页.

记一次MongoDB性能问题,附原理解析

- zffl - NoSQLFan
下面文章转载自火丁笔记,原作者描述了一次MongoDB数据迁移过程中遇到的性能问题及其解决方案,中间追查问题的方法和工具值得我们学习. 另外NoSQLFan还对作者略讲的问题产生原理进行了分析,希望对您有用. 最近忙着把一个项目从MySQL迁移到MongoDB,在导入旧数据的过程中,遇到了些许波折,犯了不少错误,但同时也学到了不少知识,遂记录下来.

高性能JavaScript模板引擎原理解析

- - 腾讯CDC
  随着 web 发展,前端应用变得越来越复杂,基于后端的 javascript(Node.js) 也开始崭露头角,此时 javascript 被寄予了更大的期望,与此同时 javascript MVC 思想也开始流行起来. javascript 模板引擎作为数据与界面分离工作中最重要一环,越来越受开发者关注,近一年来在开源社区中更是百花齐放,在 Twitter、淘宝网、新浪浪微博、腾讯QQ空间、腾讯微博等大型网站中均能看到它们的身影.

数据库水平切分的实现原理解析

- - 数据库 - ITeye博客
本文系转载,原文地址: http://lishuaibt.iteye.com/blog/409294. 随着互联网应用的广泛普及,海量数据的存储和访问成为了系统设计的瓶颈问题. 对于一个大型的 互联网应用,每天几十亿的PV无疑对数据库造成了相当高的负载. 对于系统的稳定性和扩展性造成了极大的问题.