写给自己看系列之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<NSString *> *)bridgeHandlerMethods {
    if (!_methods) {
        NSMutableArray<NSString *> *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回调。

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

推荐阅读更多精彩内容