内容简介:在页面加载前后如果连续多次调用原生的方法,会遇到回调参数未被调用的情况。在页面加载时通过jsBridge和原生进行10次以上的数据交换。在多篇文章(
- 使用机型为 Android 9,API 28
- 使用的 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,一个完整的通讯过程,其调用时序图如下:
回顾我们最初遇到的问题, 多次调用 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,都没人再调用了...
解决方法
原因已经明了,当前的问题是如何解决。切入点有以下几个,
- 查清为什么多次 iframe.src 变化只调用更少次数的 shouldOverrideUrlLoading,并解决...
- 修改 _fetchQueue 函数
- js 在调用时只能线性调用
鉴于1的实施难度对我这个切图仔来说有点大,优先考虑后续两个解决方法。
修改 _fetchQueue 函数
- 线性调用 _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 数量,但是存在队首阻塞的问题,甚至因为没保证所以没采用。
-
既然是因为 _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 上….当然我的方案不是最好的,如果你有更好的方案,欢迎留言。
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- 皮肤开发过程中遇到的3个问题
- thinkphp-queue在使用过程中 遇到的一个教训
- Java中在时间戳计算的过程中遇到的数据溢出问题
- hadoop开启Service Level Authorization 服务级认证-SIMPLE认证-过程中遇到的坑
- 遇到的加密算法
- 静态库遇到静态库
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
推荐系统与深度学习
黄昕、赵伟、王本友、吕慧伟、杨敏 / 清华大学出版社 / 2019-1-1 / 65.00元
本书的内容设置由浅入深,从传统的推荐算法过渡到近年兴起的深度学习技术。不管是初学者,还是有一定经验的从业人员,相信都能从本书的不同章节中有所收获。 区别于其他推荐算法书籍,本书引入了已被实践证明效果较好的深度学习推荐技术,包括Word2Vec、Wide & Deep、DeepFM、GAN 等技术应用,并给出了相关的实践代码;除了在算法层面讲解推荐系统的实现,还从工程层面详细阐述推荐系统如何搭建.一起来看看 《推荐系统与深度学习》 这本书的介绍吧!