@[toc]
------为什么你的页面已经"关掉了",却从来没被释放
如果你做过一段时间 RN,一定听过这些说法:
- "这个页面返回后好像越来越卡"
- "切几次页面,内存就回不去了"
- "Android 上跑久了直接 OOM"
然后你开始:
- 查
useEffect有没有 return cleanup - 看定时器、事件监听有没有 remove
- 怀疑是不是某个三方库有问题
但最后你会发现一个很反直觉的事实:
有些页面,你写得再干净,只要还在 Navigation State 里,它就不可能被 GC。
一、先统一一个关键认知:页面是否可见 ≠ 页面是否存活
这是 RN 和 Web 最大的认知差异之一。
在 Web 里
- 路由切换
- 组件卸载
- 内存自然释放(理论上)
在 RN + Navigation 里
- 页面不可见
- 但 仍然挂在导航栈中
- 组件实例依然存在
- Hook、闭包、订阅都还活着
一句话总结:
Navigation State 决定了页面"生死",而不是 UI。
二、Navigation State 如何"托住"页面内存
我们先从机制层说清楚这件事。
1. 页面组件是被 Navigator 持有的
当你写:
js
navigation.navigate('Detail')
本质上发生的是:
- Navigator 创建一个 route
- route 引用你的页面组件
- 这个 route 被保存在 Navigation State 中
只要这个 route 还在:
- 页面组件不会被卸载
- React Fiber 仍然存在
- JS 对象依然被引用
GC 根本没机会介入。
2. 多层嵌套 Navigator = 多层"内存保温箱"
看一个常见结构:
text
RootStack
└─ MainTabs
└─ HomeStack
├─ Home
├─ Detail
└─ HeavyPage
你从 HeavyPage 返回 Home,UI 上看起来一切正常。
但实际上:
js
routes: [
Home,
Detail,
HeavyPage // 还在
]
如果 HeavyPage 里有:
- 大列表
- WebView
- 视频播放器
- 全局事件监听
它们一个都没死。
三、最容易被忽略的三类"导航型内存泄漏"
这些泄漏,几乎都和 Navigation State 直接相关。
第一类:流程页面不 reset
这是最常见、也最隐蔽的一类。
场景
List → Detail → Pay → Result
你用的是:
js
navigate('Detail')
navigate('Pay')
navigate('Result')
流程结束后:
- UI 回到了 Result
- 但 State 是:
js
[List, Detail, Pay, Result]
整个流程链条全活着。
问题在哪?
- Detail 可能订阅了数据
- Pay 可能初始化了 SDK
- List 可能持有缓存引用
你只是"视觉上结束了流程",
但架构上从未结束。
第二类:Modal / 临时页面长期滞留
很多人喜欢用 Stack 来做 Modal:
js
<Stack.Screen
name="ImagePreview"
component={ImagePreview}
/>
然后:
js
navigation.navigate('ImagePreview')
问题是:
- 你几乎从不 reset 它
- 只是
goBack
结果:
js
routes: [
Home,
ImagePreview // 曾经打开过一次,就一直存在
]
如果这个 Modal 里:
- 加载了大图
- 建立了手势监听
- 做了动画缓存
一次打开,永久驻留。
第三类:被"认为已卸载"的 Tab 页面
Tab Navigator 是内存泄漏的高发区。
常见误解
"我切走 Tab 了,这个页面应该被销毁了吧?"
实际上:
- Tab 默认 不会卸载页面
- 只是切换 focus
- 页面实例仍然完整存在
如果你在 Tab 页面里写了:
js
useEffect(() => {
startPolling()
}, [])
恭喜你:
这个轮询可能会跑到 App 退出。
四、用 Navigation State 定位"内存不该存在的地方"
这一步非常关键,也是很多人没做过的。
1. 打印完整 Navigation State
js
<NavigationContainer
onStateChange={(state) => {
console.log(JSON.stringify(state, null, 2))
}}
>
不要怕长,越长越有价值。
2. 对 State 做"页面尸检"
每看到一个 route,问三个问题:
问题一:这个页面此刻还有业务意义吗?
- 没有 → 该 reset
- 有 → 继续留
问题二:它内部是否持有重资源?
- WebView
- 视频
- 地图
- 大列表
- 全局监听
如果有,但长期不活跃,这是高风险对象。
问题三:它的"退出条件"是否明确?
很多页面只有进入逻辑,没有退出策略。
没有退出策略的页面,迟早变成内存垃圾场。
五、Demo:Navigation State 导致的真实内存问题
场景:一个"看似没问题"的列表页
js
function ListPage() {
useEffect(() => {
const timer = setInterval(fetchData, 5000)
return () => clearInterval(timer)
}, [])
return <List />
}
你以为:
"我写了 cleanup,没问题。"
但如果:
- ListPage 一直在 Navigation State 里
- 只是被压在栈底
那么:
useEffect永远不会 cleanup- 定时器永远在跑
正确做法一:流程结束主动 reset
js
navigation.reset({
index: 0,
routes: [{ name: 'Home' }]
})
这一步不是优化,是释放内存的必要条件。
正确做法二:结合 focus 生命周期
js
import { useFocusEffect } from '@react-navigation/native'
useFocusEffect(
React.useCallback(() => {
startPolling()
return () => stopPolling()
}, [])
)
但注意:
focus 生命周期 ≠ 卸载生命周期
它只是缓解,不是根治。
六、成熟 RN 项目是如何"用导航管理内存"的
经验总结下来,通常有这几条硬约束。
1. 流程型页面必须 reset
- 下单
- 支付
- 注册
- 新手引导
流程结束 = Navigator 销毁
2. Modal 页面必须是"即用即销"
- 不允许长期留在 State
- 要么 replace
- 要么 reset
3. Tab 页面默认"轻量化"
- 不做长期副作用
- 不持有大对象
- 把重资源放到子页面
4. Navigation State 定期"体检"
- 打印
- 分析
- 对比版本变化
就像数据库 schema 一样对待它。
七、结语:内存问题,往往不是写坏了,而是"没被结束"
很多 RN 内存问题,本质不是:
- 没写 cleanup
- API 用错
而是:
页面从未真正退出舞台。
Navigation State 就是那张后台名单,
只要名字还在,演员就还领着工资。
当你开始:
- 用 State 看页面生死
- 用 reset 管流程生命周期
- 用导航结构限制页面职责
你会发现:
很多"玄学内存问题",突然就变得非常理性。