OC和JS交互(UIWebView)中级篇3

上一篇博文重点讲了下我们项目中最常用的JS调用OC, 花开两朵各表一枝, 本文将重点讲下OC调用JS.

OC调用JS的入口在VC, 下面是代码

[self.bridge callHandler:@"getUserInfo" data:@{@"userId":@"DX001"} responseCallback:^(id responseData) {
        NSString *userInfo = [NSString stringWithFormat:@"%@,姓名:%@,年龄:%@", responseData[@"userID"], responseData[@"userName"], responseData[@"age"]];
        UIAlertController *vc = [UIAlertController alertControllerWithTitle:@"从网页端获取的用户信息" message:userInfo preferredStyle:UIAlertControllerStyleAlert];
        UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:@"取消" style:UIAlertActionStyleCancel handler:nil];
        UIAlertAction *okAction = [UIAlertAction actionWithTitle:@"好的" style:UIAlertActionStyleDefault handler:nil];
        [vc addAction:cancelAction];
        [vc addAction:okAction];
        [self presentViewController:vc animated:YES completion:nil];
    }];
WebViewJavascriptBridge.m
- (void)callHandler:(NSString *)handlerName data:(id)data responseCallback:(WVJBResponseCallback)responseCallback {
    [_base sendData:data responseCallback:responseCallback handlerName:handlerName];
}
WebViewJavascriptBridgeBase.m
- (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];
}

最终来到了WebViewJavascriptBridgeBasesendData方法里面, 这里创建一个NSMutableDictionary对象message, 并把VC传递进来的参数data = @{@"userId":@"DX001"}, handlerName = @"getUserInfo"还有responseCallback保存起来, 和之前JS保存responseCallback方法相似, 这里也是生成一个callbackId, 并把responseCallback保存在以callbackId为key的字典self.responseCallbacks中, 最后执行[self _queueMessage:message];

WebViewJavascriptBridgeBase.h
@interface WebViewJavascriptBridgeBase : NSObject

@property (strong, nonatomic) NSMutableArray* startupMessageQueue;

WebViewJavascriptBridgeBase.m
- (void)_queueMessage:(WVJBMessage*)message {
    if (self.startupMessageQueue) {
        [self.startupMessageQueue addObject:message];
    } else {
        [self _dispatchMessage:message];
    }
}

我们来回忆下上文是怎么使用_dispatchMessage

responseCallback = ^(id responseData) {
  if (responseData == nil) {
      responseData = [NSNull null];
  }
                    
  WVJBMessage* msg = @{ @"responseId":callbackId, @"responseData":responseData };
  [self _queueMessage:msg];
};

这里是在OC的block中执行了_queueMessage, 实际也是OC调用JS. 只是在上文中, OC调用JS不是重点. 好了这里也顺便分析下我们之前遗留下来的问题:startupMessageQueue是干什么的?

WebViewJavascriptBridgeBase.m
-(id)init {
    self = [super init];
    self.messageHandlers = [NSMutableDictionary dictionary];
    self.startupMessageQueue = [NSMutableArray array];
    self.responseCallbacks = [NSMutableDictionary dictionary];
    _uniqueId = 0;
    return(self);
}

- (void)dealloc {
    self.startupMessageQueue = nil;
    self.responseCallbacks = nil;
    self.messageHandlers = nil;
}

- (void)reset {
    self.startupMessageQueue = [NSMutableArray array];
    self.responseCallbacks = [NSMutableDictionary dictionary];
    _uniqueId = 0;
}

- (void)injectJavascriptFile {
    NSString *js = WebViewJavascriptBridge_js();
    [self _evaluateJavascript:js];
    if (self.startupMessageQueue) {
        NSArray* queue = self.startupMessageQueue;
        self.startupMessageQueue = nil;
        for (id queuedMessage in queue) {
            [self _dispatchMessage:queuedMessage];
        }
    }
}

- (void)_queueMessage:(WVJBMessage*)message {
    if (self.startupMessageQueue) {
        [self.startupMessageQueue addObject:message];
    } else {
        [self _dispatchMessage:message];
    }
}

startupMessageQueueWebViewJavascriptBridgeBase中的一个数组, 这个数组在WebViewJavascriptBridgeBase初始化的时候被创建, 但是只是一个空的数组, 并且在初始化注入的时候就被取出来并置空了, 所以后面正常情况是不存在_queueMessage走进if分支的, 只有一种情况, 就是在injectJavascriptFile还没执行的时候, 先进行了OC对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 = 'wvjbscheme://__BRIDGE_LOADED__';
            document.documentElement.appendChild(WVJBIframe);
            setTimeout(function() { document.documentElement.removeChild(WVJBIframe) }, 0)
        }

这段代码写进html的时候应该是不存在的, 因为在网页加载的时候OC就完成了注入, 但是如果上面的这段代码如果不在html中, 那还是有可能的, 而且在实际开发中, 难道我们还要要求前端的同事每个网页都加上面的一段代码, 也是不现实的. 所以作者应该是通过综合的考虑才加入了startupMessageQueue的, 好了, 在本例中, startupMessageQueue还是没有实际作用, 代码最终回到上文的后半部分, OC回调JS, 这里要注意一下的问题就是, 运行JS脚本可能会存在线程安全的问题, 所以, 一定要在主线程执行JS

if ([[NSThread currentThread] isMainThread]) {
        [self _evaluateJavascript:javascriptCommand];

    } else {
        dispatch_sync(dispatch_get_main_queue(), ^{
            [self _evaluateJavascript:javascriptCommand];
        });
    }

经过一些列调用_queueMessage->_dispatchMessage->_evaluateJavascript->_handleMessageFromObjC->_dispatchMessageFromObjC->_doDispatchMessageFromObjC, 好了, 还是来到下面这段代码

function _doDispatchMessageFromObjC() {
    var message = JSON.parse(messageJSON);
    var messageHandler;
    var responseCallback;
    if (message.responseId) {
        responseCallback = responseCallbacks[message.responseId];
        if (!responseCallback) {
            return;
        }
        responseCallback(message.responseData);
        delete responseCallbacks[message.responseId];
    } else {
        if (message.callbackId) {
            var callbackResponseId = message.callbackId;
            responseCallback = function(responseData) {
                _doSend({
                handlerName: message.handlerName,
                responseId: callbackResponseId,
                responseData: responseData
                });
            };
        }
        var handler = messageHandlers[message.handlerName];
        if (!handler) {
            console.log("WebViewJavascriptBridge: WARNING: no handler for message from ObjC:", message);
        } else {
            handler(message.data, responseCallback);
        }
    }
}

我们在Safari中下断点, 发现


image.png

, 这里有个小的tips, 因为这段脚本不在html的页面里面不能直接打断点, 要先在

bridge.registerHandler('getUserInfo', function(data, responseCallback) {
    console.log("OC中传递过来的参数:", data);
    // 把处理好的结果返回给OC
    responseCallback({"userID":"DX001", "userName":"旋之华", "age":"18", "otherName":"旋之华"})
});

responseCallback这里打断点, 然后调用JS接口, Safari左侧会出现调用堆栈, 里面有我们注入的代码, 这时候就可以在_dispatchMessageFromObjC里面打断点了, 好了, 继续分析_dispatchMessageFromObjC里面的代码.
由于调用的时候并没有给responseId赋值, 所以, 代码走到

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

var handler = messageHandlers[message.handlerName];
if (!handler) {
    console.log("WebViewJavascriptBridge: WARNING: no handler for message from ObjC:", message);
} else {
    handler(message.data, responseCallback);
}               

这里是和之前JS调用OC时候, OC回调JS不同的, 这里message.responseId是没有值的, message.callbackId是有值的, 所以会在这里创建一个JS的responseCallback, 后面取出handler并调用handler(message.data, responseCallback);, 下面来看下messageHandlers吧, 实际和OC注册很相似

function registerHandler(handlerName, handler) {
        messageHandlers[handlerName] = handler;
    }

还没完, 这里OC调用JS的时候也传递了一个block, 这个block最终传递到了JS

bridge.registerHandler('getUserInfo', function(data, responseCallback) {
    console.log("OC中传递过来的参数:", data);
    // 把处理好的结果返回给OC
    responseCallback({"userID":"DX001", "userName":"旋之华", "age":"18", "otherName":"旋之华"})
});       

把函数名作为key, 回调方法作为value, 建立messageHandlers字典, 所以最终执行handler(message.data, responseCallback);实际是调用了

bridge.registerHandler('getUserInfo', function(data, responseCallback) {
    console.log("OC中传递过来的参数:", data);
    // 把处理好的结果返回给OC
    responseCallback({"userID":"DX001", "userName":"旋之华", "age":"18", "otherName":"旋之华"})
});      

中函数体里的代码, 如果没有OC传递的block, OC调用JS就到此结束了, 输出console.log("OC中传递过来的参数:", data);完成调用, 但是OC传递了block, 所以还要继续分析, responseCallback是在刚才通过JS代码创建的回调, 只有OC传递了block才会创建.

responseCallback = function(responseData) {
    _doSend({ handlerName:message.handlerName, responseId:callbackResponseId, responseData:responseData });
};

在JS中调用responseCallback({"userID":"DX001", "userName":"旋之华", "age":"18", "otherName":"旋之华"})实际会来到_doSend, responseData正是JS中传递来的参数

image.png
, 而handlerNameresponseId都是OC调用的时候传递的参数.

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

进入_doSend, 由于没传递responseCallback, 所以if走不到, 这里还是把OC传递过来的message保存在sendMessageQueue中, 然后改变src触发OC执行, 来到OC的

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

由于responseId有值, 而从_responseCallbacks取出来的responseCallback正是OC之前传入的block, 所以下面的代码

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

执行responseCallback, 回调OC之前传递的block, 至此, OC调用JS, 并把JS端数据取回完美结束.

总结

在实际中, 我们应该很少会用到JS提供接口给OC调用, 通常是OC提供稳定通用接口给JS调用, 所以本文不是我们实践的重点, 但是作为讲解框架的完整性, 我们应该把OC调用JS和JS调用OC都进行详细的分析, 这样能更好的理解作者设计的意图和架构的巧妙之处.

遗留问题

1 难道每个Web页面都要加入setupWebViewJavascriptBridge这段代码, 这应该是所有开发者都不能接受的.
2 对于WKWebView怎么处理? 也能拦截吗?
3 如何设计一个通用的WebView或者WebViewController?

下面是本文用到的代码的github, 不是小编原著.
WebViewJSBridgeDemo

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

推荐阅读更多精彩内容