做播放器的同学基本都碰到过这个场景视频放在那边播着十几秒没人碰屏幕背光先暗下去再过一会直接锁屏。如果是直播、网课、短视频这类需要长时注视内容的场景体验就很糟糕——明明声音还在跑画面已经黑了。Android 系统对“播放视频时保持屏幕常亮”这件事其实给了好几条路Window的FLAG_KEEP_SCREEN_ON、PowerManager的WakeLock以及MediaPlayer.setScreenOnWhilePlaying(true)。这篇主要拆第三个接口也是官方接口里跟播放器绑定最紧的一个它的调用链到底怎么走、源码为什么这么设计、接入时有哪些坑。文章会比较长核心分四块先说清楚它跟另外两种常亮方式的本质区别再沿着源码把调用流程跟一遍然后是完整的实战接入示例最后是问题排查笔记。适合正在做视频播放器、或者打算把播放常亮逻辑从“临时加 flag”改成正规实现的开发者看。1. 认识setScreenOnWhilePlaying它到底解决了什么1.1 从用户的体验问题说起手机屏幕自动熄灭背后的逻辑很简单一段时间内没有触摸、按键等输入事件系统就判定用户不在看屏幕从而进入熄屏省电状态。这个逻辑对普通应用没问题但对视频播放场景却是误判——你看视频恰恰是“只看不摸”的状态盯着屏幕十几分钟反而什么都不碰。所以视频类应用需要主动告诉系统“别按默认超时熄屏我这儿正在放东西用户正在看。”setScreenOnWhilePlaying就是干这个的它做的事情很单纯在 MediaPlayer 播放期间让视频渲染所在的窗口保持FLAG_KEEP_SCREEN_ON状态一旦播放停止或 Surface 销毁自动恢复原来的熄屏策略。1.2 三种常亮方案的选型对比整个 Android 系统里能实现“屏幕常亮”的常见手段就那么多我直接列个对比方便选型时一眼看明白方案作用粒度是否需要权限生命周期管理使用场景MediaPlayer.setScreenOnWhilePlaying绑定播放器 SurfaceHolder不需要跟随 Surface 和播放状态自动生效/失效标准 SurfaceView MediaPlayer 播放WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON整个 Activity / Dialog 窗口不需要需要手动 add / clear页面级常亮不限于视频播放PowerManager.WakeLock系统电源级锁需要WAKE_LOCK权限必须手动释放异常时可能泄漏特殊长时间任务如录制、导航三条路互不冲突也可以叠加使用。但实际上大多数播放器场景直接用setScreenOnWhilePlaying就够了因为它把“什么时候亮、什么时候不亮”跟播放状态绑在了一起——Surface 在播放中屏幕就亮Surface 没了或者调用方主动关了屏幕就恢复正常。1.3 使用前提绑定 SurfaceHolder有个细节必须先说透这个接口的官方注释和实现都明确指出只有在调用过setDisplay(SurfaceHolder)成功绑定 SurfaceHolder 的前提下才有效。换句话说你要走这条路渲染载体必须是SurfaceView通过它的holder绑定给 MediaPlayer。如果你用TextureView或者你是用MediaPlayer.setSurface(Surface)直接传裸 Surface那setScreenOnWhilePlaying(true)顶多只是给播放器内部存了个标志位实际不会产生任何屏幕常亮效果。这也解释了为什么网上经常有人问“我明明调了为什么还是锁屏”——大概率是渲染载体选错了。2. 核心调用流程源码级拆解2.1 从MediaPlayer.setScreenOnWhilePlaying到updateSurfaceScreenOn我直接把 AOSP 里MediaPlayer.java的这段简化逻辑摘出来方便跟代码public void setScreenOnWhilePlaying(boolean screenOn) { if (mScreenOnWhilePlaying ! screenOn) { if (screenOn mSurfaceHolder null) { Log.w(TAG, setScreenOnWhilePlaying(true) is ineffective without a SurfaceHolder); } mScreenOnWhilePlaying screenOn; updateSurfaceScreenOn(); } } private void updateSurfaceScreenOn() { if (mSurfaceHolder ! null) { mSurfaceHolder.setScreenOnWhilePlaying(mScreenOnWhilePlaying); } }注意这里的两个关键点第一它不是直接把screenOn状态传到 native 层。MediaPlayer大部分操作setVolume、seekTo、setDataSource都走了 JNI但setScreenOnWhilePlaying本质上是个纯 Java 层的“协调动作”播放器只需要记住当前标志位然后把真正的常亮实现委托给SurfaceHolder。第二调用前置条件被写进日志里了——如果mSurfaceHolder null框架只给个 WARNING不抛异常也不会产生别的效果。这意味着你在setDisplay之前调它可能代码“看起来没报错”实际上白调了。另外源码里还做了防抖只有当mScreenOnWhilePlaying ! screenOn时才更新避免重复 set 引发的冗余窗口更新。2.2 从SurfaceHolder到SurfaceView.updateSurfaceSurfaceView是SurfaceHolder的唯一实现坐标。当 MediaPlayer 调mSurfaceHolder.setScreenOnWhilePlaying(true)时实际走的是SurfaceView内部SurfaceHolder的setScreenOnWhilePlaying方法核心代码如下public void setScreenOnWhilePlaying(boolean screenOn) { if (mScreenOnWhilePlaying ! screenOn) { mScreenOnWhilePlaying screenOn; updateSurface(); } } private void updateSurface() { if (mScreenOnWhilePlaying mSurface.isValid()) { mWindow.setFlags( WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON, WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); } else { mWindow.setFlags(0, WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); } }这里逻辑很直接但有几个细节点值得展开mSurface.isValid()是决定性条件。SurfaceView 里的 Surface 是异步创建的只有在surfaceCreated回调之后底层的 BufferQueue 才真正可用isValid()才返回 true。如果 Surface 已经销毁比如页面切后台、SurfaceView 从窗口移除就算mScreenOnWhilePlaying是 true也会走 else 分支清除常亮。mWindow.setFlags设置的是 SurfaceView 自己那个窗口的 flag。很多人不知道SurfaceView 其实是一块独立的窗口MyWindow它跟宿主 Activity 的窗口不是同一个。所以这个常亮 flag 是挂在 SurfaceView 窗口上的作用范围很精准SurfaceView 在窗口在常亮就生效SurfaceView 被移除flag 自动清掉。setFlags(mask, mask)的第二个参数是mask只影响 mask 里指定的 flag其余 flag 不变这种写法比addFlags/clearFlags更精细反射出 Android 框架内部对窗口 flag 的操作习惯。2.3 Surface 生命周期回调如何跟常亮联动调用流程里还有一个容易被忽略的联动SurfaceHolder.Callback的surfaceCreated和surfaceDestroyed也会触发updateSurface()。Override public void surfaceCreated(SurfaceHolder holder) { // 内部会调用 updateSurface() updateSurface(); } Override public void surfaceDestroyed(SurfaceHolder holder) { // 内部会调用 updateSurface() updateSurface(); }这个设计很讲究。它保证了即使你忘了在onPause里调用setScreenOnWhilePlaying(false)只要 SurfaceView 随窗口销毁mSurface.isValid()变成 false常亮 flag 就会被清除。从 Android 4.0 以前到现在的版本这套“MediaPlayer标志 SurfaceHolder转发 SurfaceView窗口 flag Surface 生命周期校验”的组合拳基本没变过稳定得让人安心。Android 12 之后系统对窗口常亮 flag 的管理逻辑更集中了但对SurfaceView这层中间封装来说对外面向开发者的行为保持一致。2.4 窗口 flag 下发后的系统动作SurfaceView通过mWindow.setFlags更新窗口 flag 后系统内部的传导大致是SurfaceView.MyWindow通过ViewRootImpl向WindowManagerService汇报窗口参数变化WindowManagerService收集并更新该窗口的FLAG_KEEP_SCREEN_ON状态系统电源管理服务PowerManager 子系统把窗口的常亮需求折算成DisplayPowerRequest里的policy要求屏幕保持点亮底层DisplayPowerController接到请求后抑制自动灭屏逻辑让屏幕持续点亮。这套链路平时开发根本接触不到但它解释了为什么setScreenOnWhilePlaying不需要权限——它不是直接抓系统电源锁而是通过“窗口需求”曲线救国主导决策的是系统自己的电源策略。提示FLAG_KEEP_SCREEN_ON只能阻止无操作自动熄屏不能阻止用户手动按电源键锁屏。两者是不同层级的事别指望用这个接口防手滑按锁屏键。2.5 一个“看似到了 native 实际没到”的接口我在跟源码前一直以为setScreenOnWhilePlaying最终会走native层因为很多 MediaPlayer 接口都是这么设计的。结果看完代码才发现它压根没进libmedia_jni全程在 Java 层完成状态传递。这个“反常识”的点其实很有启发性Android 框架层并不是所有 API 都要往 native 扎如果问题能在更上层用窗口机制解决会保留在上层解决因为这样开销更低、语义更清晰。持屏常亮本质是窗口显示策略跟编解码、播放器内核半毛钱关系没有让它留在 Java 窗口层反而是最合理的架构决策。3. 实战接入一个完整的播放器示例3.1 基础布局与播放器绑定先用最标准的方式跑通链路。布局文件就是一个 SurfaceView 几个控制按钮FrameLayout xmlns:androidhttp://schemas.android.com/apk/res/android android:layout_widthmatch_parent android:layout_heightmatch_parent SurfaceView android:idid/sv_player android:layout_widthmatch_parent android:layout_heightmatch_parent / Button android:idid/btn_play android:layout_widthwrap_content android:layout_heightwrap_content android:text播放 android:layout_gravitycenter / /FrameLayoutJava/Kotlin 侧的绑定逻辑我直接给完整版public class PlayerActivity extends Activity implements SurfaceHolder.Callback { private MediaPlayer mediaPlayer; private SurfaceView surfaceView; private SurfaceHolder holder; Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_player); surfaceView findViewById(R.id.sv_player); holder surfaceView.getHolder(); holder.addCallback(this); } Override public void surfaceCreated(SurfaceHolder h) { // 关键必须在 surfaceCreated 之后再初始化播放器 initPlayer(h); } Override public void surfaceChanged(SurfaceHolder h, int format, int width, int height) { // 不需要额外处理 } Override public void surfaceDestroyed(SurfaceHolder h) { releasePlayer(); } private void initPlayer(SurfaceHolder h) { mediaPlayer new MediaPlayer(); try { mediaPlayer.setDataSource(videoUrl); mediaPlayer.setDisplay(h); // 先绑定 SurfaceHolder mediaPlayer.setScreenOnWhilePlaying(true); // 打开视频常亮 mediaPlayer.setOnPreparedListener(mp - mp.start()); mediaPlayer.prepareAsync(); } catch (IOException e) { e.printStackTrace(); } } private void releasePlayer() { if (mediaPlayer ! null) { mediaPlayer.setScreenOnWhilePlaying(false); // 恢复默认熄屏策略 mediaPlayer.release(); mediaPlayer null; } } }3.2 生命周期管理细节上面的代码能跑通但直接放线上项目还有几个生命周期场景要处理。场景一切后台、来电、弹窗。onPause时SurfaceView 虽然不会立刻销毁但 Activity 不可见播放器往往需要暂停或释放。此时应该手动把常亮关掉避免用户退到桌面后屏幕还亮着Override protected void onPause() { super.onPause(); if (mediaPlayer ! null) { mediaPlayer.setScreenOnWhilePlaying(false); mediaPlayer.pause(); } }场景二切回前台继续播放。onResume里重新拉起来就行Override protected void onResume() { super.onResume(); if (mediaPlayer ! null) { mediaPlayer.setScreenOnWhilePlaying(true); if (!mediaPlayer.isPlaying()) { mediaPlayer.start(); } } }场景三多窗口分屏。Android 10 以后分屏是常态。分屏模式下 SurfaceView 的窗口仍然有效常亮 flag 会继续生效——这其实是合理的因为用户确实还在看。但如果你的应用在分屏时希望只让播放区域显示常亮、不让整个屏幕强制常亮那setScreenOnWhilePlaying就帮不上忙了需要在业务层另做判断。注意MediaPlayer.release()之后对象已经释放底层资源此时再调setScreenOnWhilePlaying(false)会有未知行为风险。建议像示例里一样在release()之前先把常亮状态重置。3.3 配合“触摸一次暂停/全屏”等交互逻辑实际产品需求往往不是“一直常亮”而是“亮到我触摸为止”。比如 Vlog 播放器用户点击屏幕暂停时应该顺手恢复自动熄屏避免挂在那边不看还占着屏幕surfaceView.setOnClickListener(v - { if (mediaPlayer ! null) { if (mediaPlayer.isPlaying()) { mediaPlayer.pause(); mediaPlayer.setScreenOnWhilePlaying(false); // 显示播放控制栏 } else { mediaPlayer.start(); mediaPlayer.setScreenOnWhilePlaying(true); // 隐藏播放控制栏 } } });这种做法的本质是把setScreenOnWhilePlaying当成一个业务开关而不是全局无脑开关。在播放器组件内部通常只需要一个方法来同步“当前是否处于播放状态”和“当前是否应该保持常亮”两个状态对齐就很少出错。3.4 如果用 TextureView 怎么曲线救国TextureView 没有 SurfaceHolder所以直接调setScreenOnWhilePlaying是无效的具体无效原因见 1.3。但 TextureView 在某些场景下确实有优势比如支持动画、支持旋转。替代方案很直接textureView.setKeepScreenOn(true);View.setKeepScreenOn其实内部也是操作窗口的FLAG_KEEP_SCREEN_ON只是挂在 TextureView 所在的宿主视图层级上效果类似却不需要跟播放器绑定。缺点是没有自动跟随播放器生命周期需要自己在onPause/onStop里手动关闭textureView.setKeepScreenOn(false);如果你连 View 都不想引入也可以用 Window 级别的 flaggetWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);但这会把整个 Activity 窗口都变成常亮粒度比较粗不推荐在复杂页面上滥用。4. 常见问题与排查笔记实录4.1 问题速查表现象可能原因排查方向调了setScreenOnWhilePlaying(true)后依然熄屏渲染载体是 TextureView或未调用setDisplay(SurfaceHolder)检查是不是 SurfaceView检查surfaceCreated之后是否重新设置有状态播放器释放或切后台后屏幕仍然常亮没在 release / onPause 前关掉常亮或用了FLAG_KEEP_SCREEN_ON叠加检查releasePlayer里有没有先设置 false屏幕常亮状态下按电源键无效这是正常行为FLAG_KEEP_SCREEN_ON不拦手动锁屏不用排查确认是不是用户手动按键导致两个播放器实例切换后常亮状态错乱多个 MediaPlayer 同时对一个 SurfaceHolder 设置了不同状态播放器切换前先对旧实例调 false再对新实例调 truesetDisplay之后 Surface 已经销毁setScreenOnWhilePlaying不再生效SurfaceView 已从窗口解绑属于预期行为不需要修等 Surface 重新创建后再设4.2 我在实际项目里踩过的三个坑坑一在surfaceCreated之前调用setDisplay。有次我把播放器初始化放在onCreate里直接做SurfaceView的 surface 还没创建setDisplay传的 holder 实际上没有可用的 Surface。结果播放不出来常亮也没生效。后来按照回调顺序把初始化挪到surfaceCreated里问题立刻消失。记住SurfaceView 的可用 Surface 是异步创建的必须在surfaceCreated回调之后才能配合 MediaPlayer。坑二多实例切换时只在第一个实例上设了 true。项目里做一个“预览 全屏”的播放器两个实例会同时存在切换时只在全屏实例上设了 true没有在预览实例上设 false。结果预览页还绑着旧 Surface 时逻辑判断mScreenOnWhilePlaying是 true但它已经不在可视区域了导致整个窗口持续常亮。后来统一封装了一个setKeepScreenOn(boolean)方法所有实例都维护同一个状态位才彻底解决。坑三和WindowManager的FLAG_KEEP_SCREEN_ON混用导致无法恢复。有个项目早期用了getWindow().addFlags(FLAG_KEEP_SCREEN_ON)做兜底后来接入setScreenOnWhilePlaying后一直关不掉常亮排查半天才发现是 Activity 级 flag 还挂在那。两个方案叠加后要恢复默认熄屏必须两个都清掉建议走setScreenOnWhilePlaying就不要再叠 Window flag 了。4.3 调试技巧怎么确认常亮 flag 挂上了有时候代码看了半天看不出问题实际跑起来到底有没有挂上常亮 flag可以用 adb 直接查窗口参数adb shell dumpsys window windows | grep -E Window #|keepScreen|SurfaceView|mAttrs重点看输出里有没有mAttrs.flags... KEEP_SCREEN_ON如果 SurfaceView 窗口对应的mAttrs里有KEEP_SCREEN_ON说明链路已经到窗口层了系统层面按理不会再自动熄屏。如果窗口层没有再往上查 MediaPlayer 侧adb shell dumpsys media.player不过这个 dump 默认不会列屏幕常亮状态更多是播放器实例信息。真正有用的还是dumpsys window windows能看到每个窗口的实际 flags。这个调试方法在 Android 6 到最新的系统上都适用各大厂商定制系统上基本也保留了这个接口。写在最后的实践体会做了这么多年多媒体开发我对setScreenOnWhilePlaying的评价是它是 SurfaceView MediaPlayer 场景下最省心、最不容易出错的常亮方案因为它把“常亮”和“播放器渲染生命周期”绑在一起从源头上帮你消掉了很多维护状态的问题。我个人的习惯是凡是播放器组件内部处理一律优先用这个接口只有到了 Activity/Dialog 级别的页面常亮需求才考虑WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON。至于PowerManager.WakeLock我几乎不在普通视频播放场景里碰它权限、释放时机、异常泄漏哪一个都够喝一壶。最后再分享一个小技巧如果你的播放 SDK 支持自定义渲染层不妨在组件内部做一个“常亮状态同步方法”把setScreenOnWhilePlaying、Surface 回调、业务暂停/恢复全部收敛到一个方法里。这样就算后面换TextureView也只是把方法内部实现换成View.setKeepScreenOn对外暴露的接口语义完全不变坑都要少踩很多。