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这个功能。

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

推荐阅读更多精彩内容