iframe 与 webview ,记录一次使用 jsBridge 遇到的 bug 解决过程

栏目: Html · 发布时间: 5年前

内容简介:在页面加载前后如果连续多次调用原生的方法,会遇到回调参数未被调用的情况。在页面加载时通过jsBridge和原生进行10次以上的数据交换。在多篇文章(
  1. 使用机型为 Android 9,API 28
  2. 使用的 jsBridge 为 link

bug 描述

在页面加载前后如果连续多次调用原生的方法,会遇到回调参数未被调用的情况。

// 多次调用如下函数, 部分 callback 将不会被调用
window.WebViewJavascriptBridge.callHandler(api, parameter, callback);

复制代码

bug 的稳定复现方式

在页面加载时通过jsBridge和原生进行10次以上的数据交换。

出现的原因

查询所得

在多篇文章( 1 ,2)中看到是因为 jsBridge 使用 iframe 的 src 变化 和 shouldOverrideUrlLoading 来实现原生与js的沟通导致的问题, 而刷新 iframe 并不能保证 shouldOverrideUrlLoading 会被调用

于是我们以此为假设进行验证

  • 验证1: jsBridge 是否使用 iframe.src 的变化来进行js与原生的通讯

    我们可以直接看看进行一次完整的通讯的调用过程。

//依据调用链 
 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,完成一次通讯

验证1证实完毕。

  • 验证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,一个完整的通讯过程,其调用时序图如下:

iframe 与 webview ,记录一次使用 jsBridge 遇到的 bug 解决过程

回顾我们最初遇到的问题, 多次调用 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),且将以后一次调用时的参数为准。

iframe 与 webview ,记录一次使用 jsBridge 遇到的 bug 解决过程
iframe 与 webview ,记录一次使用 jsBridge 遇到的 bug 解决过程

上述图示是一次失败通讯的日志,可以看到,前6次调用为 _doSend 的调用,即改变了 6次 iframe 的 src,但实际上只有两次生效了,第一次生效的通讯调用了 _fetchQueue ,传递前 6 次的 message 给 native,但是由于清空了 message 队列,紧跟的第二次 _fetchQueue 执行时传递空数组给 native ,又因为两次 _fetchQueue 的调用间隔太短,实际上只有第二次 _fetchQueue 的调用传递给了 native ,此时 native 只收到一个 空数组的 通讯,自然没有了后续的操作。

所以我们最初 callHandler 里的 callback,都没人再调用了...

解决方法

原因已经明了,当前的问题是如何解决。切入点有以下几个,

  1. 查清为什么多次 iframe.src 变化只调用更少次数的 shouldOverrideUrlLoading,并解决...
  2. 修改 _fetchQueue 函数
  3. js 在调用时只能线性调用

鉴于1的实施难度对我这个切图仔来说有点大,优先考虑后续两个解决方法。

修改 _fetchQueue 函数

  1. 线性调用 _fetchQueue ,主要代码如下。
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 数量,但是存在队首阻塞的问题,甚至因为没保证所以没采用。

  1. 既然是因为 _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 调用时的函数

这个其实有点难处理,因为是在 js 层面,这里解决的点仍然是 2. 中的 _fetchQueue 调用频繁的问题,从这个角度切入有点隔山打牛的意味。但是因为改动只在页面,不依赖原生发包,所以在某些场景也适用。

这里的思想类似,封装 callHandler 函数,节流或者串行均可,当然串行就会有阻塞的可能,节流,这里的节流是想让 _fetchQueue 的调用节流,但是 _fetchQueue 的触发毕竟是异步,而且掌控在原生代码那边,所有其实不太推荐适用这个方案。

随便说说

纵观整个通讯过程,其实就是一个网络协议的缩影。最开始考虑部分通讯失败的问题时,想的这是不是就是网络里的丢包,想想 TCP 怎么解决丢包的,好像是记录字节序 + 定时器,但是这里响应体只包含通讯内容,光是标记请求就有点麻烦了,再加上定时器...如果要改就是大重构了…算了;后来开始针对 _fetchQueue ,要不就考虑学 HTTP 一来一回吧,但是这样效率太低了,js 单线程也没有并发,而且还有队首阻塞的问题… 后来转而一想,既然 fetchQueue 间隔短,那我控制间隔不就好了吗…于是引入了节流的方案… 变动小代码简单易懂…虽然这个 20ms 不太具有事实依据性。

总的来说解决问题并不难,难得是找到问题的核心,为了这个我甚至找了原生开发小哥 copy 一份源码…,好在之前有过 RN 调试经验… 不至于卡在配置 android studio 上….当然我的方案不是最好的,如果你有更好的方案,欢迎留言。


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

推荐系统与深度学习

推荐系统与深度学习

黄昕、赵伟、王本友、吕慧伟、杨敏 / 清华大学出版社 / 2019-1-1 / 65.00元

本书的内容设置由浅入深,从传统的推荐算法过渡到近年兴起的深度学习技术。不管是初学者,还是有一定经验的从业人员,相信都能从本书的不同章节中有所收获。 区别于其他推荐算法书籍,本书引入了已被实践证明效果较好的深度学习推荐技术,包括Word2Vec、Wide & Deep、DeepFM、GAN 等技术应用,并给出了相关的实践代码;除了在算法层面讲解推荐系统的实现,还从工程层面详细阐述推荐系统如何搭建.一起来看看 《推荐系统与深度学习》 这本书的介绍吧!

HTML 压缩/解压工具
HTML 压缩/解压工具

在线压缩/解压 HTML 代码

JS 压缩/解压工具
JS 压缩/解压工具

在线压缩/解压 JS 代码

Base64 编码/解码
Base64 编码/解码

Base64 编码/解码