敞开生长之旅!这是我参加「日新计划 12 月更文挑战」的第2天,点击检查活动概况

前言

H5页面具有跨平台、开发简略、上线不需要跟从App的版别等优点,但H5页面也有体会不如native好、没有native稳定等问题。所以现在大部分App都是运用Hybrid混合开发的。

当然有了H5页面就少不了H5与native交互,交互就会用到bridge的才干了。WebViewJavascriptBridge是一个native与JS进行音讯互通的第三方库,本章会简略解析一下WebViewJavascriptBridge的源码和完结原理。

通讯原理

JavaScriptCore

JavaScriptCore作为iOS的JS引擎为原生编程言语OC、Swift供给调用JS程序的动态才干,还能为 JS供给原生才干来补偿前端所缺才干。 iOS中与JS通讯运用的是JavaScriptCore库,正是因为JavaScriptCore这种起到的桥梁作用,所以也出现了很多运用JavaScriptCore开发App的结构,比方RN、Weex、小程序、Webview Hybrid等结构。 如图:

iOS之WebViewJavascriptBridge浅析

当然JS引擎不光有苹果的JavaScriptCore,谷歌有V8引擎、Mozilla有SpiderMoney

JavaScriptCore本章只简略介绍,后边首要解析WebViewJavascriptBridge。因为uiwebview现已不再运用了,所今后边提到的webview都是wkwebview,demo也是以wkwebview进行解析。

源码解析

代码结构

除了引擎层外,还需要native、h5和WebViewJavascriptBridge三层才干完结一整个信息通路。WebViewJavascriptBridge便是中间那个担任通信的SDK。

WebViewJavascriptBridge的中心类首要包括几个:

  • WebViewJavascriptBridge_JS:是一个JS的字符串,作用是JS环境的Bridge初始化和处理。担任接纳native发给JS的音讯,而且把JS环境的音讯发送给native。
  • WKWebViewJavascriptBridge/WebViewJavascriptBridge:首要担任WKWebView和UIWebView相关环境的处理,而且把native环境的音讯发送给JS环境。
  • WebViewJavascriptBridgeBase:首要完结了native环境的Bridge初始化和处理。

iOS之WebViewJavascriptBridge浅析

初始化

WebViewJavascriptBridge是怎么完结初始化的呢,首要要有webview容器,所以要对webview容器进行初始化,设置署理,初始化WebViewJavascriptBridge方针,加载URL。

    WKWebView* webView = [[NSClassFromString(@"WKWebView") alloc] initWithFrame:self.view.bounds];
    webView.navigationDelegate = self;
    [self.view addSubview:webView];
    // 敞开打印
    [WebViewJavascriptBridge enableLogging];
    // 创建bridge方针
    _bridge = [WebViewJavascriptBridge bridgeForWebView:webView];
    // 设置署理
    [_bridge setWebViewDelegate:self];

这里加载的便是JSBridgeDemoApp这个本地的html文件。

    NSString* htmlPath = [[NSBundle mainBundle] pathForResource:@"JSBridgeDemoApp" ofType:@"html"];
    NSString* appHtml = [NSString stringWithContentsOfFile:htmlPath encoding:NSUTF8StringEncoding error:nil];
    NSURL *baseURL = [NSURL fileURLWithPath:htmlPath];
    [webView loadHTMLString:appHtml baseURL:baseURL];

再看一下JSBridgeDemoApp这个html文件。

function setupWebViewJavascriptBridge(callback) {
// 第一次调用这个办法的时分,为false
    if (window.WebViewJavascriptBridge) { return callback(WebViewJavascriptBridge); }
// 第一次调用的时分,为false
    if (window.WVJBCallbacks) { return window.WVJBCallbacks.push(callback); }
// 把callback方针赋值给方针
  window.WVJBCallbacks = [callback];
// 加载WebViewJavascriptBridge_JS中的代码
// 相当于完结了一个到https://__bridge_loaded__的跳转
    var WVJBIframe = document.createElement('iframe');
  WVJBIframe.style.display = 'none';
  WVJBIframe.src = 'https://__bridge_loaded__';
  document.documentElement.appendChild(WVJBIframe);
  setTimeout(function() { document.documentElement.removeChild(WVJBIframe) }, 0)
  }
// 驱动一切hander的初始化
setupWebViewJavascriptBridge(function(bridge) {
...
}

在JSBridgeDemoApp的script标签下,声明了一个名为setupWebViewJavascriptBridge的办法,在加载html后直接进行了调用。 setupWebViewJavascriptBridge办法中最中心的代码是:

iOS之WebViewJavascriptBridge浅析
创建一个iframe标签,然后加载了链接为 https://bridge_loaded 的内容。相当于在当前页面内容完结了一个到 https://bridge_loaded 的内部跳转。 ps:iframe标签用于在网页内显示网页,也运用iframe作为链接的方针。

html文件内部完结了这个跳转后native端是怎么监听的呢,在webview的署理里有一个办法:decidePolicyForNavigationAction 这个署理办法的作用是只需有webview跳转,就会调用到这个办法。代码如下:

// 只需webview有跳转,就会调用webview的这个署理办法
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
    if (webView != _webView) { return; }
    NSURL *url = navigationAction.request.URL;
    __strong typeof(_webViewDelegate) strongDelegate = _webViewDelegate;
    // 假如是WebViewJavascriptBridge发送或许接纳音讯,则特殊处理。否则依照正常流程处理
    if ([_base isWebViewJavascriptBridgeURL:url]) {
        if ([_base isBridgeLoadedURL:url]) {
            // 是否是 https://__bridge_loaded__ 这种初始化加载音讯
            [_base injectJavascriptFile];
        } else if ([_base isQueueMessageURL:url]) {
            // https://__wvjb_queue_message__
            // 处理WEB发过来的音讯
            [self WKFlushMessageQueue];
        } else {
            [_base logUnkownMessage:url];
        }
        decisionHandler(WKNavigationActionPolicyCancel);
        return;
    }
    // webview的正常署理履行流程
...

从上面的代码中能够看到,假如监听的webview跳转不是WebViewJavascriptBridge发送或许接纳音讯就正常履行流程,假如是WebViewJavascriptBridge发送或许接纳音讯则对此阻拦不跳转,而且针对音讯进行处理。 当音讯url是https://bridge_loaded 的时分,会去注入WebViewJavascriptBridge_js到JS中:

// 将WebViewJavascriptBrige_JS中的办法注入到webview中而且履行
- (void)injectJavascriptFile {
    NSString *js = WebViewJavascriptBridge_js();
    // 把javascript代码注入webview中履行
    [self _evaluateJavascript:js];
    // javascript环境初始化完结今后,假如有startupMessageQueue音讯,则当即发送音讯
    if (self.startupMessageQueue) {
        NSArray* queue = self.startupMessageQueue;
        self.startupMessageQueue = nil;
        for (id queuedMessage in queue) {
            [self _dispatchMessage:queuedMessage];
        }
    }
}

[self _evaluateJavascript:js];便是履行webview中的evaluateJavaScript:办法。把JS写入webview。所以履行完此处代码JS傍边就有bridge这个方针了。初始化完结。

总结:在加载h5页面后会调用setupWebViewJavascriptBridge办法,该办法内创建了一个iframe加载内容为 https://bridge_loaded ,该音讯被decidePolicyForNavigationAction监听到,然后履行injectJavascriptFile去读取WebViewJavascriptBridge_js将WebViewJavascriptBridge方针注入到当前h5中。

WebViewJavascriptBridge 方针

整个WebViewJavascriptBridge_js文件其实便是一个字符串形式的js代码,里面包括WebViewJavascriptBridge和相关bridge调用的办法。

// 初始化Bridge方针,OC能够经过WebViewJavascriptBridge来调用JS里面的各种办法
window.WebViewJavascriptBridge = {
    registerHandler: registerHandler, // JS中注册办法
    callHandler: callHandler, // JS中调用OC的办法
    disableJavscriptAlertBoxSafetyTimeout: disableJavscriptAlertBoxSafetyTimeout,
    _fetchQueue: _fetchQueue, // 把音讯转换成JSON串
    _handleMessageFromObjC: _handleMessageFromObjC // OC调用JS的进口办法
};

WebViewJavascriptBridge方针里中心的办法有:

  • registerHandler:JS中注册办法
  • callHandler: JS中调用native的办法
  • _fetchQueue: 把音讯转换成JSON字符串
  • _handleMessageFromObjC:native调用JS的进口办法

当初始化完结后,WebViewJavascriptBridge方针和方针里的办法就现已存在而且可用了。

JS和native是怎么相互传递音讯的呢?从上面的代码中能够看到假如JS想要发送音讯给native就会调用callHandler办法;假如native想要调用JS办法那JS侧就必须先注册一个registerHandler办法。

相对应的咱们看一下native侧是怎么与JS传递音讯的,其实接口标准是一致的,native调JS的办法运用callHandler办法:

id data = @{ @"dataFromOC": @"aaaa!" };
    [_bridge callHandler:@"OCToJSHandler" data:data responseCallback:^(id response) {
        NSLog(@"JS回调的数据是:%@", response);
    }];

JS调native办法在native侧就必须先注册一个registerHandler办法:

    // 注册事情(h5调App)
    [_bridge registerHandler:@"JSTOOCCallback" handler:^(id data, WVJBResponseCallback responseCallback) {
        NSLog(@"JSTOOCCallback called: %@", data);
        responseCallback(@"Response from JSTOOCCallback");
    }];

也便是说native像JS发送音讯的话,JS侧要先注册该办法registerHandler,native侧调用callHandler; JS像native发送音讯的话,native侧要先注册registerHandler,JS侧调用callHandler。这样才干完结双端通信。

如图:

iOS之WebViewJavascriptBridge浅析

native向JS发送音讯

现在要从native侧向JS侧发送一条音讯,办法名为:”OCToJSHandler”,而且拿到JS的回调,详细完结细节如下:

JS侧

native向JS发送数据,首要要在JS侧去注册这个办法:

bridge.registerHandler('OCToJSHandler', function(data, responseCallback) {
    ...
})

这个registerHandler的完结在WebViewJavascriptBridge_JS是:

// web端注册一个音讯办法,将注册的办法存储起来
function registerHandler(handlerName, handler) {
    messageHandlers[handlerName] = handler;
}

便是将这个注册的办法存储到messageHandlers这个map中,key为办法称号,value为function(data, responseCallback) {}这个办法。

native侧

native侧调用bridge的callHandler办法,传参为data和一个callback回调

id data = @{ @"dataFromOC": @"aaaa!" };
[_bridge callHandler:@"OCToJSHandler" data:data responseCallback:^(id response) {
    NSLog(@"JS回调的数据是:%@", response);
}];

接下来会走到WebViewJavascriptBridgeBase的-sendData: responseCallback: handlerName:办法,该办法中将”data”和”handlerName”存入到一个message字典中,假如存在callback会生成一个callbackId一并存入到message字典中,而且将该回调存入到responseCallbacks中,key为callbackId,value为这个callback。代码如下:

// 一切信息存入字典
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];

将message存储到行列等候履行,履行该条message时会先将message进行序列化,序列化完结后将message拼接到字符串WebViewJavascriptBridge._handleMessageFromObjC(‘%@’);中,然后履行_evaluateJavascript履行该js办法。

// 把OC音讯序列化、而且转化为JS环境的格局,然后在主线程中调用_evaluateJavascript
- (void)_dispatchMessage:(WVJBMessage*)message {
    NSString *messageJSON = [self _serializeMessage:message pretty:NO];
    NSString* javascriptCommand = [NSString stringWithFormat:@"WebViewJavascriptBridge._handleMessageFromObjC('%@');", messageJSON];
    [self _evaluateJavascript:javascriptCommand];
}

_handleMessageFromObjC办法会将messageJSON传递给_dispatchMessageFromObjC进行处理。 首要将messageJSON进行解析,依据handlerName取出存储在messageHandlers中的办法。假如该message中存在callbackId,将callbackId作为参数生成一个回调放到responseCallback中。 代码如下:

function _doDispatchMessageFromObjC() {
// 解析发送过来的JSON
    var message = JSON.parse(messageJSON);
    var messageHandler;
    var responseCallback;
    // 自动调用
    // 假如有callbackid
    if (message.callbackId) {
    // 将callbackid当做callbackResponseId再回来回去
        var callbackResponseId = message.callbackId;
        responseCallback = function(responseData) {
        // 把音讯从JS发送到OC,履行详细的发送操作
            _doSend({ handlerName:message.handlerName, responseId:callbackResponseId, responseData:responseData });
            };
        // 获取JS注册的函数,取出音讯里的handlerName
        var handler = messageHandlers[message.handlerName];
        // 调用JS中的对应函数处理
        handler(message.data, responseCallback);
            }
    }

handler办法其实便是名为”OCToJSHandler”的办法,这时就走到了registerHandler里的那个function(data, responseCallback) {}办法了。咱们看一下办法内部的详细完结:

bridge.registerHandler('OCToJSHandler', function(data, responseCallback) {
    // OC中传过来的数据
    log('从OC传过来的数据是:', data)
    // JS回来数据
    var responseData = { 'dataFromJS':'bbbb!' }
    responseCallback(responseData)
})

data便是从native传过来的数据,responseCallback便是保存的回调,然后又生成了新数据作为参数给到了这个回调。

responseCallback的完结是:

responseCallback = function(responseData) {
    // 把音讯从JS发送到OC,履行详细的发送操作
    _doSend({ handlerName:message.handlerName, responseId:callbackResponseId, responseData:responseData });
};

将该办法的handlerName、生成的callbackResponseId(也便是callbackId)以及JS回来的数据一同给到_doSend办法。

_doSend办法将message存储到sendMessageQueue音讯列表中,并运用messagingIframe加载了一次https://wvjb_queue_message

// 把音讯从JS发送到OC,履行详细的发送操作
    function _doSend(message, responseCallback) {
    // 把音讯放入音讯列表
    sendMessageQueue.push(message);
    // 宣布js对oc的调用,让webview履行跳转操作,能够在decidePolicyForNavigationAction:中阻拦到js发给oc的音讯
    messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
    }

这时webview的监听办法decidePolicyForNavigationAction监听到了https://wvjb_queue_message 音讯后仍是履行WebViewJavascriptBridge._fetchQueue()去取数据,取到数据后依据responseId当初在_responseCallbacks中存储的callback,然后履行callback、移除responseCallbacks中的数据。到此为止,整个native向JS发送音讯的过程就完结了。

总结:

  1. JS中先调用registerHandler将办法存储到messageHandlers中
  2. native调用callHandler:办法,将音讯内容存储到message中,回调存储到responseCallbacks中。
  3. 将message音讯序列化经过_evaluateJavascript办法履行_handleMessageFromObjC
  4. 将message解析,经过message.handlerName从messageHandlers取出该办法;依据message.callbackId生成回调
  5. 履行该办法,回调

JS向native发送音讯

从JS向native发音讯其实和native向JS发音讯的接口层面是差不多的。

native侧

native侧首要要注册一个JSTOOCCallback办法

[_bridge registerHandler:@"JSTOOCCallback" handler:^(id data, WVJBResponseCallback responseCallback) {
    responseCallback(@"Response from JSTOOCCallback");
}];

该办法也相同是将该办法的callback存储起来,存储到messageHandlers傍边,key便是办法名”JSTOOCCallback”,value便是callback。

JS侧

JS侧会调用callHandler办法:

// 调用oc中注册的那个办法
bridge.callHandler('JSTOOCCallback', {'foo': 'bar'}, function(response) {
    log('JS 取到的回调是:', response)
})

这个callHandler办法相同会调用_doSend办法:将callback存储到responseCallbacks中,key为callbakid;将音讯存储到sendMessageQueue中;messagingIframe履行https://wvjb_queue_message

native的decidePolicyForNavigationAction办法监听到该音讯后相同经过WebViewJavascriptBridge._fetchQueue()去取音讯。

依据callbackId创建一个responseCallback,依据message的handlerName从messageHandlers取出该回调,然后履行:

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"]];
handler(message[@"data"], responseCallback);

调用完这个办法后,该音讯现已收到,然后将回调的内容回调给JS。 经过上面的代码能够看到,回调JS的内容便是callbackId和responseData生成的message,调用_queueMessage办法。

_queueMessage办法上面现已看过了,便是序列化音讯、参加行列、履行WebViewJavascriptBridge._handleMessageFromObjC(‘%@’);办法。

JS收到该音讯后,处理回来的音讯,从responseCallbacks中依据message中的responseId取出callback而且履行。最后删除responseCallbacks中的数据,JS向native发送数据就完结了。

总结:

  1. native侧调用registerHandler办法注册办法,办法名为JSTOOCCallback,将音讯存储到messageHandlers中,key为办法名,value为callback。
  2. JS侧调用callHandler办法:将responseCallback存储到responseCallbacks中;将message存储到sendMessageQueue中;messagingIframe履行 http://wvjb_queue_message
  3. native侧监听到该音讯后调用WebViewJavascriptBridge._fetchQueue()去取数据
  4. 依据handlerName从messageHandlers中取出该callback;依据callbackId创建callback方针作为参数放到handlerName的办法中;履行该回调。

总结

综上,WebViewJavascriptBridge的中心流程就分析完了,最中心的点是JS经过加载iframe来告诉native侧;native侧经过evaluateJavaScript办法去履行JS。

从整个SDK来看,规划的非常好,值得学习学习:

  • 运用外观形式统一调用接口,比方初始化WebViewJavascriptBridge的时分,不需要关怀运用方运用的是UIWebView仍是WKWebView,内部现已处理好了。
  • 接口统一,不管是native侧仍是JS侧,调用办法便是callHandler、注册办法便是registerHandler,不需要重视内部完结,运用非常方便。
  • 代码简练,逻辑明晰,层次分明。从类的分布就能很明晰的看出各自的功能是什么。
  • 责任单一,比方decidePolicyForNavigationAction办法只担任监听事情、_fetchQueue是担任把音讯转换成JSON字符串回来、_doSend是发送音讯到native、_dispatchMessageFromObjC是担任处理从OC回来的音讯等。虽然decidePolicyForNavigationAction也能接纳音讯,但这样就不会这么精简了。
  • 扩展性好,现在decidePolicyForNavigationAction虽然只要初始化和发音讯两个事情,假如有其他事情还能够再扩展,这也得益于办法规划的责任单一,扩展对原有办法影响会很小。