深入浅出WebViewJavascriptBridge

个人Github博客,求关注

1 前言

WebViewJavascriptBridge是iOS/OSX平台上支撑Obj-C和UIWebViews/WebViews JavaScript互发消息的库。目前主流App几乎都是某种程度的Hybrid App,该库因而得到广泛应用。

2 基础知识

在学习该库之前我们必须了解一些基础知识。主要包含前端和Native两大部分。

2.1 前端部分——HTML

Keypoint:

  • <script> 标签包裹的是JavaScript代码
  • window、iframe
  • setTimeout(0)

2.2 前端部分——JavaScript

Keypoint:

  • JavaScript函数、对象

资料:

2.3 Native部分-关于UIWebView

__TVOS_PROHIBITED @protocol UIWebViewDelegate <NSObject>

@optional
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType;
- (void)webViewDidStartLoad:(UIWebView *)webView;
- (void)webViewDidFinishLoad:(UIWebView *)webView;
- (void)webView:(UIWebView *)webView didFailLoadWithError:(NSError *)error;

@end

UIWebView的代理UIWebViewDelegate,会在UIWebView各个事件节点收到回调消息。其中最重要的是- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType

当UIWebView加载URL page或者iframe设置src的时候,UIWebViewDelegate都会执行该回调。

3 WebViewJavascriptBridge设计

在分析WebViewJavascriptBridge源码之前,我们先聊一下WebViewJavascriptBridge设计。

3.1 整体框架

图片

WebViewJavascriptBridge整体框架如上图所示。

包含4部分:

  • 前端业务逻辑
  • 前端js bridge基础设施
  • Native js bridge基础设施
  • Native业务逻辑

3.2 js bridge基础设施

总的来说js bridge基础设施主要由3部分组成:

  1. 消息流: FE和Native之间的消息传递过程;
  2. 消息体(message):message即Native和前端消息流中的消息体,主要有4个部分:函数名、参数、回调ID、响应ID;
  3. 消息队列(FE message queue):前端消息队列用来暂存前端到Native的消息体。

3.2.1 消息流

消息流如下图所示。

图片

从图中我们可以看出消息流有两个参与者,即调用方被调方调用方发起请求,收到对方的回调消息。被调方收到请求,执行请求,发送回调消息。Native和FE都可能是调用方和被调用方,所以Native和FE都至少包含两部分功能:

  • send(发送自己的调用请求到对端)
  • receive(收到了来自对端的调用请求)

3.2.2 消息体

消息体有四个成员:

  1. 函数名
  2. 参数
  3. callbackID
  4. responseID

其中函数名和参数都很好理解。这里我们主要说一下callbackID和responseID。

调用方在发起调用的同时设置回调块,该回调块在被调方执行完任务后再执行。具体的实现手段是,调用方在拼接消息体的时候,把回调块管理起来,并设置一个唯一的ID, 放到消息体的callbackID上面。 此时被调方收到的消息包含callbackID,在执行完成对应函数后,会生成一个应答消息,告知对方自己已经执行完成,这个应答消息也是一个消息体,该消息体的responseID设置为其所应答消息的callbackID,表示对该消息的应答。这时,调用方收到应答消息,检查responseID,匹配后找到之前对应的回调块并执行。

样例如图所示:


图片

4 WebViewJavascriptBridge实现

4.1 消息流和消息队列实现

消息流(前端到Native)

前端到Native的消息流由隐藏的iframe发起。每次调用js bridge函数时设置iframe的src,然后,Native的UIWebViewDelegate收到- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType回调,在回调上加一些额外的逻辑区分,Native就知道前端发起了js bridge函数调用。

消息流(Native到前端)

Native到前端的消息流比较简单。它是由UIWebView本身完成。UIWebView的- (nullable NSString *)stringByEvaluatingJavaScriptFromString:(NSString *)script可以直接执行js命令。Native要调用前端的方法时,可以把方法转化为js命令直接调用。

消息队列(FE message queue)

前端消息队列用来暂存前端到Native的消息体。
相关的点如下:

  • 前端设置iframe的src之前会先把消息存到消息队列;
  • Native收到回调后,调用相关js命令从前端获取消息队列,得到消息队列后,按照消息队列的每条消息执行相应操作——函数调用。

4.2 前端js bridge源码

4.2.1 send

参考前端到Native消息流。send通过iframe设置src和messageQueue缓存消息体实现。


// 前端调用Native
function callHandler(handlerName, data, responseCallback) {
 if (arguments.length == 2 && typeof data == 'function') {
  responseCallback = data;
  data = null;
 }
 
 var message = { handlerName:handlerName, data:data };
 if (responseCallback) { // 回调管理
  var callbackId = 'cb_'+(uniqueId++)+'_'+new Date().getTime();
  responseCallbacks[callbackId] = responseCallback; 
  message['callbackId'] = callbackId;
 }
 sendMessageQueue.push(message);
 messagingIframe.src = 'wvjbscheme://__WVJB_QUEUE_MESSAGE__';
}

