在页面加载先后若是连续屡次调用原生的方法,会遇到回调参数未被调用的状况。javascript
// 屡次调用以下函数, 部分 callback 将不会被调用
window.WebViewJavascriptBridge.callHandler(api, parameter, callback);
复制代码
在页面加载时经过jsBridge和原生进行10次以上的数据交换。html
在多篇文章(1,2)中看到是由于 jsBridge 使用 iframe 的 src 变化 和 shouldOverrideUrlLoading 来实现原生与js的沟通致使的问题,而刷新 iframe 并不能保证 shouldOverrideUrlLoading 会被调用。java
因而咱们以此为假设进行验证android
验证1: jsBridge 是否使用 iframe.src 的变化来进行js与原生的通信git
咱们能够直接看看进行一次完整的通信的调用过程。github
//依据调用链
window.WebViewJavascriptBridge.callHandler(api, parameter, callback);
function callHandler(handlerName, data, responseCallback) {
_doSend(
{
handlerName: handlerName,
data: data
},
responseCallback
);
}
function _doSend(message, responseCallback) {
if (responseCallback) {
var callbackId = "cb_" + uniqueId++ + "_" + new Date().getTime();
responseCallbacks[callbackId] = responseCallback;
message.callbackId = callbackId;
}
sendMessageQueue.push(message);
//改变html内的iframe的src
messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + "://" + QUEUE_HAS_MESSAGE;
}
// 此时步骤转到原生层面
复制代码
// shouldOverrideUrlLoading 将在 iframe.src 改变时被调用
public boolean shouldOverrideUrlLoading(WebView view, String urlString) {
super.shouldOverrideUrlLoading(view, urlString);
if (PhoneUtil.INSTANCE.startTelActivity(getActivity(), urlString)) return true;
if (mWebViewHelper.shouldOverrideUrlLoading(view, urlString)) return true;
return false;
}
//父类的 shouldOverrideUrlLoading
public boolean shouldOverrideUrlLoading(WebView view, String url) {
try {
url = URLDecoder.decode(url, "UTF-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
// 根据 url 的内容,区分是哪一种类型的操做
// 事实上 只有 YY_RETURN_DATA 和 YY_OVERRIDE_SCHEMA 两种
// YY_RETURN_DATA 根据 url 的 参数,返回数据,即原生备好数据后调用 js 原生方法(js 的回调函数)
// YY_OVERRIDE_SCHEMA 则注入脚本到 webview 调用 js 原生方法 _fetchQueue
if (url.startsWith(BridgeUtil.YY_RETURN_DATA)) {
webView.handlerReturnData(url);
return true;
} else if (url.startsWith(BridgeUtil.YY_OVERRIDE_SCHEMA)) { //
webView.flushMessageQueue();
return true;
} else {
return super.shouldOverrideUrlLoading(view, url);
}
}
//通信结束
复制代码
// YY_OVERRIDE_SCHEMA 类型通信所调用的原生方法
function _fetchQueue() {
var messageQueueString = JSON.stringify(sendMessageQueue);
console.warn(++count, "-", messageQueueString);
sendMessageQueue = [];
//android can't read directly the return data,
//so we can reload iframe src to communicate with java
messagingIframe.src =
CUSTOM_PROTOCOL_SCHEME +
"://return/_fetchQueue/" +
encodeURIComponent(messageQueueString);
}
复制代码
从源码能够看出,一个完整的通信过程,将改变两次 src,也就是说 shouldOverrideUrlLoading 会被调用两次(预计)。@Q说来 jsBridge 设计也奇怪,为何不设计成一次 src,完成一次通信。web
验证1证明完毕。api
验证2:iframe 改变 src 是否与 shouldOverrideUrlLoading 调用次数一致。数组
我在 WebViewJavascriptBridge.js 中对 ifram.src 的变化 和 BasewebviewFragment.java 的 shouldOverrideUrlLoading 调用进行计数,发现两边的次数确实不一致。网络
通信状态 | iframe 的 src 改变次数 | shouldOverrideUrlLoading 被调用次数 |
---|---|---|
预计 | 18 | 18 |
T | 13 | 9 |
T | 17 | 14 |
T | 13 | 6 |
F | 17 | 18 |
F | 6 | 3 |
T | 11 | 8 |
验证2 证明完毕。
同时咱们也得知,就算两者调用次数不一致,也不影响 js 与 native 的通信,几回通信成功的状况两者的次数都不一致,甚至咱们能够初步预测,两者的次数根本不须要一致就能实现通信。
@Q 那么通信成功的充分必要条件是什么呢?
回顾咱们以前所作的验证1,一个完整的通信过程,其调用时序图以下:
回顾咱们最初遇到的问题,屡次调用 callHandler 后,部分 callback 没有被调用,致使通信失败。
根据流程图逆行推理, callback 未被调用 => 表示携带该callback 的 respMessage 未被传递过来,也就是说 yy://return/ ${resp} 缺失了 => _fetchQueue 传递的数据有缺失
function _fetchQueue() {
var messageQueueString = JSON.stringify(sendMessageQueue);
// ATENTION 这里在将 string 化后当即清空了当前的 messageQueue
sendMessageQueue = [];
messagingIframe.src =
CUSTOM_PROTOCOL_SCHEME +
"://return/_fetchQueue/" +
encodeURIComponent(messageQueueString);
}
复制代码
从 _fetchQueue 的源码中,发如今将 message 传递后就立马清空了,实际上这并不许确,由于连续N次改变 iframe 的 src ,shouldOverrideUrlLoading 的实际调用次数为 M(M<N),且将之后一次调用时的参数为准。
上述图示是一次失败通信的日志,能够看到,前6次调用为 _doSend 的调用,即改变了 6次 iframe 的 src,但实际上只有两次生效了,第一次生效的通信调用了 _fetchQueue ,传递前 6 次的 message 给 native,可是因为清空了 message 队列,紧跟的第二次 _fetchQueue 执行时传递空数组给 native ,又由于两次 _fetchQueue 的调用间隔过短,实际上只有第二次 _fetchQueue 的调用传递给了 native ,此时 native 只收到一个 空数组的 通信,天然没有了后续的操做。
因此咱们最初 callHandler 里的 callback,都没人再调用了...
缘由已经明了,当前的问题是如何解决。切入点有如下几个,
鉴于1的实施难度对我这个切图仔来讲有点大,优先考虑后续两个解决方法。
function _fetchQueue() {
if (sendMessageQueue.length === 0 || fetchingQueueLength > 0) {
return;
}
// 记录当前等待 native 响应的个数
fetchingQueueLength += sendMessageQueue.length;
var messageQueueString = JSON.stringify(sendMessageQueue);
sendMessageQueue = [];
//android can't read directly the return data, so we can reload iframe src to communicate with java
bizMessagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://return/_fetchQueue/' + encodeURIComponent(messageQueueString);
}
/* ... */
function _dispatchMessageFromNative(messageJSON) {
setTimeout(function() {
var message = JSON.parse(messageJSON);
fetchingQueueLength--;
// 若是通信完毕,清理被阻塞的 message
if (fetchingQueueLength === 0) {
// 使用 sto,在当前的通信结束后再 _fetchQueue
setTimeout(function() {
_fetchQueue();
});
}
...
复制代码
以私有变量 fetchingQueueLength 记录等待响应的 message 数量,可是存在队首阻塞的问题,甚至由于没保证因此没采用。
既然是由于 _fetchQueue 调用间隔过短,因此就采用了切图仔经常使用的节流方案。
var lastCallTime = 0;
var stoId = null;
var FETCH_QUEUE = 20;
function _fetchQueue() {
// 空数组直接返回
if (sendMessageQueue.length === 0) {
return;
}
if (new Date().getTime() - lastCallTime < FETCH_QUEUE) {
if (!stoId) {
stoId = setTimeout(_fetchQueue, FETCH_QUEUE);
}
return;
}
lastCallTime = new Date().getTime();
stoId = null;
var messageQueueString = JSON.stringify(sendMessageQueue);
sendMessageQueue = [];
//android can't read directly the return data, so we can reload iframe src to communicate with java
bizMessagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://return/_fetchQueue/' + encodeURIComponent(messageQueueString);
}
复制代码
这个 20 ms,其实我是有些随意的定义的,从 200 开始向下试验,20 是我以为比较稳定一个数字… 。20 ms 内连续的调用 _fetchQueue 将只有一次生效,回顾以前通信流程的同窗应该知道 _fetchQueue 的触发是依靠 native 的调用的,因此 _fetchQueue 的触发对 _doSend 来讲是异步的,因此并不须要一一对应,_doSend 只是往 sendMessageQueue 里添加任务,而 _fetchQueue 只负责将 sendMessageQueue 里的任务清空,只要保证至少有一个 _fetchQueue 晚于 _doSend 执行便可。
可是这里改动 WebViewJavascriptBridge.js 是须要从新发包的。
这个其实有点难处理,由于是在 js 层面,这里解决的点仍然是 2. 中的 _fetchQueue 调用频繁的问题,从这个角度切入有点隔山打牛的意味。可是由于改动只在页面,不依赖原生发包,因此在某些场景也适用。
这里的思想相似,封装 callHandler 函数,节流或者串行都可,固然串行就会有阻塞的可能,节流,这里的节流是想让 _fetchQueue 的调用节流,可是 _fetchQueue 的触发毕竟是异步,并且掌控在原生代码那边,全部其实不太推荐适用这个方案。
纵观整个通信过程,其实就是一个网络协议的缩影。最开始考虑部分通信失败的问题时,想的这是否是就是网络里的丢包,想一想 TCP 怎么解决丢包的,好像是记录字节序 + 定时器,可是这里响应体只包含通信内容,光是标记请求就有点麻烦了,再加上定时器...若是要改就是大重构了…算了;后来开始针对 _fetchQueue ,要不就考虑学 HTTP 一来一回吧,可是这样效率过低了,js 单线程也没有并发,并且还有队首阻塞的问题… 后来转而一想,既然 fetchQueue 间隔短,那我控制间隔不就行了吗…因而引入了节流的方案… 变更小代码简单易懂…虽然这个 20ms 不太具备事实依据性。
总的来讲解决问题并不难,可贵是找到问题的核心,为了这个我甚至找了原生开发小哥 copy 一份源码…,好在以前有过 RN 调试经验… 不至于卡在配置 android studio 上….固然个人方案不是最好的,若是你有更好的方案,欢迎留言。