摘要:通訊狀態(tài)的改變次數(shù)被調(diào)用次數(shù)預計驗證證實完畢。既然是因為調(diào)用間隔太短,所以就采用了切圖仔常用的節(jié)流方案。隨便說說縱觀整個通訊過程,其實就是一個網(wǎng)絡協(xié)議的縮影。
前提-出現(xiàn)場景使用機型為 Android 9,API 28
使用的 jsBridge 為 link
在頁面加載前后如果連續(xù)多次調(diào)用原生的方法,會遇到回調(diào)參數(shù)未被調(diào)用的情況。
// 多次調(diào)用如下函數(shù), 部分 callback 將不會被調(diào)用
window.WebViewJavascriptBridge.callHandler(api, parameter, callback);
bug 的穩(wěn)定復現(xiàn)方式
在頁面加載時通過jsBridge和原生進行10次以上的數(shù)據(jù)交換。
出現(xiàn)的原因 查詢所得在多篇文章(1,2)中看到是因為 jsBridge 使用 iframe 的 src 變化 和 shouldOverrideUrlLoading 來實現(xiàn)原生與js的溝通導致的問題,而刷新 iframe 并不能保證 shouldOverrideUrlLoading 會被調(diào)用。
于是我們以此為假設進行驗證
驗證1: jsBridge 是否使用 iframe.src 的變化來進行js與原生的通訊
我們可以直接看看進行一次完整的通訊的調(diào)用過程。
//依據(jù)調(diào)用鏈
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內(nèi)的iframe的src
messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + "://" + QUEUE_HAS_MESSAGE;
}
// 此時步驟轉到原生層面
// shouldOverrideUrlLoading 將在 iframe.src 改變時被調(diào)用
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();
}
// 根據(jù) url 的內(nèi)容,區(qū)分是哪種類型的操作
// 事實上 只有 YY_RETURN_DATA 和 YY_OVERRIDE_SCHEMA 兩種
// YY_RETURN_DATA 根據(jù) url 的 參數(shù),返回數(shù)據(jù),即原生備好數(shù)據(jù)后調(diào)用 js 原生方法(js 的回調(diào)函數(shù))
// YY_OVERRIDE_SCHEMA 則注入腳本到 webview 調(diào)用 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 類型通訊所調(diào)用的原生方法
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 會被調(diào)用兩次(預計)。@Q說來 jsBridge 設計也奇怪,為什么不設計成一次 src,完成一次通訊。
驗證1證實完畢。
驗證2:iframe 改變 src 是否與 shouldOverrideUrlLoading 調(diào)用次數(shù)一致。
我在 WebViewJavascriptBridge.js 中對 ifram.src 的變化 和 BasewebviewFragment.java 的 shouldOverrideUrlLoading 調(diào)用進行計數(shù),發(fā)現(xiàn)兩邊的次數(shù)確實不一致。
通訊狀態(tài) | iframe 的 src 改變次數(shù) | shouldOverrideUrlLoading 被調(diào)用次數(shù) |
---|---|---|
預計 | 18 | 18 |
T | 13 | 9 |
T | 17 | 14 |
T | 13 | 6 |
F | 17 | 18 |
F | 6 | 3 |
T | 11 | 8 |
驗證2 證實完畢。
同時我們也得知,就算二者調(diào)用次數(shù)不一致,也不影響 js 與 native 的通訊,幾次通訊成功的情況二者的次數(shù)都不一致,甚至我們可以初步預測,二者的次數(shù)根本不需要一致就能實現(xiàn)通訊。
@Q 那么通訊成功的充分必要條件是什么呢?
通訊失敗的原因回顧我們之前所做的驗證1,一個完整的通訊過程,其調(diào)用時序圖如下:
回顧我們最初遇到的問題,多次調(diào)用 callHandler 后,部分 callback 沒有被調(diào)用,導致通訊失敗。
根據(jù)流程圖逆行推理, callback 未被調(diào)用 => 表示攜帶該callback 的 respMessage 未被傳遞過來,也就是說 yy://return/ ${resp} 缺失了 => _fetchQueue 傳遞的數(shù)據(jù)有缺失
function _fetchQueue() {
var messageQueueString = JSON.stringify(sendMessageQueue);
// ATENTION 這里在將 string 化后立即清空了當前的 messageQueue
sendMessageQueue = [];
messagingIframe.src =
CUSTOM_PROTOCOL_SCHEME +
"://return/_fetchQueue/" +
encodeURIComponent(messageQueueString);
}
從 _fetchQueue 的源碼中,發(fā)現(xiàn)在將 message 傳遞后就立馬清空了,實際上這并不準確,因為連續(xù)N次改變 iframe 的 src ,shouldOverrideUrlLoading 的實際調(diào)用次數(shù)為 M(M
上述圖示是一次失敗通訊的日志,可以看到,前6次調(diào)用為 _doSend 的調(diào)用,即改變了 6次 iframe 的 src,但實際上只有兩次生效了,第一次生效的通訊調(diào)用了 _fetchQueue ,傳遞前 6 次的 message 給 native,但是由于清空了 message 隊列,緊跟的第二次 _fetchQueue 執(zhí)行時傳遞空數(shù)組給 native ,又因為兩次 _fetchQueue 的調(diào)用間隔太短,實際上只有第二次 _fetchQueue 的調(diào)用傳遞給了 native ,此時 native 只收到一個 空數(shù)組的 通訊,自然沒有了后續(xù)的操作。 所以我們最初 callHandler 里的 callback,都沒人再調(diào)用了... 原因已經(jīng)明了,當前的問題是如何解決。切入點有以下幾個, 查清為什么多次 iframe.src 變化只調(diào)用更少次數(shù)的 shouldOverrideUrlLoading,并解決... 修改 _fetchQueue 函數(shù) js 在調(diào)用時只能線性調(diào)用 鑒于1的實施難度對我這個切圖仔來說有點大,優(yōu)先考慮后續(xù)兩個解決方法。 線性調(diào)用 _fetchQueue ,主要代碼如下。 以私有變量 fetchingQueueLength 記錄等待響應的 message 數(shù)量,但是存在隊首阻塞的問題,甚至因為沒保證所以沒采用。
既然是因為 _fetchQueue 調(diào)用間隔太短,所以就采用了切圖仔常用的節(jié)流方案。 這個 20 ms,其實我是有些隨意的定義的,從 200 開始向下試驗,20 是我覺得比較穩(wěn)定一個數(shù)字… 。20 ms 內(nèi)連續(xù)的調(diào)用 _fetchQueue 將只有一次生效,回顧之前通訊流程的同學應該知道 _fetchQueue 的觸發(fā)是依靠 native 的調(diào)用的,所以 _fetchQueue 的觸發(fā)對 _doSend 來說是異步的,所以并不需要一一對應,_doSend 只是往 sendMessageQueue 里添加任務,而 _fetchQueue 只負責將 sendMessageQueue 里的任務清空,只要保證至少有一個 _fetchQueue 晚于 _doSend 執(zhí)行即可。 但是這里改動 WebViewJavascriptBridge.js 是需要重新發(fā)包的。
修改 _fetchQueue 函數(shù)
function _fetchQueue() {
if (sendMessageQueue.length === 0 || fetchingQueueLength > 0) {
return;
}
// 記錄當前等待 native 響應的個數(shù)
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();
});
}
...
var lastCallTime = 0;
var stoId = null;
var FETCH_QUEUE = 20;
function _fetchQueue() {
// 空數(shù)組直接返回
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);
}
這個其實有點難處理,因為是在 js 層面,這里解決的點仍然是 2. 中的 _fetchQueue 調(diào)用頻繁的問題,從這個角度切入有點隔山打牛的意味。但是因為改動只在頁面,不依賴原生發(fā)包,所以在某些場景也適用。
這里的思想類似,封裝 callHandler 函數(shù),節(jié)流或者串行均可,當然串行就會有阻塞的可能,節(jié)流,這里的節(jié)流是想讓 _fetchQueue 的調(diào)用節(jié)流,但是 _fetchQueue 的觸發(fā)畢竟是異步,而且掌控在原生代碼那邊,所有其實不太推薦適用這個方案。
隨便說說縱觀整個通訊過程,其實就是一個網(wǎng)絡協(xié)議的縮影。最開始考慮部分通訊失敗的問題時,想的這是不是就是網(wǎng)絡里的丟包,想想 TCP 怎么解決丟包的,好像是記錄字節(jié)序 + 定時器,但是這里響應體只包含通訊內(nèi)容,光是標記請求就有點麻煩了,再加上定時器...如果要改就是大重構了…算了;后來開始針對 _fetchQueue ,要不就考慮學 HTTP 一來一回吧,但是這樣效率太低了,js 單線程也沒有并發(fā),而且還有隊首阻塞的問題… 后來轉而一想,既然 fetchQueue 間隔短,那我控制間隔不就好了嗎…于是引入了節(jié)流的方案… 變動小代碼簡單易懂…雖然這個 20ms 不太具有事實依據(jù)性。
總的來說解決問題并不難,難得是找到問題的核心,為了這個我甚至找了原生開發(fā)小哥 copy 一份源碼…,好在之前有過 RN 調(diào)試經(jīng)驗… 不至于卡在配置 android studio 上….當然我的方案不是最好的,如果你有更好的方案,歡迎留言。
文章版權歸作者所有,未經(jīng)允許請勿轉載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/6895.html
摘要:一原理篇下面分別介紹和與的底層交互原理在講解原理之前,首先來了解下的組件,先來看一下蘋果官方的介紹上面的意思是說是一個可加載網(wǎng)頁的對象,它有瀏覽記錄功能,且對加載的網(wǎng)頁內(nèi)容是可編程的。 做過混合開發(fā)的很多人都知道Ionic和PhoneGap之類的框架,這些框架在web基礎上包了一層Native,然后通過Bridge技術使得js可以調(diào)用視頻、位置、音頻等功能。本文就是介紹這層Bridge...
摘要:作者心葉時間原理概述簡介是代碼與代碼的通信橋梁。目前的一種統(tǒng)一方案是觸發(fā)捕獲原生分析執(zhí)行原生調(diào)用。另外調(diào)用時處理完畢后一定要及時通知進行回調(diào)要不然這個回調(diào)函數(shù)不會自動銷毀多了后會引發(fā)內(nèi)存泄漏。 作者:心葉時間:2019-03-25 10:18 原理概述 簡介 JSBridge是Native代碼與JS代碼的通信橋梁。目前的一種統(tǒng)一方案是:H5觸發(fā)url scheme->Native捕獲u...
閱讀 2400·2021-09-08 09:45
閱讀 3340·2021-09-08 09:45
閱讀 3097·2019-08-30 15:54
閱讀 3348·2019-08-26 13:54
閱讀 1405·2019-08-26 13:26
閱讀 1384·2019-08-26 13:23
閱讀 909·2019-08-23 17:57
閱讀 2178·2019-08-23 17:14