写给自己看系列之WebViewJavascriptBridge源码分析

原文:橘子不酸丶
转载:https://juejin.im/post/5e706726518825491b11e52a

前言

最近由于项目使用,在开发过程中再次研读了WebViewJavascriptBridge这个在移动开发中原生与h5交互的库。写下本文来记录源码的分析以及过程。

使用

首先先来看一下WebViewJavascriptBridge库的文件结构和大致用途。总共有四个类,逻辑也并不复杂,加起来代码不超过一千行。

  • WebViewJavascriptBridge_JS (JS文件类,定义了要加载的JS代码,整体作为字符串)
  • WebViewJavascriptBridgeBase (承载了与H5交互的核心类,主要的交互方法所在的基类)
  • WebViewJavascriptBridge (封装的UIWebView和WebView(OS)交互类)
  • WKWebViewJavascriptBridge (封装的WKWebView交互类,处理WKWebView的一些回调)

我们来看一下Bridge的使用过程,也就是Bridge的初始化使用。Bridge在初始化的时候会接管WebView的回调,需要重设Delegate来把回调转接回来。

self.webView = [[WKWebView alloc] initWithFrame:self.view.bounds];
//创建 WebViewJavascriptBridge
self.bridge = [WebViewJavascriptBridge bridgeForWebView:self.webView];
//设置完Bridge之后把delegate再设置回webview
[self.bridge setWebViewDelegate:self];

// 注册 handler
[self.handler registerHandlersForJSBridge:self.bridge];

这里使用一个handler类来实现Bridge方法的注册实现。主要用于桥接方法的分类处理和后续扩展。

NSString * const kMJYPBaseWebViewBridgeHandlerPre = @"__mjypExport__bridgeHandler_";

//公开接口, m方法名,n参数,c回调 (目前只是两个参数arg和callback)
#define BRIDGE_HANDLER_EXTERN_METHODX(m, n, c) \
- (void)__mjypExport__bridgeHandler_##m:(NSDictionary *)arg callback:(MJYPWebViewHandlerResponseBlock)callback

/// 注册 Handler
- (void)registerHandlersForJSBridge:(WebViewJavascriptBridge *)bridge;

在Handler中注册方法只需要宏定义就可以,使用runtime读取所有的注册方法。

NSString * const kMJYPBaseWebViewBridgeHandlerPre = @"__mjypExport__bridgeHandler_";
- (void)registerHandlersForJSBridge:(WebViewJavascriptBridge *)bridge {
    NSArray *handlerMethods = [self bridgeHandlerMethods];
    for (NSString *aHandlerName in handlerMethods) {
        s_ws(weakself);
        [bridge registerHandler:[self getHandlerNameWithBridgeMethod:aHandlerName] handler:^(id data, WVJBResponseCallback responseCallback) {
            s_ss(strongself, weakself);
            NSMutableDictionary *args = [NSMutableDictionary dictionary];
            if ([data isKindOfClass:[NSDictionary class]]) {
                [args addEntriesFromDictionary:data];
            }
            SEL selector = NSSelectorFromString(aHandlerName);
            if ([strongself respondsToSelector:selector]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
                [strongself performSelector:selector withObject:args withObject:responseCallback];
#pragma clang diagnostic pop
            }
        }];
    }
}

- (NSArray *)bridgeHandlerMethods {
    if (!_methods) {
        NSMutableArray *handlerMethods = [NSMutableArray new];
        
        unsigned int methodCount;
        Class cls = object_getClass(self);
        while (cls && cls != [NSObject class] && cls != [NSProxy class]) {
            Method *methods = class_copyMethodList(cls, &methodCount);
            
            for (unsigned int i = 0; i < methodCount; i++) {
                Method method = methods[i];
                SEL selector = method_getName(method);
                NSString *selName = NSStringFromSelector(selector);
                if ([selName hasPrefix:kMJYPBaseWebViewBridgeHandlerPre]) {
                    [handlerMethods addObject:selName];
                }
            }
            
            free(methods);
            cls = class_getSuperclass(cls);
        }
        
        _methods = [handlerMethods copy];
    }
    return _methods;
}

- (NSString *)getHandlerNameWithBridgeMethod:(NSString *)method {
    if ([method isKindOfClass:[NSString class]] && [method hasPrefix:kMJYPBaseWebViewBridgeHandlerPre]) {
        NSArray *components = [method componentsSeparatedByString:@":"];
        if (components.count) {
            NSString *handlerName = [components firstObject];
            return [handlerName stringByReplacingOccurrencesOfString:kMJYPBaseWebViewBridgeHandlerPre withString:@""];
        }
    }
    return method;
}

在外部初始化bridge之后,再来看一下WebViewJavascriptBridge内部的初始化逻辑。过程也很简单,初始化一个Bridge以及初始化一个BridgeBase对象来交互。

+ (instancetype)bridgeForWebView:(WKWebView*)webView {
    WKWebViewJavascriptBridge* bridge = [[self alloc] init];
    [bridge _setupInstance:webView];
    [bridge reset];
    return bridge;
}

