🔥码云GVP开源项目 12k star Uniapp+ElementUI 功能强大 支持多语言、二开方便! 广告
[TOC] # 背景 近几个月来收到了多起在Android11手机上,拖拽界面时无法滑动的问题反馈。 表现为在异常的界面上按住屏幕进行**滑动没有任何响应,但又可以进行点击**。而除了这个界面,**其他界面一切正常**。 ## 复现场景 在B界面(个人主页)发送事件(取消关注某个作者),界面A(列表界面)收到事件,进行RemoveData(移除对应作者的作品), 然后调用RecyclerView.Adapter#notifyDataSetChange操作通知刷新。再返回到A界面,此时的A界面变变得无法滑动,但可以点击。再点击进入其他界面C,C界面都可正常滑动。 ## 被合并的Move事件 大部分的滑动问题都是因为存在着嵌套滑动冲突。为了验证是否是嵌套的问题,我们需要在不同层级的View中打印接收到的MotionEvent. 很快,我们就排除了嵌套滑动的因素。因为当我们在Activity#dispatchTouchEvent的时候对MotionEvent进行打印,惊奇的发现MotionEvent在分发到Activity的时候就已经“**不同寻常**”。 1 . 手指在按压滑动过程中不会收到任何Move事件。Move事件在手指抬起后,跟随Up事件一并发送,并且有仅只有一个Move事件。 2. 通过查看这个“唯一”的Move事件,发现其MotionEvent#getHistorySize()竟然达到几十上百,存放着Move过程中的所有轨迹点。 # 排查流程 ## 系统进程 InputReader&InputDispatcher ![](https://img.kancloud.cn/f7/ec/f7ec8e13586f596f7615c5da740cfcb0_1200x716.png) InputReader 和 InputDispatcher 是跑在System Server进程中的里面的两个 Native 线程,负责**读取和分发 Input 事件**。要想分析input事件的流向,需要从这里开始入手。 1. InputReader 读取 Input 事件 2. InputReader 将读取的 Input 事件放到 InboundQueue 中 3. InputDispatcher 从 InboundQueue 中取出 Input 事件派发到目标 Connection 的 OutBoundQueue(即发送给哪个Window是由InputDispatcher决定的) 4. 同时将事件记录到各个 Connection 的 WaitQueue 5. App 接收到 Input 事件,同时记录到 PendingInputEventQueue ,然后对事件进行分发处理 6. App 处理完成后,回调 InputManagerService 将负责监听的 WaitQueue 中对应的 Input 移除 ### 工具dumpsys ``` adb shell dumpsys input ``` 出现问题的界面进行滑动,同时手指保持再屏幕上,不进行抬起,进行是adb shell dumpsys input 可以看到OutboundQueue中是没有任何东西的,而WaitQueue中堆积了大量的MotionEvent(action=MOVE),此时也并没有被合并成一个。 ![](https://img.kancloud.cn/ba/48/ba48e04ccab318f8c15c1c575dae01f5_650x183.png) ## APP进程 ### UP/DOWN 与 MOVE 在每一帧的时候应用只能对一次input事件进行响应反馈。如果在一个VSYNC周期中出现了多个input事件,每次input事件到来的时候都立即分发到应用层是比较浪费资源的。为了避免浪费,就有了Batched Consumption机制,input事件会被进行批处理,然后在每个Frame渲染时发送一个batched input事件给到应用层。 ### 工具 dump ``` 使用AndroidStudio Profile查看Java调用栈 使用AndroidStudio Profile工具,选择CPU,触摸界面并进行record,dump文件之后,可以看到java层的代码调用。(AS也可以进行native调用栈的查看) ``` 出现问题的界面进行滑动,同时手指保持再屏幕上,不进行抬起,进行是adb shell dumpsys input 可以看到OutboundQueue中是没有任何东西的,而WaitQueue中堆积了大量的MotionEvent(action=MOVE),此时也并没有被合并成一个。 #### 正常情况Consume Batched MoveEvent ![](https://img.kancloud.cn/c6/9f/c69fe0e5820f5ec1d492992dbfa33f1e_3894x1836.png) #### 异常情况Consume Batched MoveEvent ![](https://img.kancloud.cn/da/cc/dacc4348d88847671273bdfa9e5e57e3_3516x1528.png) ## ViewRootImpl 从前面的java堆栈图中,我们可以看到java层是主动调用了一个doConsumeBatchedInput来进行input事件消费的。而这个doConsumeBatchedInput与两个Runnable有关ConsumeBatchedInputRunnable 和 ConsumeBatchedInputImmediatelyRunnable **ConsumeBatchedInputRunnable 和 ConsumeBatchedInputImmediatelyRunnable**ConsumeBatchedInputRunnable和ConsumeBatchedInputImmediatelyRunnable这两个是ViewRootImpl中定义的Runnable,他们都会调用到native方法nativeConsumeBatchedInputEvents读取inputChannel中的input event,前者是等到下一个Frame绘制的时候再执行input事件消费。后者如其名称immediately,是立即进行input事件的消费,**常用于一些异常场景下的事件清零操作**。 与此对应的有mConsumeBatchInputScheduled和mConsumeBatchInputImmediatelyScheduled这两个变量,来标识是否已经将对应的Runnable添加到MessageQueue里面,避免加入重复的Runnable。在对应Runnable的内部执行中又会把这个变量置为false。 ### Android11的改动 **改动点1: ViewRootImpl#scheduleConsumeBatchedInput** ![](https://img.kancloud.cn/bf/88/bf883a9632acc147efa741506c44a327_4612x424.png) 这里对ConsumeBatchedInputRunnable的添加新增了一个开关变量mConsumeBatchedImmediatelyScheduled,使得“延时消费input”和“立即消费input”变成两个互斥的操作。 **改动点2: ViewRootImpl#setWindowStopped** ![](https://img.kancloud.cn/a2/0b/a20bf417db9123f506c53c13e9f62613_3002x1260.png) setWindowStopped中新增调用一次scheduleConsumeBatchedInputImmdiately()。目的是在window切换为stopped状态后为了避免ANR,调用scheduleConsumeBatchedInputImmdiately()**立即**进行一次input事件消费 也就是在这里mConsumeBatchedInputImmediatelyScheduled这个变量被置为true,从结果上来说,这个Runnable并没有被执行! ### 排查思路 遍历父布局 针对这两次的修改,我们大胆猜测mConsumeBatchInputImmediatelyScheduled这个在置为true之后,出现了某种异常,对应的ConsumeBatchedInputImmediatelyRunnable并没有被执行,该变量并没有被置为false,导致另外一个ConsumeBatchedInputRunnable不满足执行条件,进而引发事件消费异常。Move Event没有被应用消费,导致界面无法滑动。那么我们如何进行验证呢? ViewRootImpl是框架层的类,代码层没法直接引用到,但毕竟是万view之祖,我们可以拿到DecorView,再拿到DecorView的父View来得到ViewRootImpl,进而探访这个ViewRootImpl对象。 ![](https://img.kancloud.cn/28/cf/28cfcb18621002e8652e6ddac2c4ab35_640x283.png) ### scheduleConsumeBatchedInputImmediately ![](https://img.kancloud.cn/84/0a/840a6c2bd8e3c966f423061992665790_650x136.png) View中的getHandler()为什么会是ViewRootImpl$ViewRootHandler?先看下源码中View中是怎么取到handler的。 ## 总结下滑动问题的链路流程: 1. 我们业务对一个Stop的界面A进行了列表数据的remove 2. 回到界面A,触发onStart,在Framework的ViewRootImpl会在此时,触发一次scheduleTraversals准备下一帧的界面重绘,在Android 11的版本上,还会额外调用一个ConsumeBatchedInputImmediatelyRunnable,因为scheduleTraversals会触发同步屏障,这个ConsumeBatchedInputImmediatelyRunnable并不会被立即运行,必须等到下一帧开始绘制后才可以运行 3. 绘制开始performTraversal中会调用到onMeasure,onLayout和onDraw等流程,由于我们进行了RecyclerView数据的移除,会触发到RecyclerView#onLayout,然后触发部分ItemView的onDetachedFromWindow 4. 在这个onDetachedFromWindow中我们调用了getHandler().removeCallbacksAndMessages(null),将target同为ViewRootImpl$ViewRootHandler的ConsumeBatchedInputImmediatelyRunnable从消息队列中移除。 5. 渲染结束,但是ConsumeBatchedInputImmediatelyRunnable并没有被执行,mConsumeBatchInputImmediatelyScheduled却已经被置为true,没有被重置为false 6. 触摸屏幕,底层Down事件分发正常 7. 当底层Input事件中的Move事件到来,触发了onBatchedInputEventPending,触发到scheduleConsumeBatchedInput,因为Android 11版本新增了对mConsumeBatchInputImmediatelyScheduled开关变量检测,没有往下触发流程,导致move事件没有被消费。 8. 底层Up事件正常分发,顺带将前面被阻塞的Batched Move事件上传 ## 应对方案 这个滑动问题,造成的因素有Android 11框架层的一个冗余调用,也有业务侧对View#getHandler().removeCallbacks(null)系列方法的不规范调用。我们业务已经对内部存量的View#getHandler().removeCallbacks(null)调用进行替换和移除。考虑到Android 11框架层这个冗余调用会在短期内一直存在,同时也很难保证所有开发和第三方库在此系列方法上的规范调用,我们会维持临时修复方案。 ## 参考资料 [系统级bug解决分享:腾讯开发工程师刨根问底安卓端滑动异常](https://www.163.com/dy/article/GDU0B40U0518R7MO.html) [dumpsys](https://developer.android.google.cn/studio/command-line/dumpsys) ## 系统源码 1. https://cs.android.com/android/platform/superproject**推荐,优点是可以进行搜索,速度也挺快的** 2. https://android.googlesource.com/**推荐,AOSP开源代码仓库,优点是可以查看最新的代码和提交记录**