網(wǎng)站首頁(yè) 編程語(yǔ)言 正文
問(wèn)題來(lái)源
在我們升級(jí)Flutter2.5后,測(cè)試在走整個(gè)業(yè)務(wù)流程中發(fā)現(xiàn)了有頁(yè)面卡死現(xiàn)象,于是給我提了一個(gè)BUG。
在xx頁(yè)面多次操作后,頁(yè)面卡死,頁(yè)面還可以滾動(dòng)但是無(wú)法跳轉(zhuǎn),點(diǎn)擊長(zhǎng)按事件都失效了。
在我多次測(cè)試后發(fā)現(xiàn),確實(shí)存在這個(gè)問(wèn)題,而且老版本也都存在。
問(wèn)題難點(diǎn)
復(fù)現(xiàn)難
問(wèn)題定位
最開(kāi)始,我先確定了失效情況下,事件源頭有沒(méi)有正確發(fā)送,所以,先在_dispatchPointerDataPacket
方法上添加了斷點(diǎn)。結(jié)果發(fā)現(xiàn)都是正常。其實(shí)也好理解,頁(yè)面可以滾動(dòng),說(shuō)明引擎層發(fā)送事件肯定是正常的。
在進(jìn)行一系列沒(méi)有用的斷點(diǎn)定位后發(fā)現(xiàn),正常事件的hitTestResult
(事件中命中測(cè)試階段收集的所有能夠響應(yīng)事件的RenderObject
節(jié)點(diǎn))和錯(cuò)誤頁(yè)面的hitTestResult
的_path
數(shù)量不一樣。
正常的hitTestResult
錯(cuò)誤的hitTestResult
?
經(jīng)過(guò)對(duì)比發(fā)現(xiàn),錯(cuò)誤的列表到RenderPointerListener
這個(gè)就停止了,我看這名字還挺熟悉,難道跟IgnorePointer
有啥關(guān)系?我通過(guò)這個(gè)RenderObject
節(jié)點(diǎn)的parent
一層一層往上找,發(fā)現(xiàn)是ScrollableState
中使用了IgnorePointer
(ScrollableState
是列表組件如ListView
、SingleChildScrollView
等底層使用的Widget State)
//... Widget result = _ScrollableScope( scrollable: this, position: position, child: Listener( onPointerSignal: _receivedPointerSignal, child: RawGestureDetector( key: _gestureDetectorKey, gestures: _gestureRecognizers, behavior: HitTestBehavior.opaque, excludeFromSemantics: widget.excludeFromSemantics, child: Semantics( explicitChildNodes: !widget.excludeFromSemantics, child: IgnorePointer( key: _ignorePointerKey, ignoring: _shouldIgnorePointer, ignoringSemantics: false, child: widget.viewportBuilder(context, position), ), ), ), ), ); //...
這里會(huì)通過(guò)_ignorePointerKey
來(lái)把滾動(dòng)區(qū)域及其子節(jié)點(diǎn)的事件都屏蔽了。那么什么時(shí)候_ignorePointerKey
會(huì)被置為true
呢。
通過(guò)了解ScrollableState
源碼發(fā)現(xiàn),只要頁(yè)面在滾動(dòng)過(guò)程中,_ignorePointerKey
就會(huì)被置為true
,當(dāng)手指抬起時(shí),才會(huì)將_ignorePointerKey
重新置為false
。
通過(guò)多次斷點(diǎn)和日志輸出發(fā)現(xiàn),當(dāng)我從后面的頁(yè)面返回到目標(biāo)頁(yè)面時(shí),第一次滾動(dòng)時(shí),就觸發(fā)了ScrollableState
的setIgnorePointer
將_ignorePointerKey
置為true
了,但是后面再無(wú)事件將_ignorePointerKey
置為false
了,此后,再滾動(dòng)頁(yè)面時(shí),也無(wú)法觸發(fā)setIgnorePointer
方法。
到這里,想繼續(xù)調(diào)試,就需要比較熟悉Flutter的事件原理了,因?yàn)檫@里我只想講一下我解決這個(gè)問(wèn)題的思路,所以Flutter原理的知識(shí)不多講。后面經(jīng)過(guò)一系列調(diào)試發(fā)現(xiàn),問(wèn)題出在OneSequenceGestureRecognizer
這個(gè)抽象類(lèi)中
abstract class OneSequenceGestureRecognizer extends GestureRecognizer { //... @protected void startTrackingPointer(int pointer, [Matrix4? transform]) { // 將當(dāng)前指針和當(dāng)前的handleEvent方法添加到全局指針識(shí)別器中存儲(chǔ)緩存起來(lái) GestureBinding.instance!.pointerRouter.addRoute(pointer, handleEvent, transform); _trackedPointers.add(pointer); assert(!_entries.containsValue(pointer)); _entries[pointer] = _addPointerToArena(pointer); } @protected void stopTrackingPointer(int pointer) { if (_trackedPointers.contains(pointer)) { // 從全局指針中移出當(dāng)前指針 GestureBinding.instance!.pointerRouter.removeRoute(pointer, handleEvent); _trackedPointers.remove(pointer); // 如果_trackedPointers是空的 if (_trackedPointers.isEmpty) didStopTrackingLastPointer(pointer); } } }
OneSequenceGestureRecognizer
這個(gè)類(lèi)的作用是當(dāng)存在多個(gè)手勢(shì)時(shí),只響應(yīng)一個(gè)手勢(shì)。比如我同時(shí)兩個(gè)手指點(diǎn)擊一個(gè)按鈕,按鈕的點(diǎn)擊事件也只會(huì)觸發(fā)一次。像我們常見(jiàn)的TapGestureRecognizer
、VerticalDragGestureRecognizer
、HorizontalDragGestureRecognizer
等最終都是實(shí)現(xiàn)的這個(gè)類(lèi)。
在這個(gè)類(lèi)中startTrackingPointer
方法會(huì)在手指按下后,也就是發(fā)生PointerDownEvent
時(shí)將當(dāng)前類(lèi)的handleEvent
添加到全局指針識(shí)別器中,并且將這個(gè)pointer
(可以看做指針id)添加到_trackedPointers
中緩存起來(lái),可以這樣理解,這個(gè)方法就是一次手勢(shì)的開(kāi)始。
當(dāng)發(fā)生PointerUpEvent
等事件時(shí),會(huì)調(diào)用stopTrackingPointer
事件,將手勢(shì)移除,這就標(biāo)志著手勢(shì)的結(jié)束。
其中有個(gè)_trackedPointers.isEmpty
判斷,會(huì)調(diào)用didStopTrackingLastPointer
方法,這個(gè)方法一般是將手勢(shì)識(shí)別器的狀態(tài)置為ready
。經(jīng)過(guò)我多次對(duì)問(wèn)題頁(yè)斷點(diǎn)發(fā)現(xiàn),無(wú)論如何都調(diào)不到這個(gè)方法,也就是說(shuō)_trackedPointers
里面一直有個(gè)手勢(shì)指針沒(méi)有被移除。
這里我要介紹一下VSCode一個(gè)調(diào)試方法。因?yàn)槲疫€不知道問(wèn)題的根源,所以我復(fù)現(xiàn)問(wèn)題是通過(guò)不斷點(diǎn)擊頁(yè)面同時(shí)觸發(fā)頁(yè)面跳轉(zhuǎn)來(lái)達(dá)到的,而且只是有幾率復(fù)現(xiàn)。所以我無(wú)法通過(guò)斷點(diǎn)來(lái)確定這里為何有手勢(shì)事件沒(méi)有調(diào)用stopTrackingPointer
,所以我使用了VSCode的LogPoint
方式來(lái)對(duì)整個(gè)過(guò)程進(jìn)行日志輸出。
在不斷復(fù)現(xiàn)問(wèn)題查看日志中發(fā)現(xiàn),在跳轉(zhuǎn)頁(yè)面前,會(huì)有指針事件被添加進(jìn)_trackedPointers
,但是卻沒(méi)有調(diào)用stopTrackingPointer
方法就跳轉(zhuǎn)到新頁(yè)面了。
tap 4. addAllowedPointer (tap.dart) _down != null = true 637436658 tap 5. _trackedPointers add 195 502831342 handleEvent: 931478062 tap 5. _trackedPointers add 195 21393736 handleEvent: 790157058 tap 5. _trackedPointers add 195 126324365 handleEvent: 160402385 onNativeRouteEvent: (9): NativeRouteEvent.onCreate onNativeRouteEvent: (8): NativeRouteEvent.onPause onFlutterRouteEvent: (9): FlutterRouteEvent.onPush
問(wèn)題確定
由于我們是混合棧項(xiàng)目,我們是自己寫(xiě)的一套混合棧路由管理,類(lèi)似FlutterBoost,在進(jìn)行頁(yè)面跳轉(zhuǎn)時(shí),會(huì)將FlutterEngine
先detach,然后再跳轉(zhuǎn)。在Flutter的Android發(fā)送事件源碼里面,會(huì)對(duì)FlutterEngine
是否attach
進(jìn)行判斷,然后觸發(fā)Flutter Framework一系列處理。
@Override public boolean onTouchEvent(@NonNull MotionEvent event) { // 這里判斷是否attach if (!isAttachedToFlutterEngine()) { return super.onTouchEvent(event); } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { requestUnbufferedDispatch(event); } return androidTouchProcessor.onTouchEvent(event); }
這里由于頁(yè)面跳轉(zhuǎn)時(shí)如果還有事件在處理(比如手指按下并沒(méi)有抬起),那么跳轉(zhuǎn)后,F(xiàn)lutter再也接收不到手指抬起的事件了,所以_trackedPointers
就一直不被正確移除,導(dǎo)致了事件異常。由于是我們自己寫(xiě)的混合棧庫(kù),所以修改起來(lái)也簡(jiǎn)單。
問(wèn)題解決
Android
public class XXXFlutterView extends FlutterView { // ... @Override public boolean onTouchEvent(@NonNull MotionEvent event) { try { AndroidTouchProcessor androidTouchProcessor; Field field = this.getClass().getSuperclass().getDeclaredField("androidTouchProcessor"); field.setAccessible(true); androidTouchProcessor = (AndroidTouchProcessor)field.get(this); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { requestUnbufferedDispatch(event); } return androidTouchProcessor.onTouchEvent(event); } catch (Exception e) { e.printStackTrace(); return super.onTouchEvent(event); } } }
我們本身有一個(gè)繼承于FlutterView
的類(lèi),在其中實(shí)現(xiàn)一下父類(lèi)的onTouchEvent
方法,把isAttachedToFlutterEngine
的判斷去掉即可,由于androidTouchProcessor
是私有類(lèi),所以這里我使用了反射。
iOS解決思路還不太一樣,在新的Flutter版本中,iOS提供了forceTouchesCancelled
方法來(lái)取消Flutter中的事件,所以iOS是通過(guò)在混合棧中detach前,手動(dòng)調(diào)用一下這個(gè)方法來(lái)解決這個(gè)問(wèn)題的。
總結(jié)
由于對(duì)Flutter事件很多細(xì)節(jié)掌握的不夠到位,所以這個(gè)問(wèn)題從定位問(wèn)題到最終解決差不多花了一周時(shí)間,解決過(guò)程中也加深了我對(duì)Flutter事件的理解。
原文鏈接:https://juejin.cn/post/7024040068289396744
相關(guān)推薦
- 2023-04-12 python中l(wèi)ist.copy方法用法詳解_python
- 2022-12-29 C#使用Lambda表達(dá)式簡(jiǎn)化代碼的示例詳解_C#教程
- 2022-06-01 利用20行Python?代碼實(shí)現(xiàn)加密通信_(tái)python
- 2022-11-12 Kotlin中的惰性操作容器Sequence序列使用原理詳解_Android
- 2022-10-02 OpenCV黑帽運(yùn)算(BLACKHAT)的使用_python
- 2022-04-18 html2canvas 畫(huà)圖出現(xiàn)空白的情況,引出圖片跨域的相關(guān)問(wèn)題
- 2021-12-09 Linux環(huán)境下安裝JDK1.8_Linux
- 2022-02-15 使用數(shù)組的sort方法完成項(xiàng)目中的排序功能(數(shù)組sort方法與chart圖表展示結(jié)合)
- 最近更新
-
- window11 系統(tǒng)安裝 yarn
- 超詳細(xì)win安裝深度學(xué)習(xí)環(huán)境2025年最新版(
- Linux 中運(yùn)行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲(chǔ)小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎(chǔ)操作-- 運(yùn)算符,流程控制 Flo
- 1. Int 和Integer 的區(qū)別,Jav
- spring @retryable不生效的一種
- Spring Security之認(rèn)證信息的處理
- Spring Security之認(rèn)證過(guò)濾器
- Spring Security概述快速入門(mén)
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權(quán)
- redisson分布式鎖中waittime的設(shè)
- maven:解決release錯(cuò)誤:Artif
- restTemplate使用總結(jié)
- Spring Security之安全異常處理
- MybatisPlus優(yōu)雅實(shí)現(xiàn)加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務(wù)發(fā)現(xiàn)-Nac
- Spring Security之基于HttpR
- Redis 底層數(shù)據(jù)結(jié)構(gòu)-簡(jiǎn)單動(dòng)態(tài)字符串(SD
- arthas操作spring被代理目標(biāo)對(duì)象命令
- Spring中的單例模式應(yīng)用詳解
- 聊聊消息隊(duì)列,發(fā)送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠(yuǎn)程分支