// 获取并清空message queue,暴露给OC
function _fetchQueue() {
 var messageQueueString = JSON.stringify(sendMessageQueue);
 sendMessageQueue = [];
 return messageQueueString;
}

4.2.2 receive

参考Native到前端的消息流。receive通过registerHandler注册js bridge函数,通过_handleMessageFromObjC方法执行messageHandlers里面的函数体。

// 前端注册js bridge方法供OC调用,比OC直接调用js普通方法好在对回调的支持上面。
var messageHandlers = {};
function registerHandler(handlerName, handler) {
 messageHandlers[handlerName] = handler; 
}
// OC调用处理,暴露给OC
function _handleMessageFromObjC(messageJSON) {
 var message = JSON.parse(messageJSON);
 var messageHandler;
 var responseCallback;

 if (message.responseId) { // 回调管理(responseId匹配查找)=> message有responseId表示是一个回调调用
  responseCallback = responseCallbacks[message.responseId]; // 这个responseId必须要与当时消息寄送时所填写的responseId一致
  if (!responseCallback) {
   return;
  }
  responseCallback(message.responseData);
  delete responseCallbacks[message.responseId];
 } else {
  if (message.callbackId) { 
   var callbackResponseId = message.callbackId;
   responseCallback = function(responseData) {
           var message = { handlerName:message.handlerName, responseId:callbackResponseId, responseData:responseData };
                         sendMessageQueue.push(message);
                         messagingIframe.src = 'wvjbscheme://__WVJB_QUEUE_MESSAGE__';
   };
  }
  
  // messageHandlers在这里
  var handler = messageHandlers[message.handlerName];
  if (!handler) {
   console.log("WebViewJavascriptBridge: WARNING: no handler for message from ObjC:", message);
  } else {
   handler(message.data, responseCallback);
  }
 }
}

4.3 Native js bridge源码

4.3.1 send

参考Native到前端的消息流。Native的send是先拼接出js命令,再直接执行stringByEvaluatingJavaScriptFromString

- (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;
    }
    [self _queueMessage:message];
}
//命令拼接
- (void)_dispatchMessage:(WVJBMessage*)message {
    NSString *messageJSON = [self _serializeMessage:message pretty:NO];
    [self _log:@"SEND" json:messageJSON];
    messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\\" withString:@"\\\\"];
    messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\"" withString:@"\\\""];
    messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\'" withString:@"\\\'"];
    messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\n" withString:@"\\n"];
    messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\r" withString:@"\\r"];
    messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\f" withString:@"\\f"];
    messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\u2028" withString:@"\\u2028"];
    messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\u2029" withString:@"\\u2029"];
    
    NSString* javascriptCommand = [NSString stringWithFormat:@"WebViewJavascriptBridge._handleMessageFromObjC('%@');", messageJSON];
    if ([[NSThread currentThread] isMainThread]) {
        [self _evaluateJavascript:javascriptCommand];

    } else {
        dispatch_sync(dispatch_get_main_queue(), ^{
            [self _evaluateJavascript:javascriptCommand];
        });
    }
}
//命令执行
- (NSString*) _evaluateJavascript:(NSString*)javascriptCommand
{
    return [_webView stringByEvaluatingJavaScriptFromString:javascriptCommand];
}

4.3.2 receive

参考FE到Native消息流。

//Native js bridge方法管理(给js用的)
- (void)registerHandler:(NSString *)handlerName handler:(WVJBHandler)handler {
    _base.messageHandlers[handlerName] = [handler copy];
}
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {
    
    // core code {
    NSURL *url = [request URL];
    __strong WVJB_WEBVIEW_DELEGATE_TYPE* strongDelegate = _webViewDelegate;
    if ([_base isCorrectProcotocolScheme:url]) {
        if ([_base isQueueMessageURL:url]) {
            // 获取JS的messageQueue
            NSString *messageQueueString = [self _evaluateJavascript:[_base webViewJavascriptFetchQueyCommand]];
            [_base flushMessageQueue:messageQueueString];
        }
        return NO;
    }
    // core code }
  
}
-(NSString *)webViewJavascriptFetchQueyCommand {
    return @"WebViewJavascriptBridge._fetchQueue();";
}
- (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;
    }

    // 拿到消息,按照消息handler
    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
                };
            }
            
            // 在messageHandlers里面查找handler
            WVJBHandler handler = self.messageHandlers[message[@"handlerName"]];
            
            if (!handler) {
                NSLog(@"WVJBNoHandlerException, No handler for message from JS: %@", message);
                continue;
            }
            // 执行handler
            handler(message[@"data"], responseCallback);
        }
    }
}

5总结

本文从JS bridge的基础知识讲到WebViewJavascriptBridge的源码实现。涉及的点有消息流,消息体,消息队列等。其中比较有意思的是回调实现原理。算是对自己阅读代码的一个记录。

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

推荐阅读更多精彩内容