2026/10/18 4:50:43

动态冻结屏幕触摸:AOSP Framework层输入开关实现与版本适配解析

动态冻结屏幕触摸:AOSP Framework层输入开关实现与版本适配解析 搞过安卓系统定制的人应该都遇到过这种需求教育平板要锁住屏幕防误触、儿童模式要冻结输入、售货机/门禁一体机在待机时不想让陌生触摸点亮界面、又或者只是为了在屏幕熄灭前那几百毫秒内吞掉多余的触摸。表面看都是“动态冻结触摸屏”做起来却没那么简单。应用层弹个全屏View只能挡自己的界面根本拦不住系统UI改PowerManager又容易连电源键一起锁死。真正稳妥的思路是进入Framework层在InputReader/InputDispatcher这套输入分发链路上做一个全局输入开关。这篇文章我就从AOSP不同版本的源码差异出发把动态冻结屏幕触摸的几种落地方式、核心实现逻辑和踩过的坑一次性讲清楚。文章适合两类人一类是正在做系统定制/ROM开发需要给设备加一个“输入总闸”的工程师另一类是搞自动化测试、无障碍辅助或者设备管控想从Framework层面理解触摸事件如何被掐断的开发者。读懂这篇文章不需要你提前把InputReader源码背下来但最好对Service/Binder和View分发机制有个基本概念。1. 全局输入开关的路子为什么绕不开 Framework 层1.1 应用层遮罩的问题治标不治本先说一个最常见的方案搞一个全屏透明Window塞进一个onTouchEvent直接返回true的View把所有触摸消费掉。这个方案听起来很简单实际用起来全是坑。它只能挡住普通应用遮罩窗口本身属于应用层系统UI状态栏、下拉栏、导航手势区域根本不吃这套。很多场景下用户正好就靠下拉栏去关闭飞行模式或切换Wi-Fi结果把手势区域抠出来后发现还是能操作。多窗口模式下遮罩窗口不一定在顶层。有的设备开了分屏或画中画窗口层级一乱遮挡就失效。恢复的时候要处理窗口动画、焦点切换动作稍微慢一点就露馅。遮罩窗口会吃掉触摸但它自己也会拦截按键想单独放行实体按键又得绕一层KeyEvent处理的逻辑。一句话应用层的东西永远做不出“全局”的效果。因为输入事件最先经过的是系统进程不是App进程。想在App层拦等于在快递已经送到小区门口之后再告诉门卫“这栋楼的人今天不签收”门卫管不了这么多。1.2 真正的闸门只有三处全局输入开关必须落在系统进程里。从事件源头到应用拿到触摸中间有三道可以放闸的位置InputReader层。内核从触摸屏驱动节点读到的原始事件raw event会先交到InputReader线程在这里被解析成MotionEvent。如果在这一层就把触摸类事件丢掉后面什么都不会发生。InputDispatcher层。事件从InputReader出来后会进InputDispatcher的inbound queue由它根据窗口焦点分发给对应应用。在这个队列入口处做过滤相当于在“邮件收发室”直接拒收是最彻底的“总闸”。InputManagerService/WindowManagerPolicy层。这是在Java/SystemServer侧做拦截比如PhoneWindowManager.interceptMotionBeforeQueueing。这层可以结合系统状态是否亮屏、是否在锁屏、是否来电动态拦事件但拦得不够底层有时系统窗口比Policy更早拿到事件。这三道闸门各有各的适用场景。InputReader最接近硬件过滤颗粒度最细但改动源码路径比较深InputDispatcher是最标准的总闸位置也是AOSP官方自己放“触摸冻结”逻辑的地方系统服务层适合做策略判断但要做到“全局”往往还是要把策略下发到Dispatcher执行。1.3 三个闸门的选型对比选型实现层级动态性影响范围开发成本适配版本应用层全屏遮罩App/Window动态只管当前窗口低所有版本InputFilterJava层InputDispatcher过滤动态全局可过滤指定source中4.x以来一直可用改InputReader源码Native Reader需自建开关全局原始事件高版本差异大改InputDispatcher源码Native Dispatcher需自建开关全局输入分发高版本差异大系统APIsetInputEnabled等Dispatcher动态全局所有输入低仅部分新版本我自己做过的几个项目最终都落在InputDispatcher层或InputFilter方案上。一个是因为动态性好运行时随时可以冻结/解冻另一个是因为不用维护太多跨版本源码patch。至于InputReader层一般只在极度特殊的需求下才动它比如要连触摸硬件上报的“原始坐标采样”一起关掉。2. 输入事件的一生冻结触摸该卡在哪一步2.1 从内核到App走一遍触摸事件从物理屏幕到App回调路径大概是这样的内核的触摸屏驱动在设备节点/dev/input/eventX上报input_report_abs等原始数据 →EventHub通过epoll监听这些节点并把数据包装成RawEvent→InputReader线程从EventHub读取并解析 →InputDispatcher线程把事件排队并按窗口焦点分发 →InputChannel跨进程把事件写到目标窗口 →ViewRootImpl接收后进入View分发流程。这里面有两个关键对象InputReader负责“看懂物理数据”InputDispatcher负责“找到接收者”。冻结触摸只需要让事件在其中一个环节被拦下来即可。2.2 InputReader做的事把原始电信号变成InputEventInputReader的逻辑入口在InputReader::loopOnce()内部调用EventHub::getEvents()拿到一批原始事件然后逐个处理。核心处理函数是processEventsForDeviceLocked()这个方法会找到对应的InputDevice再调用InputMapper::process()。触摸屏对应的是TouchInputMapper。它拿到RawEvent之后会把EV_ABS/EV_KEY转换成触摸点的slot、坐标、压力等数据并通过processRawTouches()维护一套多指接触状态。最终通过InputReader::process()把生成的MotionEvent交给InputDispatcher的监听回调。如果要在InputReader层做全局触摸开关本质上就是在TouchInputMapper::processRawTouches()或InputReader::processEventsForDeviceLocked()里增加一个判断如果当前冻结标志为true就丢弃这类设备的原始事件。这个做法的好处是够低层连事件解析后的合成序列都不会启动坏处是版本之间源码位置和类结构差异很大Android 10前后的InputReader.cpp文件路径都变了维护成本高。2.3 InputDispatcher做的事决定谁先看到这个事件InputDispatcher线程的核心是dispatchOnceInnerLocked()它从inbound queue取一个事件然后根据事件类型走分发分支。触摸事件会进入dispatchTouchEventLocked()这里会根据mTouchStatesByDisplay维护的触摸会话状态决定事件给哪个窗口。InputDispatcher的入口处有两个天然适合做“总闸”的位置enqueueInboundEventLocked()事件刚进队列此时丢弃影响最小。dispatchTouchEventLocked()更精细可以按display、按source、按窗口状态判断。工程上我更推荐在enqueueInboundEventLocked()入口做全局冻结判断因为事件还没进queue既不会堆积也不会污染后续的ANR监测逻辑。如果只是针对触摸需要在过滤条件里写明event-getSource() AINPUT_SOURCE_TOUCHSCREEN。2.4 冻结触摸的两个黄金介入点综合来看落地方案就两条主线不动源码的动态方案用Java层InputFilter它本质上是InputDispatcher里的一个过滤钩子官方早就预留了动态设置、动态移除不用改内核也不用改native。改源码的定制方案在InputDispatcher里维护一个自研冻结标志在enqueueInboundEventLocked()入口过滤。这个方案彻底、可控、性能最好但要做跨版本适配还要考虑把标志位通过JNI/Binder暴露给上层。另外要特别强调如果只需要“冻结触摸”最好不要对全部输入事件一刀切。按键、鼠标、手写笔可能都需要单独策略。做“全局输入开关”是另一个话题但做“冻结触摸开关”时source过滤是必须的。3. 不同AOSP版本的源码差异与踩坑点3.1 Android 9/10经典目录结构与InputFilter通用方案在Android 9含以前InputReader.cpp和InputDispatcher.cpp都平铺在frameworks/native/services/inputflinger/目录下。那时候网上能找到的改InputDispatcher增加自定义冻结标志的patch基本上都是在这个平铺目录结构里做的。到Android 10Q输入系统做了一次大重构代码被拆到inputflinger/reader/和inputflinger/dispatcher/子目录类本身也重新整理了一遍。这次重构对改源码的开发者影响很大以前一个InputDispatcher.cpp文件打个patch就能搞定的事现在可能要同时改InputDispatcher.cpp、InputDispatcher.h、InputDispatcherInterface.h还增加了InputFlinger类统一管理两个线程。但好消息是Java层的InputFilter方案在这两个版本里都是可用的。InputManagerService.setInputFilter()这个方法一直在底层会通过JNI调用到InputDispatcher的setInputFilter()。也就是说不管源码结构怎么改动态冻结触摸这件事始终有一条官方支持的路径。3.2 Android 11/12重构后的Module化与触摸冻结内部机制Android 11和12继续推进输入系统模块化。这个阶段InputDispatcher内部的状态管理也变得更复杂mTouchStatesByDisplay等结构取代了早期单一的TouchState。同时代码里保留了自带的“触摸冻结”机制。早期版本我在InputDispatcher.cpp里看到过mTouchIsFrozen之类的标志它与PowerManager的userActivity、息屏流程联动屏幕熄灭过程中系统会短暂冻结触摸防止用户在息屏动画期间产生多余输入。这套官方机制和我们要做的“动态冻结触摸”思路是一样的只是受控于系统状态而不是我们自己的业务开关。做系统定制时完全可以模仿这套逻辑在Dispatcher里增加一个可外部控制的冻结标志。在这个阶段改源码还要注意不要直接丢弃事件。Android 11之后的事件分发对触摸序列完整性要求更高随便丢一个DOWN事件后面的UP会因为找不到触摸会话而变成一堆“幽灵事件”甚至导致某些App直接卡死。官方做法是冻结时先对当前活动触摸会话发送CANCEL事件再停止接收新事件。3.3 Android 13/14官方输入开关与系统能力收敛到Android 13/14输入系统能力进一步收敛系统级的“输入开关”概念被更明确地提出来。部分新版本InputDispatcher提供类似setInputEnabled的能力Java侧InputManagerService也有对应入口不同分支命名可能有差异需要自己翻源码确认。这个开关一旦关闭基本所有输入事件都会被挡在分发之外。但它有两个问题需要注意它不只管触摸而是所有输入。如果只需要冻结触摸屏直接关闭这个总闸会把键盘、鼠标、手写笔一起关掉。兼容性不一致。AOSP分支很多各家厂商在Android 12/13/14上的代码并不完全同步同一个API在不同厂商系统上可能行为不同。所以要在具体分支上验证不要想当然直接套用。我自己在Android 13源码分支中翻到过这类接口但在Android 12的另一个分支里就没有完全一致的实现。这就是“不同Aosp版本”最让人头疼的地方。3.4 版本演进与代码路径速查表Android版本InputReader/InputDispatcher路径动态冻结推荐方案注意事项8/9frameworks/native/services/inputflinger/InputFilter通用改Dispatcher源码老patch可直接参考目录平铺10inputflinger/reader/inputflinger/dispatcher/InputFilter改动Dispatcher需要适配新目录结构重构patch容易冲突11/12同上Dispatcher内部结构更复杂InputFilter模仿官方冻结逻辑加自定义标志触摸序列完整性要求更高别随便drop事件13/14继续收敛输入模块化InputFilter某些版本可利用系统输入开关系统开关影响全局确认版本API差异4. 三种可落地的动态冻结实现附关键代码4.1 方案AJava层InputFilter动态过滤这是我最常用的方案。写一个SystemServer内部服务或一个系统级App通过InputManager.getInstance().setInputFilter()动态设置过滤器在filterInputEvent()回调里判断source并返回false即可丢弃触摸事件。先看完整代码public class TouchFreezeController { private final Handler mHandler; private InputFilter mInputFilter; private volatile boolean mFrozen; public TouchFreezeController(Looper looper) { mHandler new Handler(looper); } public void start() { if (mInputFilter null) { mInputFilter new InputFilter(mHandler.getLooper()) { Override public boolean filterInputEvent(InputEvent event, int policyFlags) { if (!mFrozen) { return true; } // 只冻结触摸屏不影响其他事件 if (event instanceof MotionEvent (event.getSource() InputDevice.SOURCE_TOUCHSCREEN) ! 0) { return false; // 丢弃 } return true; } }; } InputManager.getInstance().setInputFilter(mInputFilter); } public void setFrozen(boolean frozen) { mFrozen frozen; } public void stop() { InputManager.getInstance().setInputFilter(null); } }几个关键点InputFilter构造必须传一个Looper回调就跑在这个Looper上。有些文章写成无参构造那是错的。filterInputEvent()运行在InputDispatcher关键路径上绝对不能做耗时操作、不能碰Binder锁、不能做网络请求。InputManager.getInstance()拿到的实例在系统进程里和非系统进程里行为不同。最好把这个逻辑放到SystemServer进程内或者通过一个持有系统签名权限的系统App调用。恢复时setFrozen(false)不会自动清除积压事件。如果冻结期间用户一直在戳屏幕解冻后的短暂时间可能会有事件爆发通常我会做200~300ms的“软恢复”即解冻后的一段时间过滤MOVE事件只放行新的DOWN。InputFilter方案的优势是跨版本稳定Android 4.x到今天都能跑劣势是它毕竟是在Java层的过滤器极端高频率的触摸事件下会有一个事件从native到java再回到native的往返虽然影响很小但追求极致性能时还是不如纯native方案。4.2 方案B改Native InputDispatcher做一个真正可控的冻结点如果要做到最彻底、最快那就只能进源码改造。以Android 12/13的InputDispatcher为例核心思路是在InputDispatcher.h中增加一个冻结标志并提供设置方法然后在enqueueInboundEventLocked()入口判断。InputDispatcher.h中增加成员public: // 自研触摸冻结开关 void setTouchFrozen(bool frozen); private: bool mTouchFrozen false;InputDispatcher.cpp中实现设置方法void InputDispatcher::setTouchFrozen(bool frozen) { std::lock_guardstd::mutex lock(mLock); if (mTouchFrozen frozen) { return; } mTouchFrozen frozen; if (frozen) { // 关键如果有正在进行的触摸会话先向当前窗口派发CANCEL // 否则触摸序列会被拦腰截断App永远不会收到UP synthesizeCancelForActiveTouchLocked(); } }然后在事件入队的地方加过滤void InputDispatcher::enqueueInboundEventLocked(InputEventEntry entry) { if (mTouchFrozen) { const InputEvent* event entry.event.get(); if (event-getType() AINPUT_EVENT_TYPE_MOTION (event-getSource() AINPUT_SOURCE_TOUCHSCREEN) ! 0) { return; // 直接丢弃事件不入队 } } // ... 原逻辑 }这里有一个很重要的细节不能只丢弃。如果当前正有一根手指放在屏幕上触摸状态机已经建立了一个会话DOWN已派发此时直接丢弃后续MOVE/UPApp永远等不到UP整个触摸流程就卡死了。所以冻结时先派发一次CANCEL告诉App“当前触摸会话被系统中断了”然后再丢弃后续事件。这样App虽然体验上会中断但状态不会坏。接下来要把这个能力暴露出去。需要改三个地方com_android_server_input_InputManagerService.cpp中增加JNI方法nativeSetTouchFrozen。InputManagerService.java中增加同名方法加hide注解。如果要从另一个进程控制需要再走一层Binder或者SystemServer内部接口。这个方案最大的问题不是实现难度而是跨版本适配。Android 10把InputDispatcher搬到新目录Android 11改了触摸状态结构Android 13又加了一堆输入模块化的壳。同一个patch从Android 10伐到Android 13基本不可能不冲突。我自己的习惯是把自定义代码单独抽diff文件每升级一个版本就重新rebase一次并把所有改动控制在极少的几个文件中。4.3 方案C直接用输入的“总闸”接口在一些较新的AOSP分支里系统已经提供了输入总闸能力。Java层入口通常在InputManagerService里方法名类似setInputEnabled(boolean)native层由InputDispatcher实现。关闭后InputDispatcher不再接受任何新的输入事件。假如你所在的分支确认有这个方法调用逻辑非常简洁InputManager im InputManager.getInstance(); // 冻结所有输入 im.setInputEnabled(false); // 恢复 im.setInputEnabled(true);但这里必须提醒这个开关影响的是全部输入触摸/鼠标/键盘/手写笔一起关不是标题里的“冻结屏幕触摸”。千万别在系统还没完全起来的时候关总闸否则SystemServer自己都收不到触摸想再开都没入口。各家厂商对这个API的保留程度不一样有的把它包成“访客模式”开关有的根本没接。实际使用前一定要在该设备分支上验证。所以只有当你确实需要“全局输入开关”而不是“仅触摸冻结”时这个方案才是首选。4.4 如何把冻结点暴露出给上层App/服务不管用哪种方案要实现业务化都要让“冻结/解冻”可控。通常有几种做法SystemServer内置服务在SystemServer里启动一个内部服务通过自定义AIDL接口暴露给系统App。服务内部持有TouchFreezeController或直接调用InputManagerService的冻结方法。Broadcast/命令控制用一个系统广播或cmd命令做调试开关。比如adb shell cmd touch_freeze enable方便在工厂测试阶段使用。SystemUI快捷入口把开关做成QuickSettings磁贴适合设备管理类场景需要改SystemUI。我比较推荐AIDL方案。它干净、可控、可跨应用复用而且方便加权限。实现时记得给AIDL方法加上// RequiresPermission之类的权限声明或者做系统签名校验避免普通App随便控制系统触觉。5. 实战中绕不开的坑排查记录5.1 触摸序列不能拦腰截断这是我在方案B上踩过最惨的坑。第一版代码直接在dispatchTouchEventLocked()里发现冻结标志就return结果一个ACTION_DOWN已经发出去了App还没收到ACTION_UP整个触摸状态卡死。再恢复冻结时屏幕怎么点都没反应。排查了半天问题是事件序列的完整性被破坏了。触摸事件不是单个独立事件DOWN、MOVE、UP是一套序列。要么在DOWN之前拦截要么在冻结那一刻主动发CANCEL结束当前会话。没有第三种选择。后来我把冻结逻辑改成两类冻结时存在活动触摸则先派发CANCEL再丢弃后续事件。冻结时没有任何触摸直接丢弃所有后续触摸事件。这样App在冻结期间最多收到一个CANCEL不会一直卡在“按下”状态。5.2 事件残留与恢复瞬间误触冻结一段时间后如果用户的手指一直没离开屏幕EventHub缓冲区里会积累一些后续的move事件。解冻之后这些事件可能会在一个极短时间内涌进来导致App突然收到一串连贯的MOVE甚至误点某个按钮。后来我加了一个“解冻缓冲期”时间约300ms。在缓冲期内不放行MOVE事件只放行新的DOWN。因为解冻时用户已经意识到要操作了真正的操作一定是以一个新的DOWN开始的。这个方案在几个项目里实测下来都很稳也不会让用户觉得触摸失效。5.3 source过滤漏掉手写笔一开始做source判断时只检查了SOURCE_TOUCHSCREEN。后来测试发现手写笔事件仍然能被看到。原因是许多手写笔设备上报的source同时包含SOURCE_STYLUS和SOURCE_TOUCHSCREEN。如果业务要求连手写笔一起冻结直接用SOURCE_TOUCHSCREEN判断是不全的。反过来如果只想“冻手指、放行手写笔”判断逻辑就要改成包含SOURCE_TOUCHSCREEN但不包含SOURCE_STYLUS时才拦截。这类细节不实测完全发现不了。5.4 系统窗口绕过冻结点有客户反馈冻结触摸后状态栏下拉还是能拉下来。定位后发现是策略放在了WindowManagerPolicy层而状态栏窗口的触摸事件在部分版本上是走独立通道的Policy没拦住。这个教训也让我确定了真正全局的冻结点必须放到InputDispatcher层。只有在那里才能对进入系统的所有触摸事件一视同仁。想用Java层Policy实现全局屏蔽基本都会有漏洞。5.5 冻结标志的线程安全问题在方案B中冻结标志会被SystemServer调用线程修改同时被InputDispatcher线程读取。如果不加锁或者不用atomic会出现设置冻结后过几十毫秒才生效的诡异现象。正确做法是用std::atomicbool配合mLock一起用。设置状态时先用mLock保护读取时用atomic保证可见性。我用过std::atomicbool后这个随机延迟的问题彻底消失了。5.6 用dumpsys和systrace排查问题冻结触摸之后排查事件有没有真的被丢弃我一般分两步dumpsys input旧版本或dumpsys inputflinger看版本查看InputDispatcher状态重点看inbound queue是否有触摸事件堆积。systrace抓取input和InputDispatcher相关sections看事件是在哪一步被吞掉的。如果看到事件在enqueueInboundEventLocked里就被return说明冻结逻辑生效了如果事件已经派发到窗口但App没响应那问题就在App或View分发层跟我们的冻结逻辑无关。收尾一点实际体会这几个方案里我实际项目最常用的是InputFilter Dispatcher自定义标志的组合。Java层InputFilter用来快速迭代、做上层动态策略Native层自定义冻结标志作为底层保底处理极端高频触摸和系统UI事件。每个版本升级时最先检查的就是InputDispatcher代码有没有被重构影响。另一个经验是凡是做冻结触摸一定要先在真机上反复测触摸序列完整性。工程师在电脑前看代码永远看不出问题只有把手指按在屏幕上不停解冻、冻结才能感受到哪些App会卡死、哪些窗口会漏事件。这个事看起来简单实际上细节非常考验细心程度。希望这篇文章能帮你少走几趟弯路。