WebViewJavascriptBridge 实现分析

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中存储的回调,然后执行。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,132评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,802评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,566评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,858评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,867评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,695评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,064评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,705评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,915评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,677评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,796评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,432评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,041评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,992评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,223评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,185评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,535评论 2 343

推荐阅读更多精彩内容