iOS-UI基础技术

WebViewJavascriptBridge 实现分析

2022-12-17  本文已影响0人  jacinzhang

UIWebView 已被全面废弃,故本文只分析 WKWebView 实现。源码见 WebViewJavascriptBridge

先来看下在 WKWebView 下,是怎么使用JSBridge 的。

    WKWebView* webView = [[NSClassFromString(@"WKWebView") alloc] initWithFrame:self.view.bounds];
    webView.navigationDelegate = self;
    [self.view addSubview:webView];
    [WebViewJavascriptBridge enableLogging];
    _bridge = [WebViewJavascriptBridge bridgeForWebView:webView];
    [_bridge setWebViewDelegate:self];
    
    [_bridge registerHandler:@"testObjcCallback" handler:^(id data, WVJBResponseCallback responseCallback) {
        NSLog(@"testObjcCallback called: %@", data);
        responseCallback(@"Response from testObjcCallback");
    }];
    
    [_bridge callHandler:@"testJavascriptHandler" data:@{ @"foo":@"before ready" }];

所以,看来 JSBridge 对象是 WebViewJavascriptBridge 实现的。

WebViewJavascriptBridge.h宏定义如下

#if defined __MAC_OS_X_VERSION_MAX_ALLOWED
    #define WVJB_PLATFORM_OSX
    #define WVJB_WEBVIEW_TYPE WebView
    #define WVJB_WEBVIEW_DELEGATE_TYPE NSObject<WebViewJavascriptBridgeBaseDelegate>
    #define WVJB_WEBVIEW_DELEGATE_INTERFACE NSObject<WebViewJavascriptBridgeBaseDelegate, WebPolicyDelegate>
#elif defined __IPHONE_OS_VERSION_MAX_ALLOWED
    #import <UIKit/UIWebView.h>
    #define WVJB_PLATFORM_IOS
    #define WVJB_WEBVIEW_TYPE UIWebView
    #define WVJB_WEBVIEW_DELEGATE_TYPE NSObject<UIWebViewDelegate>
    #define WVJB_WEBVIEW_DELEGATE_INTERFACE NSObject<UIWebViewDelegate, WebViewJavascriptBridgeBaseDelegate>
#endif

所以,在 iOS 上,其本质是

NSObject<UIWebViewDelegate, WebViewJavascriptBridgeBaseDelegate>

WKWebView 也是这个类,也遵守 UIWebViewDelegate? 是否有点奇怪?

看其内部实现,就会发现,当系统支持 WebKit 时,并且用户传的是 WKWebView 时,实例化 WebViewJavascriptBridge 对象时,就直接以 WKWebView 进行了初始化。

+ (instancetype)bridge:(id)webView {
#if defined supportsWKWebView
    if ([webView isKindOfClass:[WKWebView class]]) {
        return (WebViewJavascriptBridge*) [WKWebViewJavascriptBridge bridgeForWebView:webView];
    }
#endif
    if ([webView isKindOfClass:[WVJB_WEBVIEW_TYPE class]]) {
        WebViewJavascriptBridge* bridge = [[self alloc] init];
        [bridge _platformSpecificSetup:webView];
        return bridge;
    }
    [NSException raise:@"BadWebViewType" format:@"Unknown web view type."];
    return nil;
}

所以,接下来,我们就去看 WKWebViewJavascriptBridge
实例化对象

+ (instancetype)bridgeForWebView:(WKWebView*)webView {
    WKWebViewJavascriptBridge* bridge = [[self alloc] init];
    [bridge _setupInstance:webView];
    [bridge reset];
    return bridge;
}
- (void) _setupInstance:(WKWebView*)webView {
    _webView = webView;
    _webView.navigationDelegate = self;
    _base = [[WebViewJavascriptBridgeBase alloc] init];
    _base.delegate = self;
}

上述代码又实例化了一个很重要成员变量 WebViewJavascriptBridgeBase

整个 JSBridege objc 代码的实现就集中在了 WKWebViewJavascriptBridgeWebViewJavascriptBridgeBase,此外还有一个 JS 代码的注入文件 WebViewJavascriptBridge_js,整个库的核心可以说就是这几个文件了。

WebViewJavascriptBridge 加载

jsbridge-load
可以看到,WebViewJavascriptBridge 加载过程主要如下:
  1. h5 网页加载,内部添加了一个不展示的 iframe,其 src 属性设置为了 https://__bridge_loaded__
