[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开源代码仓库,优点是可以查看最新的代码和提交记录**
- Android
- 四大组件
- Activity
- Fragment
- Service
- 序列化
- Handler
- Hander介绍
- MessageQueue详细
- 启动流程
- 系统启动流程
- 应用启动流程
- Activity启动流程
- View
- view绘制
- view事件传递
- choreographer
- LayoutInflater
- UI渲染概念
- Binder
- Binder原理
- Binder最大数据
- Binder小结
- Android组件
- ListView原理
- RecyclerView原理
- SharePreferences
- AsyncTask
- Sqlite
- SQLCipher加密
- 迁移与修复
- Sqlite内核
- Sqlite优化v2
- sqlite索引
- sqlite之wal
- sqlite之锁机制
- 网络
- 基础
- TCP
- HTTP
- HTTP1.1
- HTTP2.0
- HTTPS
- HTTP3.0
- HTTP进化图
- HTTP小结
- 实践
- 网络优化
- Json
- ProtoBuffer
- 断点续传
- 性能
- 卡顿
- 卡顿监控
- ANR
- ANR监控
- 内存
- 内存问题与优化
- 图片内存优化
- 线下内存监控
- 线上内存监控
- 启动优化
- 死锁监控
- 崩溃监控
- 包体积优化
- UI渲染优化
- UI常规优化
- I/O监控
- 电量监控
- 第三方框架
- 网络框架
- Volley
- Okhttp
- 网络框架n问
- OkHttp原理N问
- 设计模式
- EventBus
- Rxjava
- 图片
- ImageWoker
- Gilde的优化
- APT
- 依赖注入
- APT
- ARouter
- ButterKnife
- MMKV
- Jetpack
- 协程
- MVI
- Startup
- DataBinder
- 黑科技
- hook
- 运行期Java-hook技术
- 编译期hook
- ASM
- Transform增量编译
- 运行期Native-hook技术
- 热修复
- 插件化
- AAB
- Shadow
- 虚拟机
- 其他
- UI自动化
- JavaParser
- Android Line
- 编译
- 疑难杂症
- Android11滑动异常
- 方案
- 工业化
- 模块化
- 隐私合规
- 动态化
- 项目管理
- 业务启动优化
- 业务架构设计
- 性能优化case
- 性能优化-排查思路
- 性能优化-现有方案
- 登录
- 搜索
- C++
- NDK入门
- 跨平台
- H5
- Flutter
- Flutter 性能优化
- 数据跨平台