
相信不少做Android开发的兄弟都遇到过这种场景界面明明显示正常点击按钮却毫无反应或者软键盘弹出来把界面顶得一塌糊涂怎么设置都不对再或者Dialog关闭之后原本页面的焦点彻底“丢了”按键事件处理全部失灵。这些问题十有八九都跟同一个底层概念相关——窗口焦点。这篇内容就是基于“android 窗口焦点介绍”这个方向把我在实际项目里和窗口焦点打交道的经验掰开揉碎讲清楚。适合刚接触Android Framework的初学者也适合被焦点问题折磨过的应用层开发同学。我把概念、源码逻辑、踩坑案例和排查手段串起来讲争取让你看完之后能自己定位问题而不是碰到焦点bug就两眼一抹黑。1. 窗口焦点的本质它到底在管什么事先别急着看源码先想明白一个最基本的问题窗口焦点Window Focus在Android系统里到底扮演什么角色我的理解是它就是系统在任意时刻对“哪个窗口应该接收输入事件”这件事的最终裁决结果。不管是触摸屏上的点按滑动、键盘的按键输入还是IME输入法的弹出和隐藏系统都要先确定当前“焦点窗口”是谁然后才能把事件准确分发过去。1.1 焦点不是Activity也不是View这里有一个最常见的认知误区很多新手把Activity的焦点和窗口焦点混为一谈。实际上在Android的窗口体系里Activity只是一个应用层的容器真正跟系统窗口管理器对接的是Activity对应的PhoneWindow而PhoneWindow内部又包含了DecorView这个View树根节点。系统层面管理的是Window不是Activity。窗口焦点管理在WindowManagerService以下简称WMS里进行它维护了一个窗口列表每个窗口都有自己的状态、类型和z序。WMS要做的核心工作之一就是从所有窗口中挑选出一个“焦点窗口”然后把输入事件投递给它。// WindowManagerService 中的一个核心字段 WindowState mCurrentFocus null; // 当前持有焦点的窗口 WindowState mFocusedApp null; // 当前处于 resumed 状态的 Activity 对应的窗口这两个字段很重要mCurrentFocus指向的是真正获得输入焦点的窗口它不一定是mFocusedApp。比如你弹了一个DialogDialog所在的窗口就会抢夺mCurrentFocus但是mFocusedApp仍然指向后台那个Activity的窗口。理解了这两者的区别后面好多问题就都好解释了。1.2 焦点在输入事件分发链路中的位置顺着输入事件的流动方向看。触摸事件从内核的InputReader读出来之后经过InputDispatcher进行分发。InputDispatcher在分发的时候需要知道当前屏幕上的触摸事件应该投递给哪个窗口这时候它就会去WMS查询当前的焦点窗口。触摸事件 → InputReader → InputDispatcher → WindowManagerService查询焦点窗口 → 目标窗口 → ViewRootImpl → DecorView → 业务View这个链路里有一个关键的细节InputDispatcher只认窗口它有自己的一套焦点窗口概念。在较新的Android版本里InputDispatcher内部有一个mFocusedWindowHandle这个句柄指向的就是WMS告知它的焦点窗口。WMS在焦点窗口切换的时候会调用InputManagerService的setFocusedWindow方法把最新的焦点窗口同步给InputDispatcher。所以窗口焦点的切换会直接影响输入事件的分发目标。如果你的自定义窗口没有正确获取焦点那触摸事件根本送不到你的View树上这时候在View里加再多的OnTouchListener也没用。1.3 焦点与窗口类型的关系窗口类型Window Type在焦点分配里起到决定性作用。Android的窗口类型大致可以分成三大类应用窗口FIRST_APPLICATION_WINDOW到LAST_APPLICATION_WINDOW范围比如Activity的主窗口、Dialog窗口、PopupWindow的窗口。子窗口FIRST_SUB_WINDOW到LAST_SUB_WINDOW比如PopupMenu的菜单窗口它们依附于父窗口存在。系统窗口FIRST_SYSTEM_WINDOW之后比如状态栏、输入法窗口、来电悬浮窗、Toast等。正常情况下系统窗口的z序高于应用窗口所以当系统窗口显示的时候比如输入法窗口弹出焦点往往会落在输入法窗口上这也就是为什么输入法能立刻响应你的键盘输入。而应用窗口里后添加的窗口会覆盖先添加的所以后弹出的Dialog默认会拿到焦点。2. 焦点到底是怎么被选中的WMS内部的焦点管理机制搞清楚了焦点是什么接下来我们看看WMS内部到底是怎么做选择的。这部分涉及源码逻辑我尽量讲得通俗一点但关键的判断条件和流程必须讲透。2.1 WMS的窗口列表与焦点遍历逻辑WMS内部维护了一个mWindowMap这是一个以IBinder为key的HashMap每个WindowState代表一个窗口都存放在里面。同时还有一个mWindows列表这个列表按z序从底到顶排列所有窗口。每当窗口的添加、删除、可见性、焦点状态发生变化时WMS都会重新计算焦点窗口。核心方法在WindowManagerService.updateFocusedWindowLocked它的大致逻辑是从z序最高的窗口开始向下遍历。依次检查每个窗口是否满足“可以作为焦点窗口”的条件。找到符合条件的窗口后把它设置为新的焦点窗口。如果遍历完所有窗口都没有找到就返回null表示没有窗口可接收焦点。这个方法返回的是一个WindowState但它并不是简单的“从顶往下第一个非null窗口”中间还有很多筛选逻辑。// WindowManagerService.updateFocusedWindowLocked 的简化逻辑 boolean updateFocusedWindowLocked(...) { WindowState newFocus null; // 从 z 序最高的窗口开始遍历 for (int i mWindows.size() - 1; i 0; i--) { WindowState win mWindows.get(i); if (canBeImeTarget(win)) { // 输入法焦点特殊判断 ... } if (win.canReceiveKeys()) { // 核心判断能否接收按键事件 newFocus win; break; } } ... }这里最关键的就是canReceiveKeys()方法它决定了一个窗口有没有资格成为焦点窗口。稍微翻一下这个方法里面主要检查了几个维度mViewVisibility View.VISIBLE窗口的View是可见状态。mAttachedHidden false窗口没有被隐藏。mRemoved false窗口没有被移除。mWindowRemovalAllowed和mHasSurface窗口的Surface已经创建并且可以被显示。一个窗口即使设置了FLAG_NOT_FOCUSABLE也并不会退出这个遍历逻辑而是会在更早的地方被过滤掉。可以说canReceiveKeys是窗口能否获得焦点的“最后一道大闸”。2.2 FLAG_NOT_FOCUSABLE和FLAG_ALT_FOCUSABLE_IM两个最常用的焦点flag这一节是应用层开发最需要记牢的内容。在WindowManager.LayoutParams里跟焦点强相关的flag有两组用好了能解决不少实际问题。第一组FLAG_NOT_FOCUSABLE这个flag的意思是“本窗口不需要接收按键类输入焦点”设置之后窗口不会获得焦点。但它有一个非常重要的副作用——窗口变成touchable但not focusable也就是说触摸事件依然可以点中窗口里的控件但窗口不进入焦点状态。举例来说WindowManager.LayoutParams params new WindowManager.LayoutParams(); params.flags | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; mWindowManager.addView(view, params);加了这样一个flag的悬浮窗就永远不会抢走当前Activity的焦点而它的触摸事件依然能正常工作。这正是很多悬浮球、Toast、音乐播放控制条这类悬浮窗的常规做法。否则每次弹一个悬浮窗底层Activity就失去焦点软键盘也跟着退掉体验会非常糟糕。第二组FLAG_ALT_FOCUSABLE_IM这个flag是配合FLAG_NOT_FOCUSABLE使用的用来规定窗口与输入法IME的交互方式。它的作用机制比较隐晦我直接在代码层面解释// WindowState.canReceiveKeys 中与输入法相关的逻辑简化 final boolean notFocusable (winAttrs.flags FLAG_NOT_FOCUSABLE) FLAG_NOT_FOCUSABLE; final boolean altFocusableIm (winAttrs.flags FLAG_ALT_FOCUSABLE_IM) FLAG_ALT_FOCUSABLE_IM; final boolean canReceiveKeys !notFocusable || altFocusableIm;所以如果只设置FLAG_NOT_FOCUSABLEcanReceiveKeys返回false窗口拿不到焦点输入法也就不会因为窗口的状态而弹出。但如果同时设置FLAG_NOT_FOCUSABLE | FLAG_ALT_FOCUSABLE_IMcanReceiveKeys就变成true窗口又具备接收焦点的资格了。这个组合有一种很经典的应用场景编辑框所在窗口需要触摸焦点又不想弹出系统软键盘。比如自定义键盘的输入框或者是扫码枪输入框这时候窗口既需要保持焦点来接收物理键盘输入又不能用IME。设置上面那个组合就能做到焦点照常、软键盘不弹。2.3 输入法窗口的特殊焦点接管输入法窗口在焦点体系里属于“特等公民”。当输入法弹出时它会申请一个TYPE_INPUT_METHOD类型的窗口这个窗口的z序非常高直接压在普通应用窗口之上。而且IME窗口几乎总是会获取焦点因为它需要接收用户的软键盘敲击事件。WMS在处理输入法窗口时有一个单独的mInputMethodTarget字段这个字段记录的是当前哪个窗口与输入法绑定即窗口里有可编辑的输入框。mInputMethodTarget不一定是焦点窗口但通常情况下它和焦点窗口是同一个。如果应用的某个窗口里包含EditText同时这个窗口拿到焦点系统就会自动把mInputMethodTarget指向该窗口然后唤起输入法。这也意味着如果你的业务窗口抢了焦点但内部没有任何可编辑控件输入法不会弹出但底层窗口的焦点状态已经被打断了。很多“软键盘弹不出来”的问题本质上不是输入法的锅而是焦点被某个透明窗口或非焦点窗口截胡了。3. 开发者最容易踩的坑onWindowFocusChanged与onResume的相爱相杀焦点机制落实到应用层最直观的体现就是Activity的onWindowFocusChanged回调。这个回调是排查界面焦点问题时最常用的入口但它和onResume的执行时机、含义差异常常让人误判问题。3.1 onWindowFocusChanged到底什么时候回调onWindowFocusChanged(boolean hasFocus)是Activity/Fragment里的一个回调它会在这个Activity对应的窗口获得或失去焦点时被调用。但要注意窗口焦点不等于可见性也不完全等于Resume状态。举个例子。两个Activity A和BA在栈顶此时A处于Resumed状态并且窗口有焦点。你从A启动B非全屏透明主题B绘制完成并显示出来。这时候A的onPause会调用A的窗口会失去焦点回调onWindowFocusChanged(false)B的窗口获得焦点回调onWindowFocusChanged(true)。但A仍然可能是可见的如果B不是完全不透明只是它已经不是焦点窗口了。这个场景在Android 10以后尤其明显因为系统对“可见但无焦点”和“不可见”做了更细的区分。很多开发者以为onPause之后界面就看不到了但实际上在分屏、画中画、透明Activity场景下onPause和窗口焦点丢失可以分开发生。3.2 诡异案例onResume里拿不到焦点导致的布局错乱我印象最深的一个真实bug是这样的在一个视频播放页面我在onResume里根据窗口焦点状态去刷新播放器的控制栏布局用了一个判断——如果窗口没有焦点就暂停视频。结果视频总是在从后台返回前台的时候卡一下但不是每次都卡非常偶发。后来排查发现根因是onResume的时机和onWindowFocusChanged(true)的时机并不完全对齐。在从后台回到前台的场景中系统先把Activity恢复为Resumed状态然后再去更新窗口焦点。也就是说onResume调用的时候onWindowFocusChanged(true)还没有回调窗口焦点仍然在之前的窗口上导致我的判断逻辑走了错误分支。我用了一段代码来验证这个时序Override protected void onResume() { super.onResume(); Log.d(TAG, onResume called, hasWindowFocus hasWindowFocus()); } Override public void onWindowFocusChanged(boolean hasFocus) { super.onWindowFocusChanged(hasFocus); Log.d(TAG, onWindowFocusChanged: hasFocus hasFocus); }典型输出是onResume called, hasWindowFocusfalse onWindowFocusChanged: hasFocustrue所以这里的经验是不要在onResume里依赖hasWindowFocus()的结果来判断焦点状态除非你能保证当前场景下回调顺序是可靠且符合预期的。更稳妥的做法是针对onWindowFocusChanged本身做状态记录在这个回调里统一处理焦点相关逻辑。3.3 软键盘的显隐性焦点和IME的联动陷阱软键盘的弹出条件其实比大多数人想得要“严格”不仅仅要求窗口有焦点还要求焦点的落点是一个可编辑的View通常是EditText并且窗口没有设置FLAG_ALT_FOCUSABLE_IM来禁止IME。三个条件缺一个软键盘都不会弹。实际开发里常遇到一个问题界面里有几个EditText进入页面时自动弹出软键盘。很多人的第一反应是直接请求焦点editText.requestFocus(); InputMethodManager imm (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); imm.showSoftInput(editText, InputMethodManager.SHOW_IMPLICIT);但实测有时候有效有时候无效。无效的常见原因是窗口还没有获得焦点的时候EditText拿到的是View焦点但窗口焦点还没就绪。showSoftInput要求目标View所在窗口必须有窗口焦点否则调用会被系统悄悄忽略。正确做法是等窗口焦点就绪之后再弹。监听onWindowFocusChanged在回调为true的时候再去请求焦点和弹出软键盘。或者在View.post里延迟到窗口绘制完成之后再操作。editText.postDelayed(new Runnable() { Override public void run() { editText.requestFocus(); InputMethodManager imm (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); imm.showSoftInput(editText, InputMethodManager.SHOW_IMPLICIT); } }, 100);postDelayed这种方式虽然能快糙猛解决问题但具体延迟多少毫秒是个经验值。如果窗口创建链路比较慢这个值设置得太小依然会失败。后来我干脆封装了一个工具类维护一个WindowFocusListener在焦点真正到达时弹键盘稳稳当当。4. 焦点丢失的坑位地图实战中那些莫名的焦点问题说实话窗口焦点这块真正折磨人的不是正常流程而是各种边界场景下的“灵异事件”。我把这些年遇到过的典型坑位整理成了一张表每个问题后面附上我的排查思路和最终解法这套方法论比单纯背代码有用得多。问题现象真正原因排查思路我的最终解法Dialog关闭后底层页面按键事件失灵Dialog窗口退出时焦点没有正确还回给原窗口查看WMS的焦点窗口指向检查Dialog的dismiss时机确保通过正常生命周期关闭悬浮窗弹出后软键盘立刻收起悬浮窗抢了窗口焦点判断悬浮窗是否携带FLAG_NOT_FOCUSABLE给悬浮窗加FLAG_NOT_FOCUSABLEEditText点击后软键盘闪一下就消失输入法老窗口退出和新窗口重入交替抢焦点查看dumpsys input窗口列表给窗口加FLAG_ALT_FOCUSABLE_IM并按需控制IME显隐Activity被透明主题覆盖后底层动画停止透明窗口拿走了窗口焦点确认透明窗口的属性设置判断是否需要焦点不需要则设置FLAG_NOT_FOCUSABLESurfaceView上叠加控件无法点击悬浮窗虽然是“最顶层”但触摸判断的窗口被SurfaceView覆盖结合窗口z序和触摸命中判断检查窗口的FLAG_NOT_TOUCH_MODAL和相关触摸区域设置这张表里的问题有一个共同特点应用代码逻辑看起来完全正常但系统行为就是不对劲。为什么呢因为窗口焦点问题大概率不是在业务逻辑层出错而是在WindowManager的窗口属性配置上出错。4.1 排查焦点问题的第一步dumpsys window不管问题现象多奇怪第一步永远是先看系统当前的真实状态而不是猜测。Android系统提供了一个很实用的调试命令adb shell dumpsys window windows这个命令会输出当前窗口管理器里所有窗口的详细状态包括每个窗口的包名、窗口类型、可见性、焦点状态等。我重点关注这几个字段mCurrentFocus当前系统认为的焦点窗口。mFocusedApp当前resumed状态的应用窗口。mObscuringWindow当前遮挡住其他窗口的窗口。每个WindowState后面的mViewVisibility、mHasSurface、mGivenFlags。通常看完这个输出80%的焦点问题都能定位到具体是哪个窗口在捣乱。比如你本来以为自己的Activity窗口应该持有焦点结果看到mCurrentFocus指向了一个InputMethod窗口或者一个包名奇怪的悬浮窗那问题就清楚了。4.2 实例复盘一个点击无响应的ListView表项这里复盘一个让我印象深刻的线上问题。用户反馈在某个页面点击ListView的item没有响应但页面上方的按钮能正常点击。第一时间想到的是焦点问题吗当时不是我先怀疑触摸事件被拦截了于是用dumpsys input看了一下触摸事件的分发链路。dumpsys input的输出里有一个FocusedWindow字段我看到它的值时愣了一下——它指向的是另一个应用的一个悬浮窗窗口不是当前Activity。这个悬浮窗是另一个SDK加的窗口类型是TYPE_APPLICATION_OVERLAY并且没有设置FLAG_NOT_FOCUSABLE。这个窗口覆盖在Activity上方把焦点全部抢走了。但它并不是全屏的而是只有一小块区域所以视觉上当前Activity依然完整可见。可是窗口焦点被抢以后ListView的item点击事件虽然能hit test到Activity但InputDispatcher在判断事件目标时优先看焦点窗口于是事件就发给悬浮窗了悬浮窗又没法处理最终表现为“点击无响应”。解决办法非常直接给那个悬浮窗加上FLAG_NOT_FOCUSABLE。加上之后窗口还能正常触摸但不会成为焦点窗口底层Activity的焦点和事件分发就恢复正常了。4.3 使用FLAG_NOT_TOUCH_MODAL来控制触摸区域还有一个容易混淆的flag是FLAG_NOT_TOUCH_MODAL它跟焦点没有直接关系但常常被误用来解决焦点问题。这个flag的含义是窗口可以接收自己边界内的触摸事件边界外的触摸事件不再被拦截而是传递给底层的窗口。很多人在做悬浮窗时为了让触摸事件可以穿透悬浮窗的空白区域给窗口加了FLAG_NOT_TOUCH_MODAL。但要注意这不是处理焦点的正确手段。穿透触摸和焦点分配是两个不同维度的机制——触摸事件是显式命中窗口焦点是一个全局状态。你可以在悬浮窗不抢焦点的前提下正常接收自己区域内的触摸事件。所以一个正确的悬浮窗配置通常是这样的WindowManager.LayoutParams params new WindowManager.LayoutParams( WindowManager.LayoutParams.WRAP_CONTENT, WindowManager.LayoutParams.WRAP_CONTENT, WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY, WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL, PixelFormat.TRANSLUCENT );在Android 8.0以后悬浮窗类型必须用TYPE_APPLICATION_OVERLAY这个类型本身就避免了很多历史遗留的焦点问题但仍然要显式声明FLAG_NOT_FOCUSABLE否则依然会干扰焦点。5. 多窗口模式下的焦点切换分屏与画中画才不会教你的潜规则到这里单窗口的场景讲得差不多了。但是现在的Android设备上分屏、画中画PiP、多任务切换已经是常态功能窗口焦点在这些场景下的行为跟单窗口完全不同。如果开发的应用需要适配这些场景就一定要知道下面的潜规则。5.1 分屏模式下的焦点归属分屏模式下屏幕上同时显示两个Activity窗口一上一下或者一左一右。但系统始终只有一个“焦点窗口”用户点击哪个窗口区域焦点就切换给哪个窗口。这个切换由WMS根据输入事件自动完成对应用层来说是透明的。这带来一个直接的影响不被用户点击的那个分屏窗口会失去窗口焦点。即使它依然处于可见状态它的onWindowFocusChanged(false)也会被调用。如果你的应用在窗口获得焦点时才做某些刷新操作那分屏状态下另外一侧的窗口就不会刷新。我遇到过的一个实际问题是分屏聊天的场景里上面的Activity有一个自动滚动到最新消息的逻辑这个逻辑在onWindowFocusChanged(true)里触发但用户点在下方窗口发消息时上方窗口的消息列表就停住不动了直到再次点上去才恢复。后来我把自动滚动的触发条件从“窗口获得焦点”改成了“数据更新时判断自身是否可见”问题就解决了。所以记住这个原则窗口焦点是独占的可见性和焦点是两个完全独立的状态。在分屏、PiP、多窗口场景下你的界面可能“看得见但没焦点”也可能“有焦点但不可见”比如被完全遮挡的Activity其实早就停止更新了。5.2 画中画窗口的焦点行为画中画模式更特殊。进入PiP之后Activity的窗口会被系统重新调整为一个小的悬浮窗但它的窗口拥有一个非常特殊的焦点状态PiP窗口通常不接收焦点。在AOSP的实现里PiP窗口的WindowState有一个FLAG_NOT_FOCUSABLE标志位这是系统动态加上的。也就是说即使PiP窗口在屏幕上可见它也不会成为焦点窗口按键事件会继续传递给PiP下面的主窗口。这就解释了为什么PiP播放视频时系统的媒体音量键能够控制播放器的音量——因为焦点窗口不是PiP窗口本身而是底下那个你可能都看不见的Activity。同样你在PiP窗口上做点按操作事件能到达PiP窗口的原因也不是它拿到了焦点而是触摸事件的hit test命中了它的Surface区域。// PiP窗口在进入画中画模式时系统会设置这几个flag WindowManager.LayoutParams params mWindow.getAttributes(); params.flags | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; params.flags | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL;5.3 请求焦点与释放焦点的最佳实践聊了这么多坑最后给几条关于“主动请求焦点”和“主动让出焦点”的建议。这些都是我在项目里逐步总结出来的流程不一定适用于所有场景但至少能避开大部分雷区。应用主动请求窗口焦点的推荐路径是先确认自己的窗口已经添加到WMS中onAttachedToWindow已回调。确认窗口不是FLAG_NOT_FOCUSABLE状态。对于Activity直接用getWindow().getDecorView()的requestFocus()方法来请求View焦点这会间接影响窗口焦点的状态。对于需要软键盘弹出的场景在窗口焦点稳定之后再显示IME。窗口主动释放焦点的推荐做法是如果窗口不再需要接收输入比如全屏播放视频时可以给窗口加FLAG_NOT_FOCUSABLE让焦点回到底层窗口。调用clearFocus()只能清除View焦点并不能直接改变窗口焦点状态。真正让出窗口焦点还是要靠flag变化来触发WMS重新计算。不要在一个窗口销毁的瞬间依赖另一个窗口自动获得焦点。系统窗口焦点重算需要经过一个短暂的过程如果这时候进行敏感操作可能会有竞态。另外还有一个很实用的技巧监听Window的onWindowFocusChanged不如监听DecorView的onWindowFocusChanged来得及时。因为Activity的onWindowFocusChanged要经过ActivityThread的消息队列处理而View的onWindowFocusChanged在ViewRootImpl分发窗口焦点事件时就会触发。对于性能敏感的场景用View级别回调能更早拿到焦点状态。6. 焦点状态观察与调试工具箱最后这部分我把平时调试窗口焦点问题时最常用的一套工具和方法整理出来。这些命令和工具用得好比单纯看代码效率高一个量级。6.1 命令行工具三件套第一件dumpsys window。刚才提到过它能看到所有窗口的状态和焦点。我常用的命令组合只有两个# 查看简洁窗口摘要 adb shell dumpsys window windows | grep -E Window #|mCurrentFocus|mFocusedApp # 查看某个包名的详细窗口信息 adb shell dumpsys window windows | grep -A 30 包名第二件dumpsys input。它显示的是InputDispatcher窗口层面的状态adb shell dumpsys input | grep -E FocusedWindow|FocusedApplication|TouchStates这里能看到输入端视角的焦点窗口。如果发现FocusedWindow和dumpsys window里的mCurrentFocus不一致一般有以下几种情况窗口正在切换过程中、窗口已销毁但InputDispatcher缓存还没清理、或者WMS和InputManager之间产生了不同步。后两种情况重启一下输入法服务通常能解决。第三件dumpsys activity top。它查看的是当前栈顶Activity的状态信息adb shell dumpsys activity top | grep -E ACTIVITY|Resumed|mFocused这个命令能看到Activity生命周期状态和WMS的窗口状态对照着看就能把“Activity处于什么状态”和“窗口处于什么状态”串成一条完整的链路。6.2 用Layout Inspector观察窗口层级Layout Inspector是Android Studio自带的工具主要用来查看View层级但它对窗口焦点排查也有帮助。因为窗口焦点的最终表现是落在View树的焦点状态上使用Layout Inspector可以看到当前界面里每个View的focus状态以及焦点所在的View分支。具体操作是在Android Studio里连接设备打开Tools Layout Inspector然后选中View Hierarchy可以展开所有可见窗口的View树结构。注意窗口是跨进程的Layout Inspector会以进程为单位展示。如果一个窗口是其他进程添加的需要把对应的进程也选上才能看到。6.3 强化自己应用的焦点状态日志不得不说的是系统调试命令只能看结果看不到业务逻辑层面为什么会走到那个结果。因此我强烈建议在应用里给关键窗口的焦点变化加日志。最直接的做法是在Activity基类里统一处理Override public void onWindowFocusChanged(boolean hasFocus) { super.onWindowFocusChanged(hasFocus); Log.d(WindowFocus, getClass().getSimpleName() onWindowFocusChanged: hasFocus); }然后在Dialog和PopupWindow里也加上类似日志。当线上出现焦点相关问题时先把这些日志拉出来看焦点是哪个窗口拿到、什么时候丢失、丢失之后又是谁接管的基本能快速圈定范围比对着dumpsys的静态快照瞎猜高效得多。另外一个被很多人忽略的手段是WindowManager.LayoutParams的调试信息。你可以在addView之前打印一下自己的paramsLog.d(WindowFocus, params flags Integer.toHexString(params.flags) type params.type token params.token);这样当问题出现时你至少能确认自己的窗口属性没有跟预期产生偏差。窗口焦点这个东西表面上看是一套“系统自动决定”的机制实际开发中却需要开发者手动干预的地方非常多。可以说它对应用体验的影响比大多数开发者想象中的还要大。我见过太多因为悬浮窗没设FLAG_NOT_FOCUSABLE、Dialog关闭不留神、分屏状态下焦点理解错误而引发的线上bug希望这篇内容能帮你把这些坑提前避开。如果读完你对窗口焦点体系有了一个整体的认知再遇到相关问题知道从WMS窗口状态、InputDispatcher分发链路、应用层回调三个维度去定位那就达到这篇分享的目的了。