var WVJBIframe = document.createElement('iframe');
WVJBIframe.style.display = 'none';
WVJBIframe.src = 'https://__bridge_loaded__';
document.documentElement.appendChild(WVJBIframe);
  1. iframe src 加载时,其请求被 WKWebView WKNavigationDelegate 代理方法拦截
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler 
  1. 内部判断为加载 WebViewJavascriptBridge 请求,执行注入 js 代码 WebViewJavascriptBridge_js,实现 JSBridge,挂载在 window 上。
    window.WebViewJavascriptBridge = {
        registerHandler: registerHandler,
        callHandler: callHandler,
        disableJavscriptAlertBoxSafetyTimeout: disableJavscriptAlertBoxSafetyTimeout,
        _fetchQueue: _fetchQueue,
        _handleMessageFromObjC: _handleMessageFromObjC
    };

注入 js 同时,又添加了一个 iframe 元素,其 src 设置为了 https://__wvjb_queue_message__,注入的时候就会加载,然后被 WKNavigationDelegate 拦截。

如果 load web 之前,就调用了 callHandler,则会先存储起来,先执行注入 WebViewJavascriptBridge_js,然后再去 sendData。

- (void)injectJavascriptFile {
    NSString *js = WebViewJavascriptBridge_js();
    [self _evaluateJavascript:js];
    if (self.startupMessageQueue) {
        NSArray* queue = self.startupMessageQueue;
        self.startupMessageQueue = nil;
        for (id queuedMessage in queue) {
            [self _dispatchMessage:queuedMessage];
        }
    }
}

native 调用 h5

jsbridge-native-invoke-h5.png

以 native 调用H5 testJavascriptHandler 方法为例, native 调用 h5 主要经历如下几个步骤:

  1. h5 首先注册 testJavascriptHandler
  2. native 发送的数据,被包装成 json 字符串作为参数,然后使用 WebViewJavascriptBridge._handleMessageFromObjC('%@'); 在 WKWebView 执行 js 代码。json 字符串包含:data callbackId handlerName 参数。同时将回调根据callbackId存储起来。(callbackId 使用 objc_cb_ + 自增 id)
  3. js 方法 _dispatchMessageFromObjC 来处理数据。如果有 callbackId,则会使用 _doSend 方法,将callbackId 转为 responseId ,加入 testJavascriptHandler 返回的 responseData , 然后将数据存储到 sendMessageQueue 对象中,然后加载 messagingIframe src,src 链接为 https://__wvjb_queue_message__
  4. WKNavigationDelegate 代理拦截到 messagingIframe 重新加载 信息,执行 js 代码 WebViewJavascriptBridge._fetchQueue(); 获取存在 sendMessageQueue 中数据。
  5. sendMessageQueue 中获取到 responseId,根据 responseId 取到存储在2 中存储在 responseCallbacks 的 callback handler,然后 native 调用 handler,传入 js 的 responseData

h5 调用 native

jsbridge-h5-invoke-native.png
以 h5 调用 native testObjcCallback handler 为例:
  1. native 需要先注册 testObjcCallback handler
  2. h5 调用 handler testObjcCallback
bridge.callHandler('testObjcCallback', { 'foo': 'bar' }, function (response) {
  log('JS got response', response)
})
  1. js _doSend() 方法,处理调用,将 handlerName data callbackId 包装成JSON 字典对象, 存到 sendMessageQueue 中,并存储回调(如有调用有回调),然后设置 messagingIframe,让其重新加载https://__wvjb_queue_message__ 。其中 callbackId 生成规则 'cb_'+(uniqueId++)+'_'+new Date().getTime()

  2. WKNavigationDelegate 代理拦截 https://__wvjb_queue_message__,处理调用。首先通过 WebViewJavascriptBridge._fetchQueue(); 获取 js 数据,即 sendMessageQueue 中数据。

  3. 根据 sendMessageQueue 中数据,是否有 callbackId。如果有 callbackId, 即在 native handler 中,传入 responseData,并将 callbackId 转为 responseId, 然后再次 WKWebView 执行 js 代码。

  4. h5 WebViewJavascriptBridge 中 通过 _handleMessageFromObjC 方法处理调用。拿到 responseDataresponseId, 根据 responseId,找到3中存储的回调,然后执行。

上一篇下一篇

猜你喜欢

热点阅读