这里我们主要看WebViewJavascriptBridgeBase里就可以,初始化时的reset处理,startupMessageQueue数组主要作为存储h5页面加载Bridge的JS环境之前调用的message。responseCallbacks主要为存储原生调用H5方法时的回调。

self.startupMessageQueue = [NSMutableArray array];
self.responseCallbacks = [NSMutableDictionary dictionary];
_uniqueId = 0;

再看一下H5端的初始化,我们发现WebViewJavascriptBridge会在Native和H5端都初始化一个Bridge对象。并且在H5初始化时调用'https://_ _ bridge_loaded__'来触发Native的方法并初始化JS环境挂载。

function setupWebViewJavascriptBridge(callback) {
    if (window.WebViewJavascriptBridge) { return callback(WebViewJavascriptBridge); }
    if (window.WVJBCallbacks) { return window.WVJBCallbacks.push(callback); }
    window.WVJBCallbacks = [callback];
    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)
}

而在native中通过WebView的代理方法来拦截并响应H5的初始化。如果url是bridge_loaded就加载Bridge的JS环境,如果是Message就响应消息处理。

if ([_base isWebViewJavascriptBridgeURL:url]) {
    if ([_base isBridgeLoadedURL:url]) {
        [_base injectJavascriptFile];
    } else if ([_base isQueueMessageURL:url]) {
        [self WKFlushMessageQueue];
    } else {
        [_base logUnkownMessage:url];
    }
    decisionHandler(WKNavigationActionPolicyCancel);
    return;
}

交互分析

接下来我们来分析具体的原生与H5交互过程,

原生调用H5

WebViewJavaScriptBridge在原生和H5之间传递消息的方法很巧妙,都会封装成一个message(也就是一个Dictionary)来传递。原生调用H5的方法时,首先会把回调block存储在self.responseCallbacks中并生成callbackId通过callbackId来传递到h5并传回来再调用。WebViewJavaScriptBridge吧data(参数)handlerName(H5方法名)以及回调callbackId封装为一个message并通过WebViewJavascriptBridge._handleMessageFromObjC来传递到h5。
这里很明显,Bridge如果已经加载了JS环境就处理消息,如果还未加载JS环境则存储消息在startupMessageQueue中等待JS环境加载完成后处理。injectJavascriptFile之后JS环境加载完成则把startupMessageQueue置空。

- (void)_queueMessage:(WVJBMessage*)message {
    if (self.startupMessageQueue) {
        [self.startupMessageQueue addObject:message];
    } else {
        [self _dispatchMessage: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];
        });
    }
}

原生发送消息执行JS的_handleMessageFromObjC之后,H5接收到并转发执行JS中的_dispatchMessageFromObjC()方法(这里可以通过WebViewJavascriptBridge_JS查看)。

JS端接收到message之后,会将它解析成为JS对象,然后去使用data、callbackId和handlerName。然后根据handlerName去messageHandlers里面去对应的handler函数,然后去执行这个函数。

if (message.callbackId) {
    var callbackResponseId = message.callbackId;
    responseCallback = function(responseData) {
        _doSend({ handlerName:message.handlerName, responseId:callbackResponseId,responseData:responseData });
    };
}
handler(message.data, responseCallback);

可以看到调用JS注册的handler时传入两个参数,第一个参数是传过来的data数据,第二个参数是responseCallback。responseCallback就是根据callbackId和JS方法返回值的function。然后handler里就可以处理接收到的回调了。handler通过responseId和回调数据来触发Native的调用CallBack方法,可以看到Native在收到message时如果包含responseId就视为block回调处理,则从responseCallbacks中取出之前存储的回调block并执行。这样从Native调H5并收到H5回调数据的流程就完成了。

if (responseId) {
    WVJBResponseCallback responseCallback = _responseCallbacks[responseId];
    responseCallback(message[@"responseData"]);
    [self.responseCallbacks removeObjectForKey:responseId];
}

H5调用原生方法

function _doSend(message, responseCallback) {
   if (responseCallback) {
      var callbackId = 'cb_'+(uniqueId++)+'_'+new Date().getTime();
      responseCallbacks[callbackId] = responseCallback;
      message['callbackId'] = callbackId;
   }
   sendMessageQueue.push(message);
   messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
}

从上边的H5调用原生的方法可以看到,逻辑和原生调用H5原理类似,JS中会存储callBack并生成callbackId,然后通过封装的message传递到Native,url为'https://_ _ wvjb_queue_message_ _'。
客户端在webview的代理方法中识别到message类型并处理,客户端会把message解析成WVJBMessage(NSDictionary)并处理

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);

可以看到如果存在callbackId则认为有回调,如果没有则无需回调(此时给了一个空的回调block),识别到callbackId之后客户端会执行对应的HandlerName注册方法,并可以在注册方法里回调给H5方法。回调时则是把callbackId作为responseId和responseData一起回调给H5,H5在识别到responseId之后则识别为回调并从之前存储的callback中取出来执行对应回调。

结束

至此整个交互过程就结束了,整体来说不论是从H5调用Native还是Native调用H5,思想都是一致的,从存储回调block,然后拿blockId、data、handlerName来调用对方,并通过对方返回的responseId来识别是否block回调。

你可能感兴趣的:(写给自己看系列之WebViewJavascriptBridge源码分析)