Fix一个随机出现的键盘弹出的issue后的思考(ReactNative)
最近花了近一周fix了一个移动端的bug,是个很有趣的bug,大概是这样的。这是一个比较长的故事,有兴趣的可以一直看。
是一个什么样的bug
bug的表现是在一款tablet端应用使用很久之后,第一,在输入框内输入一些内容后,点击done/search,第二,然后点击页面的一些空白区域,软键盘弹出,并且光标focus在最近输入过的输入框内。
此时应用对用户行为的响应会让用户很疑惑和费解。
总结,它有如下几个特点
- 应用最开始是正常的,不是每次都能重现
- 一旦出现这个bug,在每一个存在输入框的页面都存在这个问题
- 重现的场景不明,目前已知是应用使用的越久越容易出现,应用一旦后台关闭病重新启动,又会消失。
如何去修复这个bug
第一步 试图稳定重现
我们先是试图去找一个最小的用户journey去复现这个bug,当时运气比较好,花了大概半天时间找到了一条最小的重现路径。
不说业务背景,简单介绍下应用的页面逻辑。
我们的应用在登录之后有一个home页面,home页面存在三个tab可以滑动或者点击切换,
在tab页面之上还存在一些功能菜单,其中某个功能菜单menuA可以点击跳到另一个新的带有一个输入框的页面。
页面大概如下,不是专业ux很丑勿见怪。
我们发现的一条可以快速重现的路径是
- 登录到达home页面后,反复切换三个tab多次(20次以上)
- 点击menuA到达一个带输入框的页面
- 在输入框输入数据,并点击软键盘的done
- 点击页面空白区域
- 然后软键盘就出来了。
第二步 试图从代码部分找到为什么最小场景会出现问题
找到一个最小重现路径之后,我们可以从代码里面找找为什么会出现这个问题。
因为这个bug在应用重启后没有,我们怀疑的方向就定位在render的问题,大概率是出在组件上。
我们中间有几个猜测
- 自己封装的input组件有问题
- 三个tabs的滑动组件有问题,滑动组件内的scroll view影响了RN的手势响应系统
最后发现貌似都不是,这个时候和组内另外一个同事pair,她发现在请求比较多的时候容易有问题,中间还怀疑过网络请求处理导致的。这个怀疑其实不大对,但是确实为我们找到了一条路。
因为我们最后发现
我们所有的网络请求都在请求结果返回之前,在页面出现一层蒙版mask以及loading提示符号(在RN里面是ActivityIndicator),这个部分是会影响页面render的。
而把这部分去掉(在请求到达之前不出现蒙层),这个bug就没有了,这个发现当时还是让人很震惊的以及疑惑的,因为似乎找到了一部分原因但我们还是没搞清楚为什么。
第三步 尝试修复(未弄清根本原因的情况下)
有了这个思路的提示,我们试图尝试修复。按照业务需求,我们不能取消ActivityIndicator的使用,因为给用户适当的提示这个确实很有必要,所以我们试图去修改mask的实现。
在老的mask里面
我们使用了一个第三方的RN组件react-native-root-siblings来帮助我们在root同级插入一个兄弟元素显示我们的loading提示符号。
一般在发完请求请求结果未到达之前,我们就插入一个新的同级兄弟元素,请求完成后就删除掉它。
当时怀疑因为这部分反复的修改页面的元素结构,就把new-destory的逻辑换成了new-update的逻辑,减少了元素的修改。
update的时候只是去让ActivityIndicator不出现似乎被hide了。
第四步 测试bug是否还能重现
我们希望通过减少页面元素反复的删除创建,来fix这个bug,结果怎么样呢?
居然神奇的很难复现了,我们很开心,虽然还是没弄懂原因。
后面QA说在真机上还是遇到了几次,让我们更是费解,费解的是出现的概率确实变少了,但为啥还会出现?
第五步 分析bug产生的根本原因
这个时候我们需要了解bug产生的真正原因了。
我们重新回到这个bug的表现,为什么点击空白区域会触发TextInput的focus方法?我们尝试做了这样的事情。
找出在会触发TextInput的focus的地方,会不会是被错误的调用了。
除了在代码逻辑里面少量的通过绑定ref然后触发.focus方法(因为是少量出现,不符合我们这个bug一出现所有input都受影响的情景,快速排除不是这部分原因),我们发现在RN提供的TextInput组件里面也有很多地方会调用到focus方法。
大概查找的路径是文件node_modules/react-native/Libraries/Components/TextInput/TextInput.js中发现多处this.focus()的调用,除了正常的onFocus事件的绑定以及autoFocus,有一个在_onPress里面的调用感觉很奇怪,暂时放着
_onFocus: function(event: Event) {if (this.props.onFocus) {this.props.onFocus(event);}if (this.props.selectionState) {this.props.selectionState.focus();}},//奇怪的地方_onPress: function(event: Event) {if (this.props.editable || this.props.editable === undefined) {console.log('------> _onPress',event);//logthis.focus();}},
先打了一段log,发现点击空白区域的时候,真的被触发了呀,当然点击输入框也会触发,二者的表现一样一样的。
结论
没法确认是不是被错误的调用了,但确实是被调用了,我们去找找调用的地方看有什么线索。
看到target里面的ResponderSyntheticEvent了吗,找到这个文件打几行log 有惊喜。
在ResponderSyntheticEvent打日志获取更多信息,并对比正常和有bug时候的异同
如下
function ResponderSyntheticEvent(dispatchConfig, dispatchMarker, nativeEvent, nativeEventTarget) {console.log('-->response',dispatchConfig.registrationName,nativeEventTarget);return SyntheticEvent.call(this, dispatchConfig, dispatchMarker, nativeEvent, nativeEventTarget);
}
你会发现你点击页面的任何一个区域都会在console出现这样的记录
并且任何一个点击的响应一般都会有如下四个阶段
- onResponderGrant
- onResponderStart
- onResponderEnd
- onResponderRelease
然后试图重现bug,看看log有没有什么不一样,果然被逮住了。
其中绿色部分的log是正常的,红色划线是不正常的,发现是输入框(1387)这个node grant了手势响应但是后面手势开始是空白区域(1398),最终空白区域(1398)影响了输入框(1387)。
结论
正常情况下四个事件依次触发,出现bug的情况下input的onResponderGrant被调用后面是空白区域的onResponderStart被调用,和其他对比之后,发现onResponderGrant不应该被调用。
了解手势响应系统
还是很疑惑为什么最开始input框(1387)会grant呢?这部分涉及对手势的响应,去rn的官网上面我们去了解一下手势响应系统,看到提到
具体的实现在ResponderEventPlugin.js文件中,你可以在源码中读到更多细节和文档。
然后找到react/lib/ResponderEventPlugin.js文件,
在多个地方(主要是setResponderAndExtractTransfer方法内)找到ResponderSyntheticEvent(老朋友了,之前在ta那里打过log)的调用,比如
var grantEvent = ResponderSyntheticEvent.
getPooled(eventTypes.responderGrant,
wantsResponderInst, nativeEvent, nativeEventTarget);
而 setResponderAndExtractTransfer 方法是否调用取决于canTriggerTransfer方法的返回值。
var extracted = canTriggerTransfer(topLevelType, targetInst, nativeEvent) ? setResponderAndExtractTransfer(topLevelType, targetInst, nativeEvent, nativeEventTarget) : null;
细看canTriggerTransfer方法
function canTriggerTransfer(topLevelType, topLevelInst, nativeEvent) {console.log('-->response c3', trackedTouchCount, trackedTouchCount > 0);return topLevelInst && (// responderIgnoreScroll: We are trying to migrate away from specifically// tracking native scroll events here and responderIgnoreScroll indicates we// will send topTouchCancel to handle canceling touch events insteadtopLevelType === EventConstants.topLevelTypes.topScroll &&!nativeEvent.responderIgnoreScroll || trackedTouchCount > 0 &&topLevelType === EventConstants.topLevelTypes.topSelectionChange ||isStartish(topLevelType) || isMoveish(topLevelType));
}
其实这个地方的log最开始打了好多,最好发现是trackedTouchCount值不一样导致的。
同时去能够影响trackedTouchCount值的地方加一些log
extractEvents: function (topLevelType, targetInst, nativeEvent, nativeEventTarget) {if (isStartish(topLevelType)) {trackedTouchCount += 1;console.log('-->response trackedTouchCount+1',trackedTouchCount,topLevelType,nativeEventTarget);} else if (isEndish(topLevelType)) {if (trackedTouchCount >= 0) {trackedTouchCount -= 1;console.log('-->response trackedTouchCount-1',trackedTouchCount,topLevelType,nativeEventTarget);} else {console.log('-->response trackedTouchCount null',trackedTouchCount,topLevelType);console.error('Ended a touch event which was not counted in `trackedTouchCount`.');return null;}}***}
简单描述下这条依赖关系,但其实并不确定是不是在有bug情况下trackedTouchCount值不一样,先留一个假设。
- 变量trackedTouchCount
- 方法canTriggerTransfer返回值
- 方法setResponderAndExtractTransfer
- 影响grantEvent执行
在控制台仔细观察,随便点击几下,得到如下的截图,
这是在正常未出现bug的情况下,trackedTouchCount的值在0和1之间摆动,当tounchstart的时候+1,在touchend的时候-1。
我们再去重现bug,当我们去反复切换tab的时候,看看日志有什么区别。
简单分析
有一条toucnStart的记录987没有对应的TouchEnd,导致trackedTouchCount没法复位为0。
为什么在反复切换tab的时候,会出现这样有toucnStart而没有toucnEnd的情景,想了下发现是每次切换tab其实是做了这么几件事情
- 点击tab页签
- 页面出现mask(new一个新的)
- 页面请求数据
- 数据response到达(destroy mask)
但如果频繁点动tab页签,其实某些边界时刻,点到的是mask,对应mask的node的toucnStart被触发,然后请求即将到达,mask被destroy了,toucnEnd永远都不会被触发了。
所以当我们把mask的实现从new-destroy改成new-update的时候,保证了toucnEnd最终能够被触发了。
归纳
- mask的老的实现,导致mask的toucnEnd事件某些状况不会被响应,
- 影响了变量trackedTouchCount的值得正确性(永远无法恢复为0 ),
- 影响方法canTriggerTransfer返回值在页面有input的时候为true,
- 影响方法setResponderAndExtractTransfer中的grantEvent的被错误执行,
- 最终导致focus的时候input被错误的grant,
- 然后点击其他任何空白区域都会触发input。
什么情况下 会再次发生
这一次我们定位了这个issue的问题,并且使用了一些不是完全fix的方法,让这个bug不会由于mask的频繁使用而出现。
但有没有可能在其他的业务场景或者写代码的过程中再次引入这个bug呢? 答案是肯定的。
后续在team 内我们再次fix过几次类似的问题,简单总结如下:
- 场景1 : 给某个业务实体(人 或 物) 添加备注标签,在输入栏输入并按回车后就会生成新的备注标签,备注标签上会有一个小叉叉,点击小叉叉可以删除这个备注标签。一旦删除某个备注标签,就会重现。
- 场景2: 在某个页面,会展示一些实体(物)的详细信息,因为信息比较多,我们做了一个flip的效果,点击后会翻转展示更多,在点一下就会回到之前的,就像一个扑克牌的两面,一面是花纹,一面是具体的大小比如K。 如果连续多次翻转,就会重现。
这两个场景,以及我们最初遇到的mask的场景,看似没有任何联系,但是最终都会触发软键盘莫名显示的问题,其根本原因和之前mask的一致,都是trackedTouchCount这个变量被改坏了。
那为什么这几个场景都会改坏这个变量呢?
在排查的过程中,我们发现一旦出现某个页面元素(或者在RN的语境下称之为组件比较合适)被删除,而页面元素上的onPressOut没来得及触发,就会出现此类的问题。
这是RN事件响应系统的问题,一般很难去修改底层库,我们目前的解决办法基本上是
- 不频繁反复的删除新建同样的页面元素,让同一页面元素保持住,或者减少其删除重新新建的次数(mask 和翻转的例子);
- 不使用onPressIn事件去触发删除某些正在显示的组件(备注标签的例子);
其他
另外一个在github上面报的因为trackedTouchCount变量不正确状态导致的issue
[ListView]Scroll on ListView end with an error saying "Ended a touch event which was not counted in trackedTouchCount"
有兴趣的可以看看。
Fix一个随机出现的键盘弹出的issue后的思考(ReactNative)相关推荐
- 仿QQ空间评论随软键盘弹出和收回一个输入布局
先来看一下效果图: 点击图一底部的回复后,出现图二效果.软键盘弹出,上面显示出一个布局,包括输入框和功能按钮等. 先说下我的心路历程吧,原 ...
- 小程序中点击input控件键盘弹出时placeholder文字上移
最近做的一个小程序项目中,出现了点击input控件键盘弹出时placeholder文字上移,刚开始以为是软键盘弹出布局上移问题是传说中典型的fixed 软键盘顶起问题,因此采纳了网上搜到的" ...
- swift实现ios类似微信输入框跟随键盘弹出的效果
为什么要做这个效果 在聊天app,例如微信中,你会注意到一个效果,就是在你点击输入框时输入框会跟随键盘一起向上弹出,当你点击其他地方时,输入框又会跟随键盘一起向下收回,二者完全无缝连接,那么这是怎么实 ...
- iOS键盘弹出时动画时长失效问题
iOS键盘弹出动画问题 今天在写键盘弹出时遇见一个问题.监听UIKeyboardWillShowNotification通知让Label做一个移动的动画,指定duration为15,但动画实际完成时间 ...
- android 设置键盘弹出动画,Android实现键盘弹出界面上移的实现思路
1.首先说一下思路: 基本就是结合layout中ScrollView视图和AndroidManifest.xml中activity中的android:windowSoftInputMode属性配置实现 ...
- 移动端网站,键盘弹出对页面的影响
在移动端网站中,ios与安卓键盘弹出时对页面有不同的处理方式. ios,键盘弹出但整体页面高度不变. 安卓,页面高度=屏幕高度-键盘高度 这样对页面样式就会造成不同的影响. 当有表单弹窗,且弹窗高度在 ...
- android 软件盘弹回去的最好体验,Android 软键盘弹出 日常填坑
开发输入框的开发者都会遇到一个问题,那就是在登录界面时,当你点击输入框时,下边的按钮有时会被输入框挡住,这个不利于用户的体验,所以很多人希望软键盘弹出时,也能把按钮挤上去.这样的交互更人性化,做得合理 ...
- android软键盘把布局顶上去,Android 软键盘弹出时把原来布局顶上去的解决方法
键盘弹出时,会将布局底部的导航条顶上去. 解决办法: 在mainfest.xml中,在和导航栏相关的activity中加: android:name=".filing.MainActivit ...
- Android中软键盘弹出时关于布局的问题
当在Android的layout设计里面如果输入框过多,则在输入弹出软键盘的时候,下面的输入框会有一部分被软件盘挡住,从而不能获取焦点输入. 解决办法: 方法一:在你的activity中的oncrea ...
最新文章
- 全景分割:CVPR2019论文解析
- 《Web 标准实战》——Web开发人员必读的一本书
- SPOJ 375. Query on a tree (树链剖分)
- SpringBoot整合easyexcel实现导入导出
- (王道408考研数据结构)第五章树-第三节3:线索二叉树
- 动态ip解析 linux,ddwrt路由/linux动态解析ip(ddns)到dnspod配置
- 开发板移植mysql_数据库移植到gpu
- ModelAndView简介
- C语言数据结构——求二叉树叶子结点个数
- win10连接计算机,win10怎么连接局域网打印机
- 编译liteos(ubuntu)
- 个人推荐讲的非常好的数据结构免费[速成 速成 速成]视频了
- c语言第三章程序设计实训
- 一台双u的服务器和一台单u的服务器性能能高一半吗,单机柜供电能力提升后,选择1U还是2U?...
- 写给小白的云计算入门科普
- Java里format什么意思_java String.Format详解
- 网页设计配色应用实例剖析——橙色系
- 基于simulink的chaios混沌电路仿真
- 英语初级语法--句子成分(词性)(成分)
- docker和kvm的区别,简洁大白话篇,两者的优势对比
热门文章
- typecho除了首页其他大部分网页404怎么办?
- 安装sql server 2000
- 部署自己的tomcat,让tomcat和IIS共同享用服务器的80端口
- 分治法在二叉树遍历中的应用(JAVA)--二叉查找树高度、前序遍历、中序遍历、后序遍
- jQuery教程06-基本筛选选择器
- python 多组直方图 画图_python – 使用matplotlib的多个并排直方图?
- 开关怎么使用_水龙头漏水怎么办?使用时把控开关力度很重要
- beeline执行sql文件_MyBatis的SQL执行流程不清楚?看完这一篇就够了
- python 如何判断excel单元格为空_如何用python处理excel(二)
- html checked属性值,HTML复选框的checked属性的值是多少?