You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
// YY_OVERRIDE_SCHEMA 类型通讯所调用的原生方法function_fetchQueue(){varmessageQueueString=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 javamessagingIframe.src=CUSTOM_PROTOCOL_SCHEME+"://return/_fetchQueue/"+encodeURIComponent(messageQueueString);}
function_fetchQueue(){if(sendMessageQueue.length===0||fetchingQueueLength>0){return;}// 记录当前等待 native 响应的个数fetchingQueueLength+=sendMessageQueue.length;varmessageQueueString=JSON.stringify(sendMessageQueue);sendMessageQueue=[];//android can't read directly the return data, so we can reload iframe src to communicate with javabizMessagingIframe.src=CUSTOM_PROTOCOL_SCHEME+'://return/_fetchQueue/'+encodeURIComponent(messageQueueString);}/* ... */function_dispatchMessageFromNative(messageJSON){setTimeout(function(){varmessage=JSON.parse(messageJSON);fetchingQueueLength--;// 如果通讯完毕,清理被阻塞的 messageif(fetchingQueueLength===0){// 使用 sto,在当前的通讯结束后再 _fetchQueue setTimeout(function(){_fetchQueue();});}
...
varlastCallTime=0;varstoId=null;varFETCH_QUEUE=20;function_fetchQueue(){// 空数组直接返回 if(sendMessageQueue.length===0){return;}if(newDate().getTime()-lastCallTime<FETCH_QUEUE){if(!stoId){stoId=setTimeout(_fetchQueue,FETCH_QUEUE);}return;}lastCallTime=newDate().getTime();stoId=null;varmessageQueueString=JSON.stringify(sendMessageQueue);sendMessageQueue=[];//android can't read directly the return data, so we can reload iframe src to communicate with javabizMessagingIframe.src=CUSTOM_PROTOCOL_SCHEME+'://return/_fetchQueue/'+encodeURIComponent(messageQueueString);}
前提-出现场景
bug 描述
在页面加载前后如果连续多次调用原生的方法,会遇到回调参数未被调用的情况。
bug 的稳定复现方式
在页面加载时通过jsBridge和原生进行10次以上的数据交换。
出现的原因
查询所得
在多篇文章(1,2)中看到是因为 jsBridge 使用 iframe 的 src 变化 和 shouldOverrideUrlLoading 来实现原生与js的沟通导致的问题,而刷新 iframe 并不能保证 shouldOverrideUrlLoading 会被调用。
于是我们以此为假设进行验证
验证1: jsBridge 是否使用 iframe.src 的变化来进行js与原生的通讯
我们可以直接看看进行一次完整的通讯的调用过程。
从源码可以看出,一个完整的通讯过程,将改变两次 src,也就是说 shouldOverrideUrlLoading 会被调用两次(预计)。@q说来 jsBridge 设计也奇怪,为什么不设计成一次 src,完成一次通讯。
验证1证实完毕。
验证2:iframe 改变 src 是否与 shouldOverrideUrlLoading 调用次数一致。
我在 WebViewJavascriptBridge.js 中对 ifram.src 的变化 和 BasewebviewFragment.java 的 shouldOverrideUrlLoading 调用进行计数,发现两边的次数确实不一致。
验证2 证实完毕。
同时我们也得知,就算二者调用次数不一致,也不影响 js 与 native 的通讯,几次通讯成功的情况二者的次数都不一致,甚至我们可以初步预测,二者的次数根本不需要一致就能实现通讯。
@q 那么通讯成功的充分必要条件是什么呢?
通讯失败的原因
回顾我们之前所做的验证1,一个完整的通讯过程,其调用时序图如下:
回顾我们最初遇到的问题,多次调用 callHandler 后,部分 callback 没有被调用,导致通讯失败。
根据流程图逆行推理, callback 未被调用 => 表示携带该callback 的 respMessage 未被传递过来,也就是说 yy://return/ ${resp} 缺失了 => _fetchQueue 传递的数据有缺失
从 _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的实施难度对我这个切图仔来说有点大,优先考虑后续两个解决方法。
修改 _fetchQueue 函数
以私有变量 fetchingQueueLength 记录等待响应的 message 数量,但是存在队首阻塞的问题,甚至因为没保证所以没采用。
既然是因为 _fetchQueue 调用间隔太短,所以就采用了切图仔常用的节流方案。
这个 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 上….当然我的方案不是最好的,如果你有更好的方案,欢迎留言。
The text was updated successfully, but these errors were encountered: