在頁面加載先後若是連續屢次調用原生的方法,會遇到回調參數未被調用的狀況。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 上….固然個人方案不是最好的,若是你有更好的方案,歡迎留言。