面试题Android使用场景

WebViewJavascriptBridge 源码解析

2019-09-25  本文已影响0人  Hi川

框架简介

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];
}
上一篇下一篇

猜你喜欢

热点阅读