记一次RN Debug经历

bug是如果一个scrollview上有多个TextInput,那么一个TextInput处于focus状态时点击其它TextInput只会关闭键盘,没有将另一个TextInput进行focus,其实如果我之前了解React中事件传递顺序的话并没有必要这么麻烦,虽然花了很多时间,但是也许学习到了很多。

项目源码: Reminders

首先iOS程序员的直觉之检查canBecomeFirstResponder,打断点

canBecomeFirstResponder检查

发现正常情况该方法会被调用两次(false, true),然而异常情况只会被调用一次(false)

正常情况第二次调用的调用栈

textField canBecomeFirstResponder调用栈

图中的foucs方法暴露给了js, 猜测是由js调用的
native端 focus方法

搜索focus

focus( 在整个项目中的搜索结果


结果太多。。。猜肯定和TextInput有关

focus( 在整个TextInput中的搜索结果

TextField在_onPress里调用了focus (继续找下去可以看到JS端最终调用了UIManager.focus(), 对应上面native端focus方法)

接着搜索_onPress,看它是在哪里被调用的,可以看到这是个callback


_onPress搜索

然后在_onPress里设个断点,发现异常情况并不会调用_onPress
正常时调用_onPress的调用栈(讲真我第一眼看到这个栈想掀桌子。。):

_onPress调用栈

从上到下分析:
touchableHandlePress只是简单转发:


touchableHandlePress


在第三个_performsideEffectsForTransition打断点,发现无论怎样都会被执行多次。。慢慢分析会比较复杂。先尝试换思路,我们先确定是事件没有发出还是传输时丢失了, 我们需要先找到Js端event的源头然后推出Native发送event的位置

根据TextInput的render函数实现可知onPress信号由TouchableWithoutFeedback接受:

TextInput::render()

由TouchableWithoutFeedback的实现可知TouchableWithoutFeedback只是将child的clone加上了一大堆方法处理的属性然后直接返回child的clone


TouchableWithoutFeedback::render()


所以Js端event的接受者是child 即 {textContainer} ,对应的是native端的RCTTextField:

现在在原生找RCTTextField(一开始那个类)
原生检测touch事件无非两种方法。要么实现UIResponder的方法,要么加GestureRecognizer,
RCTTextField的实现里没有UIResponder的方法,所以确定是GestureRecognizer。
要添加GestureRecognizer必须要有RCTTextField的示例,所以必然会有RCTTextField的引用,搜索一下。


RCTTextField搜索结果

从图看来一定是在RCTTextField自身或者RCTTextFieldManager里添加的GestureRecognizer了!一定是这样没错!

看了下。。。。妈的没有。。。。
想了想,还有一种可能:响应的是parent view而不是自身。那么最有可能的就是RootView了。
看了下,真的有!(现在想想这样做最有道理,首先性能上肯定占优势,其次如果子view和parentView都有gestureRecoginzer不做处理的话同时都会响应,就麻烦了

RCTRootView初始化方法

在处理touch的方法handleGestureUpdate:里打个断点
看来是正常发出了。。。
看看JS调用栈有个
调用栈

设个断点

在receiveTouches里设断点

正常: topTouchStart,topTouchEnd,topFocus

异常:topTouchStart, topTouchEnd, topEndEditing,topBlur
(正常异常情况下topTouchStart和topTouchEnd的rootNodeID都相同)

我们都知道focus是touch的结果,所以推测是topTouchStart、topTouchEnd的后续异常处理导致的bug

继续看调用栈:_receiveRootNodeIDEvent只做了简单的转发

_receiveRootNodeIDEvent

在handleTopLevel里打个断点:
handleTopLevel

发现正常异常情况在传入topTouchEnd时传给runEventQueueInBatch的参数不同
异常:event[1]. _dispatchListeners. __reactBoundMethod = function scrollResponderHandleResponderRelease(e)
正常:event[1]. _dispatchListeners. __reactBoundMethod = function touchableHandleResponderRelease(e)
我们有理由相信就是因为touchableHandleResponderRelease没被调用导致的bug

现在可以确定bug在EventPluginHub.extraceEvents里

来看下实现:

EventPluginHub.extraceEvents

关于plugin是啥,一开始我也不知道。后来去专门看了下初始化的源码才知道。现在就当未知数(每个plugin负责监听一套事件,现在的RN虽然有两个默认plugin,但是大多数组件都依赖于原有的React自带plugin,由native端定义的plugin几乎没用上)。
我们知道的:

  1. 正常异常情况下EventPluginRegistry.plugins返回的值都是一个长度为2的数组
  2. 异常情况下接受“topTouchEnd”时第一个plugin产生的extractedEvents[1]的listener是scrollResponderHandleResponderRelease(e) 而正常情况下是touchableHandleResponderRelease(e)
  3. extractedEvents在for plugin循环里调用(一般不会去更改plugin)
    推断:
    EventPluginRegistry.plugins两次返回的都是一样的数组
    bug在possiblePlugin.extractEvents里
    进去看看。。
    possiblePlugin.extractEvents

    extracted的_dispatchListeners在这一行前是null
    执行完这一行变为含有function scrollResponderHandleResponderRelease(e)的回调
    根据accumulate这个参数名大概知道是把finalEvent的内容放进extracted里了
    看下finalEvent的内容验证一下
    finalEvent

    finalEvent在一开始初始化
    var finalEvent = ResponderSyntheticEvent.getPooled(finalTouch, responderID, nativeEvent, nativeEventTarget);
    后大概就是这样子没变过
    然后我们看一下用来初始化finalEvent的ResponderSyntheticEvent.getPooled的参数
    正常:{
    finalTouch: "onResponderRelease"
    responderID: ".r[1]{TOP_LEVEL}[0].$1.0.1.0.$scene_0.0.1.$1.1.0.1:$r_s1_1.1"
    nativeEvent: …
    nativeEventTarget: …
    }
    异常:{
    finalTouch: "onResponderRelease"
    responderID: ".r[1]{TOP_LEVEL}[0].$1.0.1.0.$scene_0.0.1.$1.1"
    nativeEvent: …
    nativeEventTarget: …
    }

可以看到responderID不同。。自然respond的方法也不同
于是我们watch responderID,找到它是在什么时候开始不同的
发现responderID从ResponderSyntheticEvent.getPooled一开始就是不同的
想想既然touchEnd触发事件,那么touchStart理论上就没意义了,个人能想到唯一有可能的功能就是确定responderId
于是在touchStart时打个断点

在touchStart时打个断点

这行结束前后console输出一下possiblePlugin.getResponderID()
possiblePlugin.getResponderID()

假设正确

所以问题其实是出在接收touchStart时调用的extractEvents方法
在该方法里watch responderId:
发现responderId在
var extracted = canTriggerTransfer(topLevelType, topLevelTargetID, nativeEvent) ? setResponderAndExtractTransfer(topLevelType, topLevelTargetID, nativeEvent, nativeEventTarget) : null;
里被改变。。。。
从名字里也能看出来是setResponderAndExtractTransfer干的。。。进去看看

setResponderAndExtractTransfer

在这changeResponder方法里repsonderId被更改
更改结果为wantsResponderID
所以是wantsResponderID出错了
该执行路径下(异常时执行路径)wantsResponderID只在声明时赋值了
wantsResponderID声明

有两种可能:
executeDispatchsInOrderStopAtTrue出错
参数shouldSetEvent不对

进executeDispatchsInOrderStopAtTrue看看:

executeDispatchsInOrderStopAtTrue

再进executeDispatchesInOrderStopAtTrueImpl
核心代码如下

executeDispatchesInOrderStopAtTrueImpl

// 省略一些无关代码
executeDispatchesInOrderStopAtTrueImpl续

这段代码大概就是找到第一个响应事件的listener然后返回listener所属的object的id
该循环在i=3时跳出
dispatchListeners[3] 里是scrollResponderHandleStartShouldSetResponderCapture
dispatchListeners[4] 里是touchableHandleStartShouldSetResponder
也就是说scrollResponderHandleStartShouldSetResponderCapture ”偷走”了事件
搜索下scrollResponderHandleStartShouldSetResponderCapture, 找到他的实现:
scrollResponderHandleStartShouldSetResponderCapture

注意那行注释。。。如果键盘打开他就会“eat taps”。。。。。。
其实bug就出在这。。。下面是因为理解错误而白白多走得几步(React系统中parentView有一个方法专门“偷”子view事件)

下面是之前想错了多想的几步,没有考虑到原生中点击事件也有可能在hitTest里就被截取了=。=

所以bug出在这?其实仔细想想并不是。。。这个只是说了如果键盘打开他就会接收事件
在iOS中如果parent view 和 sub view 都能响应点击事件(UIResponder),那么subView会得到事件的优先处置权
所以bug出在parentView的优先级高于subView,也就是event._dispatchListeners顺序错误
回到setResponderAndExtractTransfer方法。发现异常情况下shouldSetEvent._dispatchListeners在这一行被赋值:

setResponderAndExtractTransfer

进去看看:
accumulateTwoPhaseDispatches

再进
forEachAccmulated

上三步可以简化为
accumulateTwoPhaseDispatchesSingle(shouldSetEvent)

EventPluginHub.injection.getInstanceHandle().traverseTwoPhase(event.dispatchMarker,accumulateDirectionalDispatches, event);
大概做的事是从rootView遍历到标记为event.dispatchMarker的view,然后反向遍历(event.dispatchMarker会被访问两次,一次正向,一次反向),每个节点调用accumulateDirectionalDispatches,参数为(节点id,遍历方向,event)

accumulateDirectionalDispatches

大概就是问一下每个view:你接不接受这个event,接受的话就把你放到event的候选目标里
但是为啥会需要两个遍历阶段?

capturephase是啥?
查了一下文档:

Gesture Responder System:http://facebook.github.io/react-native/releases/0.26/docs/gesture-responder-system.html#capture-shouldset-handlers
里面有这么一句话:

However, sometimes a parent will want to make sure that it becomes responder. This can be handled by using the capture phase. Before the responder system bubbles up from the deepest component, it will do a capture phase, firing on*ShouldSetResponderCapture. So if a parent View wants to prevent the child from becoming responder on a touch start, it should have a onStartShouldSetResponderCapture handler which returns true.

。。。

嗯。。。我现在就想抽自己。。。当初为啥不好好看文档。。。不过至少对RN了解深入了很多。。。
其实这么说来bug原因就是scrollView不应该capture event。而是如果子view都不处理才去处理。
打开scrollResponder把

scrollResponderHandleStartShouldSetResponderCapture和scrollResponderHandleStartShouldSetResponder的实现互换就解决了。

这样还有个问题,如果打开键盘后直接点击返回键盘不消失,好办,在Touchable的响应方法里加一句这个就搞定了
TextInputState.blurTextInput(TextInputState.currentlyFocusedField())
当然这样textInput被点击时也会触发这个事件,但是因为iOS如果在一个时间周期内收到关、开键盘。他就会忽视前一个信号。所以一切正常啦~

update:
在RN的repo里提了这个问题,有人说可以组合keyboardDismissMode和keyboardShouldPersistent两个属性,来解决。但是个人感觉这依旧是个workaround呀。。。因为我觉得通过capture事件来隐藏键盘还是不符合正常事件传递顺序。而且这样点击返回按钮依然不会隐藏键盘。然而没人继续回我了。。。大概600多个issue确实太忙了。。看来又不能在大型项目中提交PR了。。。桑心。。。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容