1. 为什么放着reLaunch不用,非要动“冷重启”的手
先说结论:uni.reLaunch和“冷重启”看着像,治的病完全不一样。
做过一段时间 uniapp 的都知道,uni.reLaunch能关掉所有页面、重新打开某个页面,页面栈会被清干净。但“页面栈清了”不等于“应用状态干净了”。globalData里的用户信息、内存里缓存的基类实例、插件模块里存着的 socket 连接、甚至是某几个第三方 SDK 在原生层留下的回调状态,reLaunch全都管不了。也就是说,你只是把舞台上的道具撤了,后台的灯还亮着,演员还没退场。
冷重启是另一套逻辑:把整个 JS 引擎状态、原生层模块状态、页面栈、全局变量、各插件持有的内存对象,全部推倒重来。用 uniapp 官方接口来操作,就是plus.runtime.restart(),它会让应用回到“刚启动”的状态,重新走一遍生命周期,重新加载App.vue。
这个功能真正用得上的场景,往往不是日常跳转,而是这种硬核时刻:
- 切换账号:当前用户退出登录,下一个用户要登录,为了不让上一个账号的 data、
uni.getStorageSync里残留的页面级状态、以及第三方统计 SDK 里的 userId 串号,最好的办法不是一个个清,而是直接重启。 - 切换语言/换主题:多语言切换后,页面文本全部要刷新,虽然理论上可以触发视图重渲染,但一旦用了原生 tabbar、nvue 页面、或第三方 UI 库的文案缓存,你就会发现漏网之鱼特别多。冷启动一次,所有东西老老实实重新读配置。
- 权限变更后:用户同意或撤回某个隐私权限,某些原生 SDK 只在启动时读取权限状态,运行中改是无效的。
- 测试/演示环境快速验证:自动化测试跑完一个用例,需要让 App 回到干净的初始状态,冷重启是最省心的方案。
所以这篇不是教你怎么在正常业务流程里玩“重启大法”,而是把冷重启这个能力吃透:怎么调、调了之后状态怎么控制、哪些平台会失效、怎么把参数带进重启后的“新世界”。这些细节,都是我在实际项目里一个个踩出来的。
2. plus.runtime.restart():冷重启的唯一硬核入口
这一节先把方案本身讲透。plus.runtime.restart()是 HTML5+ 提供的原生方法,在 uniapp 的 App 端可以直接通过plus.runtime.restart()调用。
调用的效果:杀掉当前 WebView 和 JS 引擎实例,重新创建原生 WebView 并加载应用入口。
2.1 最基础的用法
// 页面中某个按钮的点击事件 function handleRestart() { plus.runtime.restart() }就这么简单。但注意,这段代码只能跑在 App 端,也就是经过 HBuilderX 打包后的真机 App、模拟器 App 上。小程序端没有plus对象,H5 端默认也没有,跨端项目必须做条件编译。
2.2 带参数的冷重启:先存后取
既然重启会销毁内存中所有状态,那么想“重启后带着某个信息进入新世界”,最常见也最可靠的做法是:先把参数写进 Storage,再重启。
function restartWithParams(params) { uni.setStorageSync('restart_params', params) // 可以加个延迟,确保 Storage 写入完成再重启 setTimeout(() => { plus.runtime.restart() }, 300) }等应用重新启动后,在App.vue的onLaunch或某个启动页的onLoad里读取:
// App.vue 或者入口页面 onLaunch() { const params = uni.getStorageSync('restart_params') if (params) { // 比如存储的是 { path: '/pages/user/login', mode: 'switchAccount' } // 可在这处理启动跳转逻辑 uni.removeStorageSync('restart_params') } }提示:
plus.runtime.restart()是异步的,但调用后整个应用会在极短时间内被杀掉重建,所以一般不用等待回调函数。倒是“写入 Storage 后再重启”这个动作有可能因为时序问题失败,稳妥做法就是加 200ms-500ms 的延迟,给 Storage 操作留出落盘时间。
2.3 和“重新编译 API 方案”的对比
很多人会问,uniapp 的文档里不是有“重新编译 API”吗?uni.restart()之类的是什么情况?这就要理清一个概念:
plus.runtime.restart()是 App 原生层的“重启应用”。- 有些方案叫“重新编译”,比如 uni-app 编译器提供的内置 API,实质上是对当前应用进行 JS 层的重新加载。
uni.reLaunch()只是页面栈的重置,不是冷启动。
我在项目里验证过:
- 重新编译 API:把整个应用 JS 重新执行一遍,页面会重新创建,但原生层的一些模块状态是否彻底重置,取决于各模块实现。可靠性比
plus.runtime.restart()差。 - plus.runtime.restart():App 回到系统桌面级重启,相当于用户手动划掉 App 再打开,可靠性最高。
- uni.reLaunch():只是当前 WebView 的场景切换,和冷重启完全是两码事。
如果你正在做的是纯 App 项目,切账号、清状态这种事,首选plus.runtime.restart()。如果是多端项目,我后面有一节专门讲各端的替代方案。
2.4 冷重启 vs 热更新的边界
有的项目里把“冷重启”和“热更新”混在一起聊,这里顺带做个区分:
- 热更新(wgt 资源包更新):更新的是前端资源,通过
plus.runtime.applyUpdate()安装新资源包。安装完成后需要重启才能让新资源生效。 - 冷重启:不管你有没有新资源,直接把应用清干净重启。
所以热更新流程的最后一步,通常也会调一次plus.runtime.restart()来加载新资源。这里有个小坑:applyUpdate()成功后,部分 Android 机型不会立刻让新资源完全生效,需要隔一点时间再重启,或者用plus.runtime.restart()连续调用两次。我在生产环境踩到过一次,后面“踩坑实录”里会细讲。
3. 冷重启前的“清场动作”:别再让旧状态阴魂不散
冷重启看起来很彻底,但它不等于“失忆”。Storage 里的东西不会自己消失,系统剪贴板里的内容不会自己清掉,第三方 SDK 写在原生沙盒里的缓存文件也不会无端被删。所以做冷重启之前,先想清楚:你希望重启后,哪部分是“恢复出厂”,哪部分是“保留现场”?
3.1 该清的、该留的,列一张清单
拿“切换账号后冷重启”举例,这是最常见的场景。我把要处理的项列出来:
| 数据项 | 处理方式 | 原因 |
|---|---|---|
| 旧用户 token | 必须清除 | 否则新用户可能拿到旧身份 |
| 本地用户资料缓存 | 必须清除 | 避免界面显示上一个用户的头像昵称 |
| 购物车、订单列表等页面级缓存 | 建议清除 | 防止串号 |
| 埋点上报队列 | 建议保留 | 切换账号前产生的埋点还要上报 |
| 环境配置(API 地址、主题色) | 必须保留 | 这是应用本身的配置,与用户无关 |
| 设备唯一标识 | 必须保留 | SDK 权限校验依赖它 |
我在项目里写过一个clearBeforeRestart方法,专门处理这类逻辑:
function clearBeforeRestart(options = {}) { const { keepKeys = [], clearKeys = [] } = options // 保留需要保留的 const keepMap = {} keepKeys.forEach(key => { keepMap[key] = uni.getStorageSync(key) }) // 拿到所有 storage key // 注意:uni.getStorageInfoSync 返回的 keys 只有一级 key,嵌套对象里的字段不受影响 const { keys } = uni.getStorageInfoSync() keys.forEach(key => { if (!keepKeys.includes(key) && !clearKeys.includes(key)) { // 默认清掉 uni.removeStorageSync(key) } }) // 写回保留项 Object.keys(keepMap).forEach(key => { uni.setStorageSync(key, keepMap[key]) }) }这个方法的思路是“白名单制”:不传参数时默认清空所有 Storage,传入keepKeys就保住指定的 key。比“黑名单制”安全,不容易漏掉不该留的东西。
提示:
uni.removeStorageSync对不存在的 key 不会报错,可以放心循环调用。
3.2 别只盯着 Storage,第三方模块的状态也要管
冷重启会把内存里的第三方模块状态清掉,但原生层某些持久化状态不会自动清。最典型的是:
- 推送模块:如果用的是个推、极光这类推送 SDK,切换账号前要把旧账号的别名、标签解绑,否则新设备上会收到属于老账号的推送。
- IM 模块:融云、环信这类即时通讯 SDK,内部有本地数据库缓存和历史消息,切换账号前要做 logout 操作,否则重启后 SDK 可能还认为自己是登录态。
- 定位模块:某些定位 SDK 会缓存最近定位结果,权限变更场景下需要手动清除定位缓存。
做法是:在调用冷重启之前,先执行第三方模块的清理方法,等这些异步操作全部完成后,再调plus.runtime.restart()。
我一般用 Promise 把清理流程串起来:
async function beforeRestart() { // 1. 清除业务数据 clearBeforeRestart({ keepKeys: ['deviceId', 'envConfig'] }) // 2. 解绑推送别名 await new Promise(resolve => { pushModule.unbindAlias({ success: resolve, fail: resolve }) }) // 3. IM 登出并清缓存 await imModule.logout() // 4. 全部完成后再重启 plus.runtime.restart() }这里有个细节:success和fail都调resolve,是为了保证清理流程不会因为某个 SDK 的失败回调而中断。冷重启本来就是最后的兜底手段,前面任何一步失败,都不应该阻塞重启本身。
3.3 防止无限重启:打一个“标记位”
冷重启最恐怖的问题不是重启本身,而是重启后又被某个逻辑触发重启,然后无限循环。真机上一旦出现这种情况,App 会在启动动画里反复横跳,用户只能卸载重装。
触发无限重启的常见原因:
App.vue的onLaunch里有判断逻辑,某个条件不满足就调用了plus.runtime.restart()- 冷重启后读取旧 Storage,发现状态不对,再次触发“重置”逻辑
- 第三方 SDK 初始化失败后的自愈逻辑也调了
restart()
我的习惯是加一个“重启标记位”:
// 在重启前写入 function safeRestart() { uni.setStorageSync('is_restarting', true) plus.runtime.restart() } // App.vue 的 onLaunch 里 onLaunch() { const isRestarting = uni.getStorageSync('is_restarting') if (isRestarting) { // 说明这次启动是由重启触发的 uni.removeStorageSync('is_restarting') // 在这里做恢复现场、跳转逻辑,绝不再调 restart() return } // 正常启动的逻辑 }这样,只有“正常启动”和“业务主动触发重启”两种路径能调到restart(),重启后产生的isRestarting标记位会拦掉第二轮重启。
4. 要重启,就带“口令”进去:冷启动参数传递的三种姿势
冷重启之后,整个应用从零开始。如果你希望新启动的应用“知道”自己是因为什么被重启的,就得在重启前把“口信”留下来。这个需求太常见了:切完账号要直接跳首页,清完缓存要回登录页,换完语言要回到当前页面,这些都要靠参数传递。
4.1 姿势一:全局变量 + 定时器(不推荐,但要说清为什么)
最直觉的做法是:重启前把参数塞进globalData或某个全局变量,然后重启,重启后读取。
// 错误示例 getApp().globalData.restartReason = 'logout' plus.runtime.restart()这个做法在大部分 Android 机上可能“碰巧能用”,因为plus.runtime.restart()在某些版本里并没有真正销毁原 JS 引擎。但它本质是在赌“重启器”不会清内存。iOS 上内存管理更严格,冷启动后globalData大概率是全新的,你存的值会被丢掉。
所以我的结论是:不能全局变量传参,因为重启就是要清内存,你偏要反向依赖内存,逻辑上就自相矛盾。
4.2 姿势二:Storage 传参(最稳定,推荐)
把参数写进 Storage,这是我自己项目主要用的方式。核心逻辑前面已经写过了,这里补充几个注意点:
- 参数别太复杂:Storage 只存
{ reason: 'logout', target: '/pages/login/index' }这类简单 JSON,不要存大对象。冷重启后 App 要重新加载,parse 大 JSON 会拖慢首屏。 - 读取之后要清理:在
App.vue的onLaunch里读完参数后,立刻removeStorageSync,防止下次启动“误食”旧参数。 - 跨页面读取:如果你的参数要在业务页面里才能决定跳转(比如登录页要读“是否刚从退出登录状态过来”),可以在
onLaunch里把参数存到globalData,这样页面启动时globalData已经是完整可读的了。
4.3 姿势三:URL Scheme / 推送消息传参(适合外部唤醒场景)
有些冷重启不是用户在 App 内点按钮触发的,而是 App 被系统杀掉了,用户点了推送消息或扫码,系统重新拉起应用。这时候要从外部带参数进应用,走的是 uniapp 的onLaunch的options:
// App.vue onLaunch(options) { // options.path 是启动页面路径 // options.query 是启动参数 if (options && options.query) { const { scene, target } = options.query // scene 可以区分是扫码、推送还是其他方式进入 } }这个机制和plus.runtime.restart()本身没有直接关系,但很多时候你会把“冷重启”和“外部冷启动”搞混。外部冷启动由系统决定,你唯一能做的就是在onLaunch里接住参数,做好路由分发。我见过不少团队把“外部冷启动”也封装成重启逻辑,结果推送来了没法正常跳转,排查半天才发现是启动链路里混进了一个restart()。
4.4 一个完整的“切账号冷重启”示例
把上面几个点串起来,就是一个完整的方案:
// 在退出登录页面 function handleLogout() { uni.showModal({ title: '提示', content: '退出登录后应用将重启,是否继续?', success: async (res) => { if (!res.confirm) return // 1. 清业务数据 clearBeforeRestart({ keepKeys: ['deviceId'] }) // 2. 写重启参数(先于 restart 执行) uni.setStorageSync('restart_params', { reason: 'logout', needLogin: true }) // 3. 打标记位,防止无限重启 uni.setStorageSync('is_restarting', true) // 4. 延迟后重启 setTimeout(() => { plus.runtime.restart() }, 300) } }) }重启后启动页读取参数并做跳转:
// pages/index/index.vue 或某个启动页 onLoad() { const params = uni.getStorageSync('restart_params') if (params && params.reason === 'logout' && params.needLogin) { uni.removeStorageSync('restart_params') uni.reLaunch({ url: '/pages/user/login' }) } }提醒:不要直接在
App.vue的onLaunch里做uni.reLaunch,因为App.vue生命周期执行时页面栈尚未就绪,跳转有时会失败或出现奇怪的白屏。放到启动页的onLoad里做跳转,最稳。
5. 跨端适配:小程序和 H5 没有 plus,怎么实现“伪冷启动”
uniapp 的生态里,“App 端”是主场景,但项目经常要同时跑小程序和 H5。plus.runtime.restart()在非 App 端直接调,代码会报错(plus is not defined)。这时候你得想别的办法。
5.1 小程序端:用 reLaunch 清栈 + 手动重置全局状态
小程序没有“重启应用”这个能力。最接近冷启动的方案是uni.reLaunch到首页,并且在小程序入口文件(App.vue的onLaunch)里做一轮防御性状态重置。
// 条件编译,只有非 App 端编译这段代码 // #ifndef APP-PLUS function pseudoRestart() { // 清空 Storage 中需要清理的 key clearBeforeRestart({ keepKeys: ['deviceId'] }) // 重置 globalData const app = getApp() if (app) { app.globalData = {} } // 关掉所有页面,回到首页 uni.reLaunch({ url: '/pages/index/index' }) } // #endif小程序重启动画是没法控制的,reLaunch会闪一下再进首页,体验上比“杀了重开”还生硬。但功能上能覆盖大多数场景:页面栈清零、JS 对象状态会被销毁。
唯一没法模拟的是:原生层缓存。小程序没有原生层概念,所以问题不大。小程序的 Storage 是通过wx.setStorageSync管理的,clearBeforeRestart方法对它同样有效。
5.2 H5 端:location.reload + sessionStorage 传参
H5 端的“冷重启”本质上就是刷新页面。window.location.reload()会把当前页面的所有 JS 状态清空,重新加载整个 SPA。
// #ifdef H5 function h5Restart() { // H5 端冷启动参数用 sessionStorage 存,不用 localStorage,避免“永生参数” sessionStorage.setItem('restart_params', JSON.stringify({ reason: 'logout' })) window.location.reload() } // #endif注意,H5 端刷新后,onLaunch里的参数要从sessionStorage读,而不是uni.getStorageSync。我还习惯在刷新前用sessionStorage.removeItem先清理一次,确保旧参数不会在下下次刷新时再次出现。
5.3 多端统一封装:对外只暴露一个接口
如果你的项目是多端共用一套代码,最简单的做法是封装一个通用的appRestart方法,内部通过条件编译分流。这样业务层调用时不用关心当前是什么端,代码可读性也高。
export function appRestart(params = {}) { // 先存参数(各端都能用) uni.setStorageSync('restart_params', params) // #ifdef APP-PLUS uni.setStorageSync('is_restarting', true) setTimeout(() => { plus.runtime.restart() }, 300) // #endif // #ifdef H5 sessionStorage.setItem('restart_params', JSON.stringify(params)) window.location.reload() // #endif // #ifndef APP-PLUS || H5 // 小程序端 clearBeforeRestart({ keepKeys: ['deviceId'] }) const app = getApp() if (app) app.globalData = {} uni.reLaunch({ url: '/pages/index/index' }) // #endif }提示:
#ifndef APP-PLUS || H5的写法要注意编译优先级,如果你用的是 vue-cli 创建的项目,条件编译写法和 HBuilderX 创建的略有差异,建议统一用#ifndef APP-PLUS嵌套判断来避免歧义。
5.4 各端方案对比
| 端 | 方案 | 是否真正“冷” | 页面栈 | 全局状态 | Storage |
|---|---|---|---|---|---|
| App | plus.runtime.restart() | 是,JS 引擎重建 | 全部清空 | 全部清空 | 保留 |
| 小程序 | uni.reLaunch+ 手动清 globalData | 否,只是页面栈重置 | 全部清空 | 手动重置 | 保留 |
| H5 | window.location.reload() | 是,浏览器刷新 | 全部清空 | 浏览器自主决定 | Session 保留,Local 保留 |
需要说明的是:H5 刷新后,globalData会全部丢失,所以如果你在 H5 端要保留一些环境配置,要么放localStorage,要么放服务端下发。这就回到了“冷启动前要规划好哪些保留、哪些清掉”的话题。
6. 冷重启的坑,我替你踩过了:经验与教训
冷重启这个功能,原理只有一行代码,真正的问题都藏在细节里。这一节分享几个真实踩坑,每一条都对应一个实际线上问题。
6.1 坑一:Storage 写入后立刻就 restart,参数丢了
之前有次切账号后,重启完发现新用户登录页没能拿到“该去登录”的参数,排查很久,定位到问题出在时序上。
uni.setStorageSync是同步方法,看起来写完立刻就能读。但在 Android 部分机型上,可能是异步 I/O 缓冲的原因,进程被restart()杀掉时,Storage 的磁盘写入还没真正落盘,导致重启后读不到。
解决办法也很简单:setStorageSync之后加一个小延迟(200ms~500ms)再调restart(),让数据落盘。我在生产环境验证过:不加延迟,大概有 3%-5% 的概率参数会丢;加了 300ms 延迟后,这个问题基本绝迹。
6.2 坑二:Android 上重启后偶现白屏,查了三天发现是生命周期顺序
App 端的冷重启流程是:调用restart()→ 原生层杀掉 WebView → 重新创建 WebView → 重新加载 bundle → 执行App.vue的onLaunch→ 渲染pages/index/index.vue。
我遇到的情况是:在App.vue的onLaunch里做了uni.reLaunch跳转到某个业务页,结果这个跳转动作和首页onLoad的执行顺序产生了竞争,偶发出现白屏。
后来把跳转动作全部移到首页的onLoad里做,白屏问题不再出现。规律是:App.vue生命周期里只做数据准备,不做页面跳转;页面跳转一律放到页面级onLoad里执行。
6.3 坑三:热更新后直接 restart,新资源没生效
在“热更新 + 重启”这个组合里,官方推荐plus.runtime.applyUpdate()后会触发一次自动重启,但我在部分 Android 机上发现新资源并没有完全生效,App.vue甚至还是旧版本。
后来做法改成:applyUpdate成功后,延迟 1s,再调一次plus.runtime.restart()。这个方法不依赖官方的“自动重启”逻辑,可靠性更高。
plus.runtime.applyUpdate({ success: function() { setTimeout(() => { plus.runtime.restart() }, 1000) }, fail: function() { // 更新失败,提示用户 } })6.4 坑四:iOS 上重启后 getApp() 可能拿到的是旧实例
这个坑比较隐蔽。iOS App 冷启动时,getApp()有极小概率返回旧的 App 实例(内存还没完全清理),导致你后续调用的getApp().globalData是你“以为已经重置”的那份旧数据。
我的应对方式是:在App.vue的onLaunch里主动重置globalData,不依赖系统层面的“全新实例”假设:
onLaunch() { const isRestarting = uni.getStorageSync('is_restarting') this.globalData = { restarting: isRestarting, // ...其他默认值 } if (isRestarting) { uni.removeStorageSync('is_restarting') } }6.5 坑五:重启后首屏广告/启动图异常
有些项目的首屏是一张倒计时广告页,冷重启后会重新走启动流程。如果重启参数里带了reason: 'logout',但启动页的广告逻辑没有判断“这是重启启动”,用户会再次被广告拦住几秒钟,体验很差。
建议在启动页判断is_restarting标记(或restart_params),如果是从重启来的,就跳过广告倒计时,直接进目标页。
6.6 坑六:把冷重启当万能药,滥用后出现“状态丢失”事故
最后这条其实不算“坑”,是心态问题。有的同事遇到页面状态不对,第一反应就是“重启一下”。但冷重启会丢掉用户在页面里还没提交的输入内容、临时选择项、甚至当前浏览位置,体验伤害极大。
我内部定的规矩是:
- 只在应用级状态异常时用冷重启,页面级异常先查代码。
- 调冷重启前先弹确认框,告诉用户“需要重新启动应用完成操作”,别默默重启。
- 每次冷重启都带上 reason 参数,方便线上日志排查是哪条业务触发的重启。
7. 从冷重启到整套状态管理方案:我个人的兜底思路
写到这里,把冷重启的“术”基本讲完了。但如果你只记住了plus.runtime.restart()这一行代码,这篇文的价值就打折了。作为一个在 uniapp 项目里被各种状态问题折磨过的开发者,我的核心体会是:冷重启是一剂猛药,不是保健品。
日常最该做的,还是把状态管理做细:globalData可控、Storage 分级、页面间通信走 uni 官方事件或 Vuex/Pinia。冷重启更像是一个“紧急停机闸”,只有在常规手段确实解决不了问题时才拉响。
我现在的项目实践里,冷重启兜底方案是这么搭配的:
restart_params统一管理冷启动参数is_restarting标记位防止无限重启- 启动页
onLoad做跳转分发 - 每次重启都上报一条日志(带上 reason、时间戳、当前版本号),方便出问题后还原现场
线上跑了半年,冷重启触发的频次不高,但每次触发都能帮助用户从“坏状态”回到“干净状态”,团队内部已经把它当成一套比较可靠的保底机制了。
如果你也准备在自己的 uniapp 项目里上冷重启,建议先拿测试包把各个平台的restart()调一遍,确认没有白屏和参数丢失问题,再谈上线。真到了线上才暴雷,那可就真要被用户“冷启动”了。