iOS WebViewJavascriptBridge源码分析

前言

JSBridgeYes主要是为了替代WebViewJavascriptBridge,同时也有兼容原来的方法,所以对WebViewJavascriptBridge的源码以及原理做了比较深入的了解,以下主要是针对源码的解读

一、介绍

WebviewJavascriptBridge是一个第三方的支持webview和native进行通信的库,通过JSBridge,webview可以调用native的能力,native也可以webview上执行一些逻辑。

二、基本构成

对于整个框架来说,一共有三部门组成


image.png
  • OC部分:包括oc处理暴露给js接口的类:WKWebViewJavascriptBridge(WebViewJavascriptBridge使用的是UIWebView,基本不用了)
  • js部分:包括js处理暴露给oc接口的文件: ExampleApp.html
  • bridge处理部分:这个部分对于oc和js都各有一个文件,oc是类:WebViewJavascriptBridgeBase,js则是WebViewJavascriptBridge_JS
  • oc和js部分的作用就是声明给对方调用的方法,以及提供供自身使用可以调用对方的一个接口,但是具体如何调用的js、如果调用的oc或者说如何注册给js、如何注册给oc使用的方法,这些逻辑都放在两端的bridge部分进行处理。

三、WVJB实现

image.png

1. 初始化

// iOS 初始化 
self.brige = [WebViewJavascriptBridge bridgeForWebView:_webView];
[self.brige registerHandler:@"imageSelectFunc" handler:^(id data, WVJBResponseCallback responseCallback) {
 
 }];

在native端和webview端注册Bridge,本质就是用一个对象把所有函数储存起来

// js
function registerHandler(handlerName, handler) {
    messageHandlers[handlerName] = handler;
}

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

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


// Html
function setupWebViewJavascriptBridge(callback) {
        if (window.WebViewJavascriptBridge) { return callback(WebViewJavascriptBridge); }
        if (window.WVJBCallbacks) { return window.WVJBCallbacks.push(callback); }
        window.WVJBCallbacks = [callback]; // 创建一个 WVJBCallbacks 全局属性数组,并将 callback 插入到数组中。
        var WVJBIframe = document.createElement('iframe'); // 创建一个 iframe 元素
        WVJBIframe.style.display = 'none'; // 不显示
        WVJBIframe.src = 'wvjbscheme://__BRIDGE_LOADED__'; // 设置 iframe 的 src 属性
        document.documentElement.appendChild(WVJBIframe); // 把 iframe 添加到当前文导航上。
        setTimeout(function() {document.documentElement.removeChild(WVJBIframe) }, 0)
    }
    
 // 这里主要是注册 OC 将要调用的 JS 方法
 setupWebViewJavascriptBridge(function(bridge){
       
 });

这段代码主要做了以下几件事:
(1)创建一个名为WVJBCallbacks的数组,将传入的callback参数放到数组内
(2)创建一个iframe,设置不可见,设置src为 https://bridge_loaded
(3)设置定时器移除这个iframe

3.在客户端监听URL请求

// 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属性
(4)继续判断,如果是isQueueMessageURL,那么这就是个处理消息的回调,需要执行一些消息处理的方法

4. webview调用native

当webview调用native时,会调用callHandler方法

// h5代码有封装 截取一部分 
function invoke (fun, params, cb) {
   setupWebViewJavascriptBridge(function (bridge) {
     bridge.callHandler(fun, params, function (res) {
       cb && cb(res)
       console.log('invoke', res)
       let logData = {
         JsBridge: 'invoke',
         BridgeName: fun,
         BridgeParam: params,
         Response: res
       }
       logger.info(logData)
     })
   })
 }

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

5. 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。

image.png

6. native侧发送消息 sendData

- (void)sendData:(id)data responseCallback:(WVJBResponseCallback)responseCallback handlerName:(NSString*)handlerName {
    NSMutableDictionary* message = [NSMutableDictionary dictionary];
    
    if (data) {
        message[@"data"] = data;
    }
    
    if (responseCallback) {
        NSString* callbackId = [NSString stringWithFormat:@"objc_cb_%ld", ++_uniqueId];
        self.responseCallbacks[callbackId] = [responseCallback copy];
        message[@"callbackId"] = callbackId;
    }
    
    if (handlerName) {
        message[@"handlerName"] = handlerName;
}

OC要调用javascript环境的方法,其实就是调用ExampleApp.html中的bridge.registerHandler注册的方法。把所有信息存入一个名字为message的字典中。里面拼装好参数data、回调IDcallbackId、消息名字handlerName,把OC消息序列化、并且转化为javascript环境的格式。然后在主线程中调用_evaluateJavascript。

WebViewJavascriptBridge._handleMessageFromObjC('{\"callbackId\":\"objc_cb_1\",\"data\":{\"OC调用JS方法\":\"OC调用JS方法的参数\"},\"handlerName\":\"OC调用JS提供的方法\"}');

image.png

7.总结

结合上面的逻辑图,原理其实很简单

  • 分别在OC环境和javascript环境都保存一个bridge对象,里面维持着requestId,callbackId,以及每个id对应的具体实现。

  • OC通过javascript环境的window.WebViewJavascriptBridge对象来找到具体的方法,然后执行。

  • javascript通过改变iframe的src来触发webview的代理方法webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler从而实现把javascript消息发送给OC这个功能。

你可能感兴趣的:(iOS WebViewJavascriptBridge源码分析)