美文网首页面试题Android使用场景
WebViewJavascriptBridge 源码解析

WebViewJavascriptBridge 源码解析

作者: Hi川 | 来源:发表于2019-09-25 06:58 被阅读0次

    框架简介

    marcuswestin/WebViewJavascriptBridge 是用于在 WKWebViews, UIWebViews & WebViews 中,JS 与 Obj-C 相互发送消息的桥接库。

    通常实现 iOS 与 JS 桥接的方式有两种:

    1. 通过 Webview 拦截请求的方式。
    2. 通过 iOS7 之后的 JavaScriptCore 框架。

    marcuswestin/WebViewJavascriptBridge 使用的正是第一种方式。

    该桥接库在 Github 上拥有 1.2 w+ 的 star 数,可见该库受欢迎的程度。

    功能解析

    我们从该框架的几个重要功能点入手,来逐步了解其对应的实现细节。

    主要功能分为 6 点:

    1. Obj-C Bridge 对象的初始化。
    2. JS Bridge 对象的初始化。
    3. Obj-C 注册函数。
    4. JS 调用 Obj-C 注册的函数,并支持回调 JS。
    5. JS 注册函数。
    6. Obj-C 调用 JS 的函数,并支持回调 Obj-C。

    相关功能对应的使用方法如下:

    // Obj-C bridge 对象的初始化。
    _bridge = [WebViewJavascriptBridge bridgeForWebView:webView];
    
    // JS bridge 对象的初始化。
    // JS 中引入的代码
    function setupWebViewJavascriptBridge(callback) {
        if (window.WebViewJavascriptBridge) { return callback(WebViewJavascriptBridge); }
        if (window.WVJBCallbacks) { return window.WVJBCallbacks.push(callback); }
        window.WVJBCallbacks = [callback];
        var WVJBIframe = dObj-Cument.createElement('iframe');
        WVJBIframe.style.display = 'none';
        WVJBIframe.src = 'https://__bridge_loaded__';
        dObj-Cument.dObj-CumentElement.appendChild(WVJBIframe);
        setTimeout(function() { dObj-Cument.dObj-CumentElement.removeChild(WVJBIframe) }, 0)
    }
    // Obj-C 桥接库中注入的 JS 代码
    setTimeout(_callWVJBCallbacks, 0);
    function _callWVJBCallbacks() {
        var callbacks = window.WVJBCallbacks;
        delete window.WVJBCallbacks;
        for (var i=0; i<callbacks.length; i++) {
           callbacks[i](WebViewJavascriptBridge);
        }
    }
    
    // Obj-C 注册函数供 JS 调用
    [_bridge registerHandler:@"testObjcCallback" handler:^(id data, WVJBResponseCallback responseCallback) {
        responseCallback(@"Response from testObjcCallback");
    }];
    
    // JS 调用 Obj-C 注册的方法,传参,并支持回调 JS
    bridge.callHandler('testObjcCallback', {'foo': 'bar'}, function(response) {
        log('JS got response', response)
    })
    
    // JS 注册函数供 Obj-C 调用
    bridge.registerHandler('testJavascriptHandler', function(data, responseCallback) {
        var responseData = { 'Javascript Says':'Right back atcha!' }
        responseCallback(responseData)
    })
    
    // Obj-C 调用 JS 注册的方法,传参,支持回调 Obj-C
    [_bridge callHandler:@"testJavascriptHandler" data:@{ @"foo":@"before ready" } responseCallback:^(id responseData) {
        NSLog(@"responseCallback called: %@", responseData);
    }];
    

    源码解析

    源码解析从作者 ExampleApp-iOS 这个 demo 入手,我们看下 ExampleUIWebViewController.m 中的 demo 代码。

    Obj-C 初始化 Bridge

    // Obj-C 初始化 bridge 对象
    _bridge = [WebViewJavascriptBridge bridgeForWebView:webView];
    // ViewController 实现 WebView 的代理方法
    [_bridge setWebViewDelegate:self];
    

    bridgeForWebView: 方法中创建 WebViewJavascriptBridge 对象 bridge,并设置 webView 的代理为 bridge,这里主要是需要代理方法 shouldStartLoadWithRequest: 与 JS 通信。创建 WebViewJavascriptBridgeBase 对象 _base,并设置 _base 代理为 bridge,这里是需要 WebView 对象提供 evaluateJavaScript: 方法与 JS 通信。

    WebViewJavascriptBridgeBase 的初始化方法中,会创建 messageHandlersstartupMessageQueueresponseCallbacks 对象,messageHandlers 用于存储 Obj-C 注册的函数,startupMessageQueue 存储 webview 载入页面前就调用的 JS 函数,responseCallbacks 存储 Obj-C 调用 JS 函数后要回调 Obj-C 的回调方法。

    这几个 Array 和 Dictionary 如何作用,后面会有介绍。

    JS 初始化 bridge

    因为 JS 与 Obj-C 之间会相互调用,不容易理清两者的调用关系,所以我画了一个泳道图辅助理解。

    WebViewJavascriptBridge 源码解析 - JS Bridge 初始化.png

    JS 初始化 bridge 的过程要比 Obj-C 复杂。

    首先,WebView 要载入 html 页面,里面有初始化相关的 JS 代码。

    - (void)loadExamplePage:(UIWebView*)webView {
        NSString* htmlPath = [[NSBundle mainBundle] pathForResource:@"ExampleApp" ofType:@"html"];
        NSString* appHtml = [NSString stringWithContentsOfFile:htmlPath encoding:NSUTF8StringEncoding error:nil];
        NSURL *baseURL = [NSURL fileURLWithPath:htmlPath];
        [webView loadHTMLString:appHtml baseURL:baseURL];
    }
    

    载入 html 页面会加载 JS 代码,JS 相关代码如下:

        function setupWebViewJavascriptBridge(callback) {
            // 如果有 bridge 对象则直接调用 callback 并传入 bridge对象。
            if (window.WebViewJavascriptBridge) { return callback(WebViewJavascriptBridge); }
            // 如果有 WVJBCallbacks 则将回调函数 push 到数组里,后面初始化 bridge 时会统一遍历调用 callback,并传入 bridge。
            if (window.WVJBCallbacks) { return window.WVJBCallbacks.push(callback); }
            // 首次载入,创建 WVJBCallbacks 并放入 callback,用 iframe 加载 url  https://__bridge_loaded__'
            window.WVJBCallbacks = [callback];
            var WVJBIframe = dObj-Cument.createElement('iframe');
            WVJBIframe.style.display = 'none';
            WVJBIframe.src = 'https://__bridge_loaded__';
            dObj-Cument.dObj-CumentElement.appendChild(WVJBIframe);
            setTimeout(function() { dObj-Cument.dObj-CumentElement.removeChild(WVJBIframe) }, 0)
        }
    
        // 用 bridge 注册函数和调用 Obj-C 函数
        setupWebViewJavascriptBridge(function(bridge) {
            var uniqueId = 1
           // 注册 JS 函数
            bridge.registerHandler('testJavascriptHandler', function(data, responseCallback) {
                var responseData = { 'Javascript Says':'Right back atcha!' }
                responseCallback(responseData)
            })
    
            dObj-Cument.body.appendChild(dObj-Cument.createElement('br'))
    
            var callbackButton = dObj-Cument.getElementById('buttons').appendChild(dObj-Cument.createElement('button'))
            callbackButton.innerHTML = 'Fire testObjcCallback'
            callbackButton.onclick = function(e) {
              // 调动 Obj-C 的函数
                bridge.callHandler('testObjcCallback', {'foo': 'bar'}, function(response) {
                    log('JS got response', response)
                })
            }
        })
    

    iframe 加载 url https://__bridge_loaded__ 后,会调用 webview 的回调方法 - (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType
    然后判断 url 的 host 是 bridge_loaded 后调用如下代码:

    if ([_base isBridgeLoadedURL:url]) {
        [_base injectJavascriptFile];
    }
    
    - (void)injectJavascriptFile {
        // 注入初始化 JS bridge 的 JS 代码。
        NSString *JS = WebViewJavascriptBridge_JS();
        [self _evaluateJavascript:JS];
        // 如果有在 JS bridge 没有初始化好就调用的 JS 函数(这些函数调用保存到 startupMessageQueue 中),则在 JS bridge 初始化好后,统一调用。
        if (self.startupMessageQueue) {
            NSArray* queue = self.startupMessageQueue;
            self.startupMessageQueue = nil;
            for (id queuedMessage in queue) {
                [self _dispatchMessage:queuedMessage];
            }
        }
    }
    

    WebViewJavascriptBridge_JS 中是一段 JS 代码,需要注入到 JS 环境中,这段代码就是初始化 JS bridge 的代码。

    精简后的代码如下:

    NSString * WebViewJavascriptBridge_JS() {
        #define __wvjb_JS_func__(x) #x
        
        // BEGIN preprObj-CessorJSCode
        static NSString * preprObj-CessorJSCode = @__wvjb_JS_func__(
    ;(function() {
        if (window.WebViewJavascriptBridge) {
            return;
        }
        // 初始化 bridge
        window.WebViewJavascriptBridge = {
            registerHandler: registerHandler,
            callHandler: callHandler,
            disableJavscriptAlertBoxSafetyTimeout: disableJavscriptAlertBoxSafetyTimeout,
            _fetchQueue: _fetchQueue,
            _handleMessageFromObjC: _handleMessageFromObjC
        };
    
        var sendMessageQueue = [];
        var messageHandlers = {};
        // 略一部分代码
        var CUSTOM_PROTObj-COL_SCHEME = 'https';
        var QUEUE_HAS_MESSAGE = '__wvjb_queue_message__';
        
        // 调用 https://__wvjb_queue_message__ 通知 Obj-C,统一调用 JS 启动时 JS 调用 Obj-C 的所有方法
        messagingIframe = dObj-Cument.createElement('iframe');
        messagingIframe.style.display = 'none';
        messagingIframe.src = CUSTOM_PROTObj-COL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
        dObj-Cument.dObj-CumentElement.appendChild(messagingIframe);
        
        // 将 JS bridge 传给 callback
        setTimeout(_callWVJBCallbacks, 0);
        function _callWVJBCallbacks() {
            var callbacks = window.WVJBCallbacks;
            delete window.WVJBCallbacks;
            for (var i=0; i<callbacks.length; i++) {
                callbacks[i](WebViewJavascriptBridge);
            }
        }
    })();
        ); // END preprObj-CessorJSCode
    
        #undef __wvjb_JS_func__
        return preprObj-CessorJSCode;
    };
    
    

    调用 _callWVJBCallbacks 函数,将 window.WVJBCallbacks 数组中的所有 callback 调用一遍,并传入初始化好的 bridge 对象。
    此处正好和前面的 setupWebViewJavascriptBridge 的代码呼应上。到此 JS bridge 的初始化结束。

    Obj-C 注册函数

    [_bridge registerHandler:@"testObjcCallback" handler:^(id data, WVJBResponseCallback responseCallback) {
        NSLog(@"testObjcCallback called: %@", data);
        responseCallback(@"Response from testObjcCallback");
    }];
    
    - (void)registerHandler:(NSString *)handlerName handler:(WVJBHandler)handler {
        _base.messageHandlers[handlerName] = [handler copy];
    }
    

    handlerName 作为 Key,handlerBlock作为 Value 存入 _base 的 messageHandlers 字典中。

    JS 调用 Obj-C 函数

    管道图辅助理解 JS 与 Obj-C 两者调用关系。

    WebViewJavascriptBridge 源码解析 - JS 调用 Obj-C 函数.png

    JS 调用 Obj-C 代码如下:

    bridge.callHandler('testObjcCallback', {'foo': 'bar'}, function(response) {
        log('JS got response', response)
    })
    
    // WebViewJavascriptBridge_JS.m 中的 JS 代码
    function callHandler(handlerName, data, responseCallback) {
        if (arguments.length == 2 && typeof data == 'function') {
            responseCallback = data;
            data = null;
        }
        _doSend({ d: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_PROTObj-COL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
    }
    

    将 JS 调用 Obj-C 函数的一些信息封装成 message 放入 sendMessageQueue 队列中,并通知 Obj-C 去获取 JS 的队列消息,并逐一调用 Obj-C 的注册函数。

    message['handlerName'] = handlerName;
    message['data'] = data;
    message['callbackId'] = callbackId;
    

    webview 代理方法 shouldStartLoadWithRequest:,接受到 iframe 的 url 请求,判断是 QUEUE_HAS_MESSAGE 后,

    if ([_base isQueueMessageURL:url]) {
        NSString *messageQueueString = [self _evaluateJavascript:[_base webViewJavascriptFetchQueyCommand]];
        [_base flushMessageQueue:messageQueueString];
    }
    

    获取 JS bridge 的消息队列,调用 flushMessageQueue 方法来消费这些消息。

    代码如下:

    - (void)flushMessageQueue:(NSString *)messageQueueString{
        id messages = [self _deserializeMessageJSON:messageQueueString];
        for (WVJBMessage* message in messages) {
            // 略一些不影响逻辑的代码
            NSString* responseId = message[@"responseId"];
            if (responseId) {
                // 处理 Obj-C 调用 JS 函数时,Obj-C 需要回调的情况,后面还会提到。
                WVJBResponseCallback responseCallback = _responseCallbacks[responseId];
                responseCallback(message[@"responseData"]);
                [self.responseCallbacks removeObjectForKey:responseId];
            } else {
                WVJBResponseCallback responseCallback = NULL;
                NSString* callbackId = message[@"callbackId"];
                if (callbackId) {
                    // 处理 JS 调用 Obj-C 函数时,JS 需要回调的情况。
                    // 构建一个 blObj-Ck,传给 handler,需要时回调 JS。
                    responseCallback = ^(id responseData) {
                        if (responseData == nil) {
                            responseData = [NSNull null];
                        }
                        // 开始回调 JS,构建回调 message,并发送给 JS bridge 处理。
                        WVJBMessage* msg = @{ @"responseId":callbackId, @"responseData":responseData };
                        [self _queueMessage:msg];
                    };
                } else {
                    responseCallback = ^(id ignoreResponseData) {
                        // Do nothing
                    };
                }
                // 获取之前注册的 handler 函数,并调用。
                WVJBHandler handler = self.messageHandlers[message[@"handlerName"]];
                
                if (!handler) {
                    NSLog(@"WVJBNoHandlerException, No handler for message from JS: %@", message);
                    continue;
                }
                
                handler(message[@"data"], responseCallback);
            }
        }
    }
    

    这里在说明一下,如何在 JS 调用 Obj-C 函数时,再回调 JS。

    bridge.callHandler('testObjcCallback', {'foo': 'bar'}, function(response) {
        log('JS got response', response)
    })
    

    function(response) {} 这个函数就是 JS 需要的回调函数,那具体是怎么回调的呢?

    回过头看下 Obj-C bridge 的 - (void)flushMessageQueue:(NSString *)messageQueueString 函数。

    if (callbackId) {
        responseCallback = ^(id responseData) {
            if (responseData == nil) {
                responseData = [NSNull null];
            }
            WVJBMessage* msg = @{ @"responseId":callbackId, @"responseData":responseData };
            [self _queueMessage:msg];
        };
    }
    WVJBHandler handler = self.messageHandlers[message[@"handlerName"]];
    handler(message[@"data"], responseCallback);
    

    判断有 callbackId 时说明,JS 需要回调,然后构建一个回调的 message,responseId 就是传来的 callbackId,responseData 是要回传的数据,之所以用 responseId 这个 key 是因为做消息的类型区分。[self _queueMessage:msg] 就是将消息发回给 JS bridge。JS bridge 通过 responseId 找到之前保存的函数,调用即可。

    function _doDispatchMessageFromObjC() {
        var message = JSON.parse(messageJSON);
        var messageHandler;
        var responseCallback;
       // 处理 JS 的回调函数,找到 responseId 对应的函数并调用。
        if (message.responseId) {
            responseCallback = responseCallbacks[message.responseId];
            if (!responseCallback) {
                return;
            }
            responseCallback(message.responseData);
            delete responseCallbacks[message.responseId];
        } else {
            // 略...
        }
    }
    

    JS 注册函数

    bridge.registerHandler('testJavascriptHandler', function(data, responseCallback) {
        var responseData = { 'Javascript Says':'Right back atcha!' }
        responseCallback(responseData)
    })
    
    // WebViewJavascriptBridge_JS.m 中的 JS 代码
    function registerHandler(handlerName, handler) {
        messageHandlers[handlerName] = handler;
    }
    

    Obj-C 调用 JS 函数

    管道图辅助理解两者调用关系。

    WebViewJavascriptBridge源码解析 - Obj-C 调用 JS 函数.png
    [_bridge callHandler:@"testJavascriptHandler" data:@{ @"foo":@"before ready" }];
    

    最终调用的是 WebViewJavascriptBridgeBase 中的 - (void)sendData:(id)data responseCallback:(WVJBResponseCallback)responseCallback handlerName:(NSString*)handlerName 方法,根据参数拼装 message 字典。

    message[@"data"] = data;                // 数据参数
    message[@"callbackId"] = callbackId;    // 回调方法
    message[@"handlerName"] = handlerName;  // 函数名
    

    然后调用方法 - (void)_queueMessage:(WVJBMessage*)message 将拼装好的 message 传入,我们看具体的 _queueMessage 方法实现。

    - (void)_queueMessage:(WVJBMessage*)message {
        if (self.startupMessageQueue) {
            [self.startupMessageQueue addObject:message];
        } else {
            [self _dispatchMessage:message];
        }
    }
    

    如果有 self.startupMessageQueue 对象,将消息放入其中,否则发送消息给 JS,之所以要存入 startupMessageQueue 中,是因为在 viewWillAppear 中的 callHandler 不能立马调用到 JS 中,因为 html 没有载入,JS bridge 的环境也没有准备完成。先存入 startupMessageQueue 中,待 JS bridge 准备完成后,在统一调用 startupMessageQueue 中的消息到 JS,并将 startupMessageQueue 置为空。

    接下来我们看如何调用 JS 的函数。

    - (void)_dispatchMessage:(WVJBMessage*)message {
        NSString *messageJSON = [self _serializeMessage:message pretty:NO];
        // 略
        NSString* javascriptCommand = [NSString stringWithFormat:@"WebViewJavascriptBridge._handleMessageFromObjC('%@');", messageJSON];
        if ([[NSThread currentThread] isMainThread]) {
            [self _evaluateJavascript:javascriptCommand];
    
        } else {
            dispatch_sync(dispatch_get_main_queue(), ^{
                [self _evaluateJavascript:javascriptCommand];
            });
        }
    }
    

    将之前拼装好的 message 传给 JS,用 JS bridge 的 _handleMessageFromObjC 函数处理 Obj-C 的调用请求。

    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;
          // 在 JS 调用 Obj-C 函数,且要 JS 回调的情况下的处理方式
            if (message.responseId) {
                responseCallback = responseCallbacks[message.responseId];
                if (!responseCallback) {
                    return;
                }
                responseCallback(message.responseData);
                delete responseCallbacks[message.responseId];
            } else {
               // 这部分是 Obj-C 调用 JS 函数的处理
                if (message.callbackId) {
                   // 构建一个新的 message 用来回调 Obj-C,responseCallback 函数用于 JS 回调 Obj-C。
                    var callbackResponseId = message.callbackId;
                    responseCallback = function(responseData) {
                        _doSend({ handlerName:message.handlerName, responseId:callbackResponseId, responseData:responseData });
                    };
                }
                // 通过 handlerName 获取到对应的函数,并调用。
                var handler = messageHandlers[message.handlerName];
                if (!handler) {
                    console.log("WebViewJavascriptBridge: WARNING: no handler for message from ObjC:", message);
                } else {
                    handler(message.data, responseCallback);
                }
            }
        }
    }
    

    JS 通过传来的 message,获取到 handlerName,从注册 JS 函数时,保存 handler 处理函数的 messageHandlers 对象中取出要调用的 handler 函数,并调用。

    如何处理 Obj-C 的回调函数。

    在Obj-C 调用 JS 的函数时,有时候 Obj-C 需要 JS 调用 Obj-C 的回调函数返回一些数据,如:

    [_bridge callHandler:@"testJavascriptHandler" data:@{ @"foo":@"before ready" } responseCallback:^(id responseData) {
        NSLog(@"responseCallback called: %@", responseData);
    }];
    

    这个 responseCallback 就是 JS 要回调 Obj-C 的回调函数。实现方式是在 JS 处理 Obj-C 消息的 _doDispatchMessageFromObjC 方法中,如果 message 有 callbackId 意味着 Obj-C 需要这个回调,然后构建一个 JS 函数 responseCallback,传给 JS 的 handler 函数,供注册的 JS 函数回调时调用,调用 responseCallback 之后,responseCallback 会创建一个回调的 message 信息 responseId 就是 callbackId,调用 _doSend 函数发送给 Obj-C。
    _doSend 中主要是将消息添加到 sendMessageQueue 队列中,供 Obj-C 获取后,通过 responseId 找到 Obj-C 保存的回调函数并调用,实现请看 - (void)flushMessageQueue:(NSString *)messageQueueString 方法中 if (responseId) { ... } 这一段。

    // - (void)flushMessageQueue:(NSString *)messageQueueString 中
    if (responseId) {
        WVJBResponseCallback responseCallback = _responseCallbacks[responseId];
        responseCallback(message[@"responseData"]);
        [self.responseCallbacks removeObjectForKey:responseId];
    }
    

    相关文章

      网友评论

        本文标题:WebViewJavascriptBridge 源码解析

        本文链接:https://www.haomeiwen.com/subject/scliuctx.html