成人国产在线小视频_日韩寡妇人妻调教在线播放_色成人www永久在线观看_2018国产精品久久_亚洲欧美高清在线30p_亚洲少妇综合一区_黄色在线播放国产_亚洲另类技巧小说校园_国产主播xx日韩_a级毛片在线免费

資訊專欄INFORMATION COLUMN

iframe 與 webview ,記錄一次使用 jsBridge 遇到的 bug 解決過(guò)程

Jensen / 1323人閱讀

摘要:通訊狀態(tài)的改變次數(shù)被調(diào)用次數(shù)預(yù)計(jì)驗(yàn)證證實(shí)完畢。既然是因?yàn)檎{(diào)用間隔太短,所以就采用了切圖仔常用的節(jié)流方案。隨便說(shuō)說(shuō)縱觀整個(gè)通訊過(guò)程,其實(shí)就是一個(gè)網(wǎng)絡(luò)協(xié)議的縮影。

前提-出現(xiàn)場(chǎng)景

    使用機(jī)型為 Android 9,API 28

    使用的 jsBridge 為 link

bug 描述

在頁(yè)面加載前后如果連續(xù)多次調(diào)用原生的方法,會(huì)遇到回調(diào)參數(shù)未被調(diào)用的情況。

// 多次調(diào)用如下函數(shù), 部分 callback 將不會(huì)被調(diào)用
window.WebViewJavascriptBridge.callHandler(api, parameter, callback);

bug 的穩(wěn)定復(fù)現(xiàn)方式

在頁(yè)面加載時(shí)通過(guò)jsBridge和原生進(jìn)行10次以上的數(shù)據(jù)交換。

出現(xiàn)的原因 查詢所得

在多篇文章(1,2)中看到是因?yàn)?jsBridge 使用 iframe 的 src 變化 和 shouldOverrideUrlLoading 來(lái)實(shí)現(xiàn)原生與js的溝通導(dǎo)致的問(wèn)題,而刷新 iframe 并不能保證 shouldOverrideUrlLoading 會(huì)被調(diào)用。

于是我們以此為假設(shè)進(jìn)行驗(yàn)證

驗(yàn)證1: jsBridge 是否使用 iframe.src 的變化來(lái)進(jìn)行js與原生的通訊

我們可以直接看看進(jìn)行一次完整的通訊的調(diào)用過(guò)程。

 //依據(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;
 }
 
  // 此時(shí)步驟轉(zhuǎn)到原生層面
// shouldOverrideUrlLoading 將在 iframe.src 改變時(shí)被調(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ū)分是哪種類型的操作
  	// 事實(shí)上 只有 YY_RETURN_DATA 和 YY_OVERRIDE_SCHEMA 兩種
  	// YY_RETURN_DATA 根據(jù) url 的 參數(shù),返回?cái)?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);
    }
}

//通訊結(jié)束 

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

從源碼可以看出,一個(gè)完整的通訊過(guò)程,將改變兩次 src,也就是說(shuō) shouldOverrideUrlLoading 會(huì)被調(diào)用兩次(預(yù)計(jì))。@Q說(shuō)來(lái) jsBridge 設(shè)計(jì)也奇怪,為什么不設(shè)計(jì)成一次 src,完成一次通訊。

驗(yàn)證1證實(shí)完畢。

驗(yàn)證2:iframe 改變 src 是否與 shouldOverrideUrlLoading 調(diào)用次數(shù)一致。

我在 WebViewJavascriptBridge.js 中對(duì) ifram.src 的變化 和 BasewebviewFragment.java 的 shouldOverrideUrlLoading 調(diào)用進(jìn)行計(jì)數(shù),發(fā)現(xiàn)兩邊的次數(shù)確實(shí)不一致。

通訊狀態(tài) iframe 的 src 改變次數(shù) shouldOverrideUrlLoading 被調(diào)用次數(shù)
預(yù)計(jì) 18 18
T 13 9
T 17 14
T 13 6
F 17 18
F 6 3
T 11 8

驗(yàn)證2 證實(shí)完畢。

同時(shí)我們也得知,就算二者調(diào)用次數(shù)不一致,也不影響 js 與 native 的通訊,幾次通訊成功的情況二者的次數(shù)都不一致,甚至我們可以初步預(yù)測(cè),二者的次數(shù)根本不需要一致就能實(shí)現(xiàn)通訊。

@Q 那么通訊成功的充分必要條件是什么呢?

通訊失敗的原因

回顧我們之前所做的驗(yàn)證1,一個(gè)完整的通訊過(guò)程,其調(diào)用時(shí)序圖如下:

回顧我們最初遇到的問(wèn)題,多次調(diào)用 callHandler 后,部分 callback 沒(méi)有被調(diào)用,導(dǎo)致通訊失敗。

根據(jù)流程圖逆行推理, callback 未被調(diào)用 => 表示攜帶該callback 的 respMessage 未被傳遞過(guò)來(lái),也就是說(shuō) yy://return/ ${resp} 缺失了 => _fetchQueue 傳遞的數(shù)據(jù)有缺失

function _fetchQueue() {
  var messageQueueString = JSON.stringify(sendMessageQueue);  
  
  // ATENTION 這里在將 string 化后立即清空了當(dāng)前的 messageQueue 
  sendMessageQueue = [];
  
  messagingIframe.src =
    CUSTOM_PROTOCOL_SCHEME +
    "://return/_fetchQueue/" +
    encodeURIComponent(messageQueueString);
}

從 _fetchQueue 的源碼中,發(fā)現(xiàn)在將 message 傳遞后就立馬清空了,實(shí)際上這并不準(zhǔn)確,因?yàn)檫B續(xù)N次改變 iframe 的 src ,shouldOverrideUrlLoading 的實(shí)際調(diào)用次數(shù)為 M(M

上述圖示是一次失敗通訊的日志,可以看到,前6次調(diào)用為 _doSend 的調(diào)用,即改變了 6次 iframe 的 src,但實(shí)際上只有兩次生效了,第一次生效的通訊調(diào)用了 _fetchQueue ,傳遞前 6 次的 message 給 native,但是由于清空了 message 隊(duì)列,緊跟的第二次 _fetchQueue 執(zhí)行時(shí)傳遞空數(shù)組給 native ,又因?yàn)閮纱?_fetchQueue 的調(diào)用間隔太短,實(shí)際上只有第二次 _fetchQueue 的調(diào)用傳遞給了 native ,此時(shí) native 只收到一個(gè) 空數(shù)組的 通訊,自然沒(méi)有了后續(xù)的操作。

所以我們最初 callHandler 里的 callback,都沒(méi)人再調(diào)用了...

解決方法

原因已經(jīng)明了,當(dāng)前的問(wèn)題是如何解決。切入點(diǎn)有以下幾個(gè),

    查清為什么多次 iframe.src 變化只調(diào)用更少次數(shù)的 shouldOverrideUrlLoading,并解決...

    修改 _fetchQueue 函數(shù)

    js 在調(diào)用時(shí)只能線性調(diào)用

鑒于1的實(shí)施難度對(duì)我這個(gè)切圖仔來(lái)說(shuō)有點(diǎn)大,優(yōu)先考慮后續(xù)兩個(gè)解決方法。

修改 _fetchQueue 函數(shù)

    線性調(diào)用 _fetchQueue ,主要代碼如下。

function _fetchQueue() {
    if (sendMessageQueue.length === 0 || fetchingQueueLength > 0) {
        return;
    }

    // 記錄當(dāng)前等待 native 響應(yīng)的個(gè)數(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,在當(dāng)前的通訊結(jié)束后再 _fetchQueue 
            setTimeout(function() {
                _fetchQueue();
            });
        }
      
      ...

以私有變量 fetchingQueueLength 記錄等待響應(yīng)的 message 數(shù)量,但是存在隊(duì)首阻塞的問(wèn)題,甚至因?yàn)闆](méi)保證所以沒(méi)采用。

    既然是因?yàn)?_fetchQueue 調(diào)用間隔太短,所以就采用了切圖仔常用的節(jié)流方案。

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

    這個(gè) 20 ms,其實(shí)我是有些隨意的定義的,從 200 開始向下試驗(yàn),20 是我覺得比較穩(wěn)定一個(gè)數(shù)字… 。20 ms 內(nèi)連續(xù)的調(diào)用 _fetchQueue 將只有一次生效,回顧之前通訊流程的同學(xué)應(yīng)該知道 _fetchQueue 的觸發(fā)是依靠 native 的調(diào)用的,所以 _fetchQueue 的觸發(fā)對(duì) _doSend 來(lái)說(shuō)是異步的,所以并不需要一一對(duì)應(yīng),_doSend 只是往 sendMessageQueue 里添加任務(wù),而 _fetchQueue 只負(fù)責(zé)將 sendMessageQueue 里的任務(wù)清空,只要保證至少有一個(gè) _fetchQueue 晚于 _doSend 執(zhí)行即可。

    但是這里改動(dòng) WebViewJavascriptBridge.js 是需要重新發(fā)包的。

修改 js 調(diào)用時(shí)的函數(shù)

這個(gè)其實(shí)有點(diǎn)難處理,因?yàn)槭窃?js 層面,這里解決的點(diǎn)仍然是 2. 中的 _fetchQueue 調(diào)用頻繁的問(wèn)題,從這個(gè)角度切入有點(diǎn)隔山打牛的意味。但是因?yàn)楦膭?dòng)只在頁(yè)面,不依賴原生發(fā)包,所以在某些場(chǎng)景也適用。

這里的思想類似,封裝 callHandler 函數(shù),節(jié)流或者串行均可,當(dāng)然串行就會(huì)有阻塞的可能,節(jié)流,這里的節(jié)流是想讓 _fetchQueue 的調(diào)用節(jié)流,但是 _fetchQueue 的觸發(fā)畢竟是異步,而且掌控在原生代碼那邊,所有其實(shí)不太推薦適用這個(gè)方案。

隨便說(shuō)說(shuō)

縱觀整個(gè)通訊過(guò)程,其實(shí)就是一個(gè)網(wǎng)絡(luò)協(xié)議的縮影。最開始考慮部分通訊失敗的問(wèn)題時(shí),想的這是不是就是網(wǎng)絡(luò)里的丟包,想想 TCP 怎么解決丟包的,好像是記錄字節(jié)序 + 定時(shí)器,但是這里響應(yīng)體只包含通訊內(nèi)容,光是標(biāo)記請(qǐng)求就有點(diǎn)麻煩了,再加上定時(shí)器...如果要改就是大重構(gòu)了…算了;后來(lái)開始針對(duì) _fetchQueue ,要不就考慮學(xué) HTTP 一來(lái)一回吧,但是這樣效率太低了,js 單線程也沒(méi)有并發(fā),而且還有隊(duì)首阻塞的問(wèn)題… 后來(lái)轉(zhuǎn)而一想,既然 fetchQueue 間隔短,那我控制間隔不就好了嗎…于是引入了節(jié)流的方案… 變動(dòng)小代碼簡(jiǎn)單易懂…雖然這個(gè) 20ms 不太具有事實(shí)依據(jù)性。

總的來(lái)說(shuō)解決問(wèn)題并不難,難得是找到問(wèn)題的核心,為了這個(gè)我甚至找了原生開發(fā)小哥 copy 一份源碼…,好在之前有過(guò) RN 調(diào)試經(jīng)驗(yàn)… 不至于卡在配置 android studio 上….當(dāng)然我的方案不是最好的,如果你有更好的方案,歡迎留言。

文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。

轉(zhuǎn)載請(qǐng)注明本文地址:http://systransis.cn/yun/6895.html

相關(guān)文章

  • H5Native交互之JSBridge技術(shù)

    摘要:一原理篇下面分別介紹和與的底層交互原理在講解原理之前,首先來(lái)了解下的組件,先來(lái)看一下蘋果官方的介紹上面的意思是說(shuō)是一個(gè)可加載網(wǎng)頁(yè)的對(duì)象,它有瀏覽記錄功能,且對(duì)加載的網(wǎng)頁(yè)內(nèi)容是可編程的。 做過(guò)混合開發(fā)的很多人都知道Ionic和PhoneGap之類的框架,這些框架在web基礎(chǔ)上包了一層Native,然后通過(guò)Bridge技術(shù)使得js可以調(diào)用視頻、位置、音頻等功能。本文就是介紹這層Bridge...

    zacklee 評(píng)論0 收藏0
  • (轉(zhuǎn))iOS- JSBridge原理

    摘要:作者心葉時(shí)間原理概述簡(jiǎn)介是代碼與代碼的通信橋梁。目前的一種統(tǒng)一方案是觸發(fā)捕獲原生分析執(zhí)行原生調(diào)用。另外調(diào)用時(shí)處理完畢后一定要及時(shí)通知進(jìn)行回調(diào)要不然這個(gè)回調(diào)函數(shù)不會(huì)自動(dòng)銷毀多了后會(huì)引發(fā)內(nèi)存泄漏。 作者:心葉時(shí)間:2019-03-25 10:18 原理概述 簡(jiǎn)介 JSBridge是Native代碼與JS代碼的通信橋梁。目前的一種統(tǒng)一方案是:H5觸發(fā)url scheme->Native捕獲u...

    txgcwm 評(píng)論0 收藏0

發(fā)表評(píng)論

0條評(píng)論

最新活動(dòng)
閱讀需要支付1元查看
<