ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

小程序屏幕常亮setKeepScreenOn失效?生命周期与真机排查全攻略

小程序屏幕常亮setKeepScreenOn失效?生命周期与真机排查全攻略

先说结论:微信小程序里要让屏幕保持常亮,很多人的第一反应就是调wx.setKeepScreenOn({ keepScreenOn: true }),三行代码完事。但真到了扫码、计时、直播这类需要长时间亮屏的页面上,你会发现它经常不按剧本走,屏幕该熄还是熄,甚至设置了跟没设置一个样。这个"无效"的问题我前前后后折腾过很久,网上搜到的答案多半只说"放 onShow 里就好",但实际原因远不止一个。这篇文章把我排查下来验证有效的思路、代码和边界情况完整记录下来,给同样被这个 API 折腾的人一条可复现的排查路径。

1. setKeepScreenOn 的生效逻辑:先搞清楚"无效"到底是哪种无效

1.1 基础用法与 success 回调的认知误区

先看最基础的调用方式:

wx.setKeepScreenOn({ keepScreenOn: true, success: (res) => { console.log('设置常亮成功', res) }, fail: (err) => { console.error('设置常亮失败', err) } })

很多人的误区是:success回调只要执行了,就以为屏幕已经保持常亮。实际上success只代表"微信小程序框架收下了这条指令,并且向系统申请了一次屏幕唤醒锁(wakelock)",不代表系统电源策略一定会一直买账。常亮最终能否持续,取决于当前页面是否在前台、是否有原生组件干扰、是否处于低电量模式,以及微信版本和基础库版本对 wakelock 的管理方式。

也有相当一部分人根本没看fail回调。如果基础库版本过低,setKeepScreenOn这个 API 可能压根不存在,你调的只是一个 undefined 函数,自然一点反应都没有。排查前先做一次能力检测:

if (wx.canIUse('setKeepScreenOn')) { wx.setKeepScreenOn({ keepScreenOn: true }) } else { console.warn('当前基础库不支持 setKeepScreenOn,请升级微信') }

这一步能砍掉一个很隐蔽的"无效"分支。

1.2 "无效"的三种表现对应三种不同根因

我遇到的"无效"并不是同一种表现,按现象拆开看,每种背后的原因完全不一样。

表现最可能的原因
首次进入页面就一直不常亮基础库版本过低、开发者工具里测试、系统省电模式
刚进入是常亮的,切后台再回来就恢复熄屏调用写在 onLoad/mounted 里,恢复前台时没有重新设置
某些页面常亮正常,跳转几个页面后变回熄屏全局 API 状态被其他页面的生命周期覆盖或系统回收

这个分类很重要。如果你只按"放 onShow"这一招去修,最多只能解决第二类问题;第一类和第三类需要不同的处理方式。下面逐个拆开聊。

2. 生命周期是重灾区:onLoad、mounted 和 onShow 的坑

2.1 为什么 onLoad 里设置常亮不可靠

先看小程序页面生命周期的执行时机。

  • onLoad:页面首次加载时执行一次,之后不会再跑。
  • onShow:页面每次显示时都会执行,包括第一次进入、从后台切回前台、从其他页面返回当前页。

如果常亮逻辑写在onLoad里,第一次进入确实生效了,但小程序页面实例会被页面栈保留下来,你切后台再回来时,页面不会重新触发onLoad,而是触发onShow。这时候之前申请的屏幕唤醒锁大概率已经被系统回收,表现在真机上就是:进去是亮的,按 Home 键再切回来,没过多久屏幕又熄了。

我见过很多这个问题的帖子,代码基本长这样:

Page({ onLoad() { wx.setKeepScreenOn({ keepScreenOn: true }) // 切后台回来就失效 } })

正确的做法是放到onShow里:

Page({ onShow() { wx.setKeepScreenOn({ keepScreenOn: true }) } })

2.2 vue 系写法的映射陷阱:mounted 不等于 onShow

这个坑在 uni-app、Taro、mpvue 这类 vue 系小程序框架里尤其普遍。很多人习惯把所有初始化逻辑塞进mounted,在微信小程序底层,mounted对应的是组件挂载完成,等价于页面首次创建,而不是页面每次显示。

小程序页面栈会把页面组件和 Vue 实例一起保留,从后台回到前台时,组件不会重新挂载,mounted当然不会再次执行。所以在mounted里写常亮,跟你写在原生的onLoad里是同一个问题:只在第一次生效。

uni-app 的 Vue 3 写法里,应该用@dcloudio/uni-app提供的生命周期组合式函数:

import { onShow } from '@dcloudio/uni-app' onShow(() => { uni.setKeepScreenOn({ keepScreenOn: true }) })

Taro 里也是类似道理,如果用的是 React 写法,useDidShow才是页面每次显示会触发的钩子,useEffect只在挂载时跑一次。确认一下自己用的框架钩子是不是真正对应了页面的onShow,这一步能避免一大半框架层面的"无效"。

2.3 全局 API 和页面栈切换之间的状态拉扯

还有一个容易被忽略的事实:setKeepScreenOn是全局 API,不是页面级配置。A 页面设置true之后,跳到 B 页面,B 页面如果不主动维护这个状态,常亮到底还活不活着,取决于微信小程序当时有没有把 wakelock 回收掉。我实测下来,这种"跨页面残留"非常不可靠,有时候 A 设完去 B 还亮,有时候 B 页面刚加载出来没几秒就熄了,跟微信版本、基础库版本、机型都有关系。

所以不要指望"设一次,整个小程序所有页面都常亮"。你需要在每个需要常亮的页面onShow里主动设置true,在不需要常亮的页面onShow里主动设置false,把状态控制权握在自己手里,而不是赌系统的回收时机。

3. 工具测试是伪命题:真机上还有 iOS 和安卓的差异化表现

3.1 为什么在开发者工具里测常亮毫无意义

小程序开发者工具跑在桌面端,本质上是一个模拟器容器,它控制不了你电脑操作系统的息屏策略。你在工具里调用setKeepScreenOn({ keepScreenOn: true }),只会得到一次 API 调用成功,你的桌面该多久休眠还是多久休眠,开发者工具也没有一个"虚拟屏幕"可以真的被点亮或熄灭。

所以如果只在开发者工具里验证,得出"无效"的结论本身就是无效的。常亮这类跟系统电源策略相关的接口,请一律用真机预览测试。这是很多人浪费一两个小时排查后才会意识到的点。

3.2 一套可复现的真机验证流程

要确认setKeepScreenOn到底有没有生效,推荐按下面这个流程来,验证结果才可信:

  1. 手机端打开调试模式,系统自动锁屏时间调到最短(iOS 可以设 30 秒,安卓通常有 15 秒/30 秒选项)。
  2. 用开发者工具生成预览二维码,真机扫码打开对应页面。
  3. 打开 vConsole,观察接口调用日志,确认success或fail被触发。
  4. 保持页面静止,开始计时。如果到了系统设定的自动锁屏时间后屏幕依然亮着,说明常亮生效了。
  5. 再测一组"切后台再回来":按 Home 键切后台,等几秒再切回小程序,重新计时观察。

测试时建议一次只改一个变量,避免把"系统省电模式"和"生命周期问题"混在一起排查。我习惯用表格记录每台测试机的表现,机型、系统版本、微信版本、基础库版本四个字段缺一不可,因为很多"无效"其实是特定版本的行为差异。

3.3 iOS 回前台立即设置偶尔失效的延迟处理

在部分 iOS 微信版本上,从后台回到小程序,onShow触发后如果立刻调用setKeepScreenOn,常亮偶尔会不生效。我推测跟 WKWebView 的页面活跃状态恢复时序有关,但这不重要,重要的是实测有效的规避手段:稍微延迟一下再设置。

Page({ onShow() { this.clearKeepScreenOnTimer() this.keepScreenOnTimer = setTimeout(() => { wx.setKeepScreenOn({ keepScreenOn: true }) }, 300) }, onHide() { this.clearKeepScreenOnTimer() }, onUnload() { this.clearKeepScreenOnTimer() }, clearKeepScreenOnTimer() { if (this.keepScreenOnTimer) { clearTimeout(this.keepScreenOnTimer) this.keepScreenOnTimer = null } } })

延迟 200 到 500 毫秒都试过,300 毫秒在我这边表现最稳定。注意离开页面时要把定时器清掉,否则页面都销毁了定时器还挂着,属于不必要的副作用。

4. 原生组件、视频播放和系统省电策略:几个文档不会明说的边界

4.1 视频和直播类原生组件对常亮的干扰

setKeepScreenOn管的是 WebView 渲染层的常亮策略,但小程序里的video、live-player、map、camera这类组件是原生组件,渲染层级在 WebView 之上,电源管理行为更接近原生 App。最典型的表现是:页面onShow里设了常亮,点开一个全屏视频,播放到一半屏幕照样能熄。

根据我的实践经验,视频场景最好不要只依赖页面级的onShow设置。监听视频组件的播放事件,在播放开始后重新设置一次常亮,在暂停或结束时按业务需要决定是否关闭:

<video id="myVideo" src="{{videoSrc}}" bindplay="onVideoPlay" bindpause="onVideoPause" bindended="onVideoEnded" />
Page({ onVideoPlay() { wx.setKeepScreenOn({ keepScreenOn: true }) }, onVideoPause() { // 按业务需要决定,如果暂停时也要求亮屏,就保持 true wx.setKeepScreenOn({ keepScreenOn: false }) }, onVideoEnded() { wx.setKeepScreenOn({ keepScreenOn: false }) } })

这样处理的逻辑是:视频真正进入播放态,系统播放器持有前台媒体会话,此刻再申请一次 wakelock,才会在原生层生效。

4.2 直播页的常亮组合方案

直播类页面比普通视频更依赖常亮,因为用户可能长时间只看不操作。除了页面onShow设置一次外,我建议在live-player进入播放状态后再设置一次:

Page({ onShow() { wx.setKeepScreenOn({ keepScreenOn: true }) }, onLivePlayerStateChange(e) { const code = e.detail.code // 进入播放状态码各家不一样,以实际返回为准 if (code === 200) { wx.setKeepScreenOn({ keepScreenOn: true }) } else if (code < 0) { // 出错/缓冲时,播放器可能短暂接管屏幕状态,可重新设置一次 setTimeout(() => { wx.setKeepScreenOn({ keepScreenOn: true }) }, 500) } } })

还有一个容易被忽视的细节:长时间高负载播放会让手机发热,部分安卓定制系统会出于温控策略强制熄灭屏幕,哪怕你持有 wakelock。这种情况代码层面很难完全绕过,只能尽量优化页面渲染和播放清晰度,减少不必要的性能开销。

4.3 低电量与省电模式:不是代码问题,但你必须知道

iOS 的低电量模式、安卓的超级省电/极致省电,都会无视第三方应用的 wakelock。你代码写得再对,系统层面照样强制休眠。这类"无效"其实是系统策略的兜底,不是小程序的问题。

如果业务强依赖屏幕常亮,可以考虑用wx.getBatteryInfoSync()检测电量,在电量偏低时主动提示用户:

const batteryInfo = wx.getBatteryInfoSync() if (batteryInfo.level < 20 && batteryInfo.isCharging === false) { wx.showToast({ title: '低电量模式可能影响常亮', icon: 'none' }) }

提示文案尽量写得像业务建议,而不是系统报错,避免给用户造成困扰。

5. 一套可以直接抄的常亮管理方案

5.1 原生小程序页面模板

这是我在普通页面里最常用的写法:onShow里设置常亮,onHide和onUnload里不做对称式关闭,只在需要取消常亮的页面onShow里显式设置false。

Page({ onShow() { wx.setKeepScreenOn({ keepScreenOn: true }) } })

为什么不推荐在onHide里设置false?因为页面跳转时新旧页面的onHide和onShow触发顺序在部分场景下会有交错,如果你在 A 页的onHide里关掉常亮,B 页的onShow还没来得及把它打开,就会产生一个熄屏窗口,甚至在特定顺序下直接把下一个页面的常亮覆盖掉。让每个页面只对自己的onShow负责,反而更稳。

5.2 uni-app 的页面生命周期写法

如果你用 uni-app,最常见的坑是拿mounted代替页面生命周期,前面已经详细说过了。正确写法如下:

<script setup> import { onShow, onHide, onUnload } from '@dcloudio/uni-app' onShow(() => { uni.setKeepScreenOn({ keepScreenOn: true }) }) onHide(() => { // 不建议在这里设 false,理由同上 }) </script>

注意onHide、onUnload没有特别必要就别去动常亮状态,除非你的业务明确要求"页面一旦离开就立刻取消常亮"。

5.3 用 Behavior 做一处维护、全局生效

如果小程序里有十几个页面都需要常亮,每个页面复制粘贴onShow调用太蠢了,也容易漏。原生小程序可以用Behavior做一个公共能力:

// keepScreenOnBehavior.js module.exports = Behavior({ onShow() { wx.setKeepScreenOn({ keepScreenOn: true }) } })

页面里只需要挂载这个 behavior:

const keepScreenOnBehavior = require('../../behaviors/keepScreenOnBehavior') Page({ behaviors: [keepScreenOnBehavior] })

对于不需要常亮的页面,不挂载这个 behavior 即可。这样全局只有一份常亮设置逻辑,想调整的时候不用翻十几个页面。需要提醒的是,页面使用behaviors对基础库版本有要求,如果项目要兼容很老的微信版本,建议退一步,抽一个公共函数,在每个页面的onShow里手动调用,维护成本虽然高一点,但兼容性更稳。

6. 排查复盘:一张表定位问题

把前面所有内容收拢成一张快速定位表,遇到"无效"直接对照:

现象优先排查处理手段
工具里测试无效本来就不生效,别浪费时间换真机预览
真机首次进入就无效基础库版本、系统省电模式用wx.canIUse检测;提示用户关省电模式
首次有效,切后台回来失效调用写在 onLoad 或 mounted改到 onShow,回前台重新设置
跨页面跳转后失效全局 API 状态被覆盖每个需要常亮的页面 onShow 手动设置
全屏视频播放时熄屏原生组件接管电源策略监听 video 播放事件重新设置
iOS 上回前台偶发无效页面活跃时序延迟 200-500ms 再调用
低电量提示后熄屏系统省电策略兜底页面内引导关闭省电模式

我现在的常规排查顺序是这样的:先确认基础库支持,再改到onShow,然后真机复现,排除省电模式,最后检查是不是原生组件场景。这套顺序走下来,九成以上的"setKeepScreenOn 无效"都能定位到根因。

真正让我在"无效"上卡得最久的,不是这个 API 有多难,而是我一度盲目相信success回调等于屏幕真的常亮了。现在每次接到类似问题,我都会先问一句:你是在开发者工具里测的,还是真机上测的?这个问题的答案,能直接砍掉一半的无效排查工作。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进