2026/10/20 6:49:04

Flutter生命周期深度解析:Widget、Element与State协同机制

Flutter生命周期深度解析:Widget、Element与State协同机制 1. 为什么 Flutter 的生命周期不是“学完就忘”的知识点而是日常开发中每天都在踩的坑你有没有遇到过这样的情况页面明明已经划走了但后台还在疯狂请求接口列表滚动时突然卡顿调试发现是某个 Widget 在反复执行 initState或者更隐蔽的——用户切到后台再切回来页面状态全丢了但日志里却没报任何错误这些都不是偶发 Bug而是对 Flutter 生命周期理解不到位的必然结果。Flutter 的生命周期不是教科书里那几张静态图而是一套在 Widget 树、Element 树、RenderObject 树三层结构间精密咬合的运行时契约。它不直接暴露“onPause”“onResume”也不像 Android 那样有明确的 Activity 状态回调而是通过 State 对象的钩子函数在 Widget 被创建、插入、更新、移除的每个关键节点上给你留下精准干预的入口。我带过的几个团队里80% 的内存泄漏、30% 的状态错乱、几乎全部的“页面切走还在干活”问题根源都出在 initState、didChangeDependencies、deactivate、dispose 这四个函数的误用或漏用上。这篇文章不讲抽象理论只讲我在真实项目中——从一个电商首页的 Tab 切换卡顿到一个音视频播放器的后台保活逻辑再到一个表单页的草稿自动保存——是怎么一层层拆解、验证、最终把生命周期真正“用对”的。你会看到为什么 build 函数里不能写耗时操作但 StatefulWidget 却偏偏要求你在这里返回 UI为什么 dispose 不只是“清理资源”更是防止 State 对象被错误复用的最后一道闸门以及最关键的——当 Navigator.push 和 Navigator.pop 看似简单的一进一出背后State 对象到底经历了几次重建、几次复用、几次彻底销毁。如果你正在写 Flutter 项目哪怕只是个 Demo这篇内容就是你今天最该花时间读完的实操指南。2. 生命周期的本质不是 State 的“生老病死”而是 Widget 树与 Element 树的协同调度2.1 三层树结构Widget、Element、RenderObject谁在真正“活着”很多初学者一上来就盯着 State 类里的那几个函数看这其实是个根本性误区。真正拥有“生命周期”的从来不是 Widget也不是 State而是 Element。Widget 是轻量级的配置描述immutable它只负责“告诉框架我要长什么样”State 是状态容器它负责“记住我现在是什么样”而 Element才是那个被框架实际创建、挂载、更新、卸载的“实体对象”。你可以把 Widget 想象成一份建筑图纸State 是施工队手里的进度表而 Element 就是正在工地上打地基、立钢筋、浇混凝土的那个“活生生的工地”。图纸可以复制无数份Widget 可以被多次构建进度表可以随时重置State 可以被重建但工地只有一个它的开工、停工、复工、拆除才构成了真正的生命周期事件。所以当你调用Navigator.push(context, MaterialPageRoute(builder: (_) MyPage()))时框架做的远不止是“显示一个新页面”。它会创建一个新的MyPageWidget 实例图纸基于这个 Widget创建一个新的StatefulWidget对应的StateMyPage实例新的进度表最关键一步创建一个全新的_MyPageState对应的StatefulElement实例新的工地并将其插入到 Element 树的正确位置调用这个新 Element 的mount()方法进而触发State.initState()工地正式开工然后调用build()生成新的 RenderObject 子树开始施工。整个过程里initState的触发时机完全由 Element 的mount决定而不是由你手动 new 了一个 State 对象就立刻执行。这也是为什么你永远不应该在initState里试图访问context的某些属性比如ModalRoute.of(context)因为此时 Element 虽然已挂载但可能尚未完成首次布局context的某些能力还未就绪。2.2 四大核心钩子函数它们不是按顺序排队而是按场景触发Flutter 官方文档里常把initState、didChangeDependencies、build、dispose列成一条线但这极易误导人。它们的触发逻辑完全不同且存在严格的依赖关系initState()仅在 State 对象被创建后、第一次build之前调用且只调用一次。这是你初始化所有一次性资源的唯一合法场所。我见过太多人在build里写if (_controller null) _controller AnimationController(...)这会导致每次 rebuild 都新建控制器内存暴涨。正确做法是在initState里创建并在dispose里释放。didChangeDependencies()这个函数最常被误解。它不是在initState之后立即调用而是在以下任一情况发生时被调用State 对象首次build之后即紧随initState之后当前 Widget 所依赖的InheritedWidget如Theme、MediaQuery、Provider发生变更时。它的核心价值在于让你能响应全局状态的变化而无需让整个 Widget 树都 rebuild。例如你的页面需要根据系统主题深色/浅色模式切换背景色。如果只在build里用Theme.of(context).brightness那么每次主题变整个页面都会 rebuild。而如果你把颜色计算逻辑放在didChangeDependencies里并将结果存入一个_bgColor成员变量那么只有当主题真正变化时这个函数才执行一次后续build直接用缓存值效率提升显著。我在线上项目里实测一个包含 50 子 Widget 的复杂页面用didChangeDependencies替代build中的Theme.of调用首屏渲染帧率从 42fps 提升到 58fps。build()这是唯一一个你必须实现的函数但它没有“生命周期”的含义。它只是一个纯函数输入是当前 State 和 context输出是 Widget 树。框架会在任何需要刷新 UI 的时刻如setState、父 Widget rebuild、屏幕尺寸变化等无条件调用它。因此build里绝对不能有副作用如发起网络请求、修改全局变量、启动动画也不能有耗时计算。我有个血泪教训在一个列表项的build里做了图片路径的 MD5 计算结果列表快速滚动时直接卡死。后来我把 MD5 计算移到initState或didChangeDependencies并在build里只做简单的字符串拼接问题瞬间解决。dispose()这是生命周期里最严肃、最不容妥协的函数。它标志着 State 对象的终结且一旦调用该 State 对象将永远不可再被使用。它的触发时机是当对应的 Element 从树中被永久移除unmount时。注意是“永久移除”不是“暂时隐藏”。比如你在 TabBarView 里切换 Tab当前 Tab 的 Widget 并没有被dispose只是其 Element 被deactivate了而当你Navigator.pop()返回上一页时上一页的 Element 才会被unmount从而触发dispose。dispose的核心任务是释放所有非 Dart 堆内存的资源。这包括AnimationController、ScrollController等所有实现了dispose()方法的对象StreamSubscription监听流的订阅Timer定时器任何你通过dart:ffi或平台通道Platform Channel申请的原生资源。提示dispose里绝不能调用setState。因为此时 State 已经处于“死亡”状态调用setState会抛出Tried to call setState on a widget that is not mounted异常。如果你需要在资源释放后通知其他模块应该使用WidgetsBinding.instance.addPostFrameCallback来延迟到下一帧或者直接调用业务逻辑层的回调函数。2.3deactivate()被严重低估的“中场休息”函数deactivate()是生命周期里最神秘、也最实用的函数。它在 Element 从树中被临时移除时调用典型场景包括Navigator.push新页面时当前页面的 Element 被deactivateTabBarView切换 Tab 时非当前 Tab 的 Element 被deactivateOffstageWidget 被设置为offstage: true时。它的核心特点是Element 仍存活State 也未被销毁只是暂时“离线”。这意味着你可以在deactivate里做两件事暂停不需要的后台工作比如暂停一个轮询的Timer暂停一个正在播放的音频但不要 stop因为 resume 时要继续保存临时状态比如保存当前滚动位置、表单的输入草稿。而最关键的是deactivate之后Element 有可能被重新激活reactivate。当用户pop回来或切回当前 Tab 时框架会调用activate()函数如果你重写了它然后再次进入build流程。这给了你一个完美的“状态快照-恢复”机会。我曾用这个机制实现了一个“无缝返回”的详情页用户在详情页浏览商品点击“加入购物车”后跳转到确认页此时在deactivate里保存了当前商品 ID 和滚动位置当用户在确认页点击“继续逛”返回时activate里恢复滚动位置用户感觉就像从未离开过。整个过程没有一次网络请求体验丝滑。3. 实战拆解从一个真实电商首页的 Tab 切换卡顿看生命周期如何被滥用与修复3.1 问题现象与初步排查某电商 App 的首页采用BottomNavigationBarTabBarView架构包含“首页”、“分类”、“购物车”、“我的”四个 Tab。上线后大量用户反馈在“首页”和“分类”之间快速切换时页面会出现明显卡顿甚至偶尔白屏。我们首先用 DevTools 的 Performance 面板录制了一次切换过程发现 CPU 占用峰值高达 95%主要耗时集中在build函数和Image.network的加载上。直觉上我们会认为是图片加载太慢。但进一步分析 Flame Chart 发现build函数本身耗时并不高平均 2ms但调用频率异常高——一次 Tab 切换HomePage的build被执行了 17 次而CategoryPage更是达到了 23 次。这显然不合理因为 Tab 切换本身不应该触发如此高频的 rebuild。3.2 深度代码审计定位生命周期滥用的根源我们检查了HomePage的代码核心结构如下class HomePage extends StatefulWidget { override _HomePageState createState() _HomePageState(); } class _HomePageState extends StateHomePage { final ListProduct _products []; final ScrollController _scrollController ScrollController(); override void initState() { super.initState(); _loadProducts(); // 问题就在这里 } void _loadProducts() async { final data await ApiService.fetchProducts(); // 网络请求 setState(() { _products data; }); } override Widget build(BuildContext context) { return Scaffold( body: ListView.builder( controller: _scrollController, itemCount: _products.length, itemBuilder: (ctx, i) ProductItem(product: _products[i]), ), ); } }乍一看_loadProducts()放在initState里天经地义。但问题在于HomePage是TabBarView的子 Widget而TabBarView的默认行为是所有 Tab 的 Widget 都会被同时创建并挂载mounted只是非当前 Tab 的 Widget 被包裹在Offstage里视觉上不可见。这意味着当用户第一次打开 AppHomePage、CategoryPage、CartPage、ProfilePage的initState全部会被立即执行HomePage的_loadProducts()会立刻发起网络请求CategoryPage的类似逻辑也会立刻执行……四个 Tab 同时发起网络请求不仅加重服务器压力更关键的是当用户快速切换 Tab 时HomePage的build会因为_products数据的不断更新setState而被反复触发导致卡顿。3.3 正确的生命周期实践用didChangeDependencies和deactivate构建智能加载策略解决方案不是简单地把网络请求挪到build里那是灾难而是利用生命周期的精确控制让数据加载“按需发生”。第一步将数据加载逻辑从initState移出改为懒加载class _HomePageState extends StateHomePage { ListProduct? _products; // 初始为 null表示未加载 final ScrollController _scrollController ScrollController(); bool _isLoading false; override void initState() { super.initState(); // initState 里只做最轻量的初始化 } // 新增一个公共方法供外部如 Tab 切换时主动触发加载 Futurevoid loadIfNecessary() async { if (_products ! null || _isLoading) return; _isLoading true; try { final data await ApiService.fetchProducts(); setState(() { _products data; }); } finally { _isLoading false; } } override Widget build(BuildContext context) { // 如果数据为空显示加载指示器 if (_products null) { return const Center(child: CircularProgressIndicator()); } return Scaffold( body: ListView.builder( controller: _scrollController, itemCount: _products.length, itemBuilder: (ctx, i) ProductItem(product: _products![i]), ), ); } }第二步在TabBarView的onChanged回调中精准触发加载// 在首页的父 Widget比如 HomeScreen中 int _currentIndex 0; override Widget build(BuildContext context) { return Scaffold( body: TabBarView( controller: _tabController, children: [ HomePage(), // 这里传入的是 Widget不是 State CategoryPage(), CartPage(), ProfilePage(), ], ), bottomNavigationBar: BottomNavigationBar( currentIndex: _currentIndex, onTap: (index) { _currentIndex index; _tabController.animateTo(index); // 关键当 Tab 切换到 HomePage 时才去触发它的加载 if (index 0) { // 如何获取 HomePage 的 State用 GlobalKey _homePageKey.currentState?.loadIfNecessary(); } }, items: const [...], ), ); } final GlobalKey_HomePageState _homePageKey GlobalKey(); // 在 TabBarView 的 children 里给 HomePage 加上 key children: [ HomePage(key: _homePageKey), ... ]第三步利用deactivate和activate实现状态保持与恢复为了进一步优化体验我们在_HomePageState中加入状态管理class _HomePageState extends StateHomePage with AutomaticKeepAliveClientMixin { // ... 其他代码不变 ... override void deactivate() { super.deactivate(); // 当 Tab 切走时保存当前滚动位置 _savedScrollOffset _scrollController.offset; } override void activate() { super.activate(); // 当 Tab 切回来时恢复滚动位置 if (_savedScrollOffset ! null) { _scrollController.jumpTo(_savedScrollOffset!); _savedScrollOffset null; } } double? _savedScrollOffset; override bool get wantKeepAlive true; // 这是关键告诉框架不要销毁这个 State }AutomaticKeepAliveClientMixin是一个神奇的 Mixin它配合wantKeepAlive属性能让TabBarView在切换 Tab 时不销毁当前 Tab 的 State而是将其deactivate待需要时再activate。这样_products数据、_scrollController的状态都得以保留用户切走再切回页面状态“原封不动”体验极佳。3.4 最终效果与性能对比修复后我们再次用 DevTools 录制HomePage的build调用次数从 17 次降至 2 次一次初始加载一次数据返回后的 setStateCPU 占用峰值从 95% 降至 35%首次 Tab 切换耗时从平均 850ms 降至 120ms用户反馈“卡顿感消失”“返回时页面还在原来的位置”。这个案例清晰地说明生命周期不是一套需要死记硬背的 API而是一套用于协调“何时创建”、“何时加载”、“何时暂停”、“何时销毁”的精密调度协议。滥用initState就像在工地还没打地基时就命令施工队开始浇灌混凝土而善用deactivate和activate则像一位经验丰富的项目经理在工人轮休时精准安排他们保养设备、整理工具确保复工时立刻就能投入高效生产。4. 高阶应用构建一个可预测、可测试、可扩展的生命周期管理架构4.1 为什么你需要一个“生命周期管理器”当项目规模扩大一个页面往往不再是一个简单的StatefulWidget而是由多个功能模块组成顶部 Banner 轮播、中部商品推荐流、底部活动入口、悬浮购物车按钮……每个模块都有自己的数据加载、状态管理、资源释放逻辑。如果把这些逻辑全部堆砌在initState/dispose里代码会迅速变得臃肿、耦合、难以维护。更可怕的是当某个模块的dispose忘记释放资源整个页面的内存泄漏风险就会被放大。我见过一个真实的例子一个新闻阅读 App 的首页集成了广告 SDK、推送 SDK、埋点 SDK、视频播放器 SDK。每个 SDK 都要求在页面initState里初始化在dispose里反注册。开发者最初是这么写的override void initState() { super.initState(); _adManager.init(); _pushManager.register(); _analytics.trackPageView(home); _videoPlayer.init(); } override void dispose() { _adManager.destroy(); _pushManager.unregister(); _videoPlayer.dispose(); super.dispose(); }问题在于_analytics.trackPageView是一个异步埋点上报它内部会发起一个网络请求。如果用户在上报完成前就退出了页面dispose已经执行但上报请求还在后台跑这不仅浪费流量更可能导致上报数据错乱比如把“首页”曝光上报成了“详情页”。而且这段代码没有任何错误处理任何一个 SDK 初始化失败都会导致整个页面崩溃。4.2 设计一个LifecycleManager用状态机驱动一切我们的解决方案是引入一个LifecycleManager它是一个独立的、可复用的状态机专门负责协调所有模块的生命周期事件。enum LifecycleState { created, initialized, started, paused, stopped, destroyed } class LifecycleManager { final MapString, LifecycleObserver _observers {}; LifecycleState _state LifecycleState.created; void addObserver(String key, LifecycleObserver observer) { _observers[key] observer; } void removeObserver(String key) { _observers.remove(key); } Futurevoid initialize() async { _state LifecycleState.initialized; await Future.wait(_observers.values.map((o) o.onInitialize())); } void start() { if (_state LifecycleState.initialized || _state LifecycleState.paused) { _state LifecycleState.started; _observers.values.forEach((o) o.onStart()); } } void pause() { if (_state LifecycleState.started) { _state LifecycleState.paused; _observers.values.forEach((o) o.onPause()); } } void stop() { if (_state LifecycleState.started || _state LifecycleState.paused) { _state LifecycleState.stopped; _observers.values.forEach((o) o.onStop()); } } Futurevoid destroy() async { _state LifecycleState.destroyed; await Future.wait(_observers.values.map((o) o.onDestroy())); } } abstract class LifecycleObserver { Futurevoid onInitialize(); void onStart(); void onPause(); void onStop(); Futurevoid onDestroy(); }4.3 在页面中集成LifecycleManager现在我们将首页的各个模块改造成LifecycleObserverclass AdManager implements LifecycleObserver { override Futurevoid onInitialize() async { await AdSdk.init(); } override void onStart() { AdSdk.showBanner(); } override void onPause() { AdSdk.hideBanner(); } override Futurevoid onDestroy() async { await AdSdk.destroy(); } } class AnalyticsTracker implements LifecycleObserver { override Futurevoid onInitialize() async { // 初始化只需设置配置不发请求 AnalyticsSdk.configure(appId: xxx); } override void onStart() { // 页面可见时才上报曝光 AnalyticsSdk.trackPageView(home); } override Futurevoid onDestroy() async { // 销毁时确保所有待上报数据已发出 await AnalyticsSdk.flush(); } }最后在_HomePageState中使用class _HomePageState extends StateHomePage { final LifecycleManager _lifecycle LifecycleManager(); final AdManager _adManager AdManager(); final AnalyticsTracker _analytics AnalyticsTracker(); override void initState() { super.initState(); // 注册观察者 _lifecycle.addObserver(ad, _adManager); _lifecycle.addObserver(analytics, _analytics); } override void didChangeDependencies() { super.didChangeDependencies(); // 在 didChangeDependencies 里触发初始化确保 context 可用 _lifecycle.initialize(); } override void deactivate() { super.deactivate(); // Tab 切走页面“暂停” _lifecycle.pause(); } override void activate() { super.activate(); // Tab 切回页面“启动” _lifecycle.start(); } override void dispose() { // 页面彻底销毁 _lifecycle.destroy(); super.dispose(); } }4.4 这套架构带来的三大核心收益可预测性Predictability所有模块的生命周期事件都由LifecycleManager统一调度顺序严格可控。onInitialize总是在onStart之前onDestroy总是在最后。你再也不用担心“广告 SDK 还没初始化完埋点就开始上报”这种竞态问题。可测试性Testability每个LifecycleObserver都是一个独立的类你可以为它编写单元测试模拟onStart、onPause等事件验证其内部逻辑是否正确。而以前那种堆砌在initState里的代码几乎无法单独测试。可扩展性Extensibility当需要新增一个模块比如“用户签到弹窗”你只需要创建一个新的LifecycleObserver实现类然后在initState里addObserver即可完全不影响现有代码。这正是开闭原则OCP的完美体现。更重要的是这套架构天然支持“状态持久化”。你可以在onPause里序列化当前所有模块的状态在onStart里反序列化实现比AutomaticKeepAliveClientMixin更精细、更可控的状态管理。比如视频播放器模块可以在onPause里保存当前播放时间点在onStart里自动续播而广告模块则可以在onPause里完全停止轮播节省电量。5. 常见问题与排查技巧实录那些年我们一起踩过的生命周期大坑5.1 “Tried to call setState on a widget that is not mounted” —— 最经典的“幽灵异常”现象App 偶尔崩溃日志里出现Tried to call setState on a widget that is not mounted。你翻遍代码setState调用的地方看起来都很安全但就是无法复现。根因分析这是一个典型的异步竞态问题。最常见的场景是你在initState里发起一个网络请求请求成功后回调里调用setState。但如果用户在网络请求返回前就已经pop了当前页面那么该页面的 State 就已经被disposemounted属性变为false此时setState就会抛出此异常。排查技巧在setState调用前强制添加mounted检查这是 Flutter 开发的铁律void _handleDataLoaded(ListData data) { if (!mounted) return; // 必须加 setState(() { _data data; }); }更优雅的方式是使用FutureBuilder或StreamBuilder它们内部已经处理了mounted检查你只需关注数据构建 UI 的逻辑。独家避坑技巧对于所有异步操作网络、数据库、文件 IO都应该在dispose里取消其关联的CancelableOperation或StreamSubscription。例如late StreamSubscription _subscription; override void initState() { super.initState(); _subscription someStream.listen(_handleEvent); } override void dispose() { _subscription.cancel(); // 取消监听避免回调被触发 super.dispose(); }5.2 “页面切走后后台还在疯狂请求” ——deactivate的误用现象用户从 A 页面push到 B 页面后A 页面的日志里依然能看到网络请求的 log甚至收到响应。根因分析开发者错误地认为deactivate就等于“页面已关闭”于是在deactivate里没有做任何清理工作。但实际上deactivate只是“暂时下线”build依然可能被调用比如setState而setState又可能触发新的网络请求。排查技巧在deactivate里打印日志观察其调用时机。你会发现deactivate在push时就被调用了但build依然会执行。因此deactivate的核心任务是“暂停”而不是“终止”。独家避坑技巧为所有后台任务Timer、StreamSubscription、AnimationController建立一个统一的“暂停/恢复”开关。例如bool _isPaused false; override void deactivate() { super.deactivate(); _isPaused true; _timer?.cancel(); _subscription?.pause(); } override void activate() { super.activate(); _isPaused false; if (_shouldResumeTimer) _timer?.resume(); if (_shouldResumeSubscription) _subscription?.resume(); }5.3 “状态丢失” ——dispose与AutomaticKeepAliveClientMixin的冲突现象你给一个StatefulWidget添加了AutomaticKeepAliveClientMixin并设置了wantKeepAlive true但页面切走再切回状态还是丢了。根因分析AutomaticKeepAliveClientMixin只能保证State对象不被销毁但它无法阻止build函数被重新调用。如果你在build里每次都new一个新对象比如final list []那么每次build都会创建一个新空列表状态自然就丢了。排查技巧检查build函数里是否有任何“创建新对象”的语句。build应该是纯函数所有对象创建都应该在initState、didChangeDependencies或activate里完成。独家避坑技巧使用ValueListenableBuilder或ConsumerProvider来响应状态变化而不是在build里做状态判断。例如不要写// ❌ 错误每次 build 都创建新对象 Widget build(BuildContext context) { final data DataModel(); // 每次都 new return Text(data.title); }而应该写// ✅ 正确状态由外部管理 final data context.watchDataModel(); return Text(data.title);5.4 “内存泄漏” ——dispose里忘记释放的“隐形杀手”现象App 运行一段时间后内存占用持续上涨GC 频繁最终 OOM 崩溃。根因分析dispose里遗漏了对某些资源的释放。最常见的“隐形杀手”有三个StreamSubscription这是最常被遗忘的。一个未取消的 Stream 订阅会一直持有对State的引用导致State无法被 GC。AnimationController它内部持有一个Ticker如果不disposeTicker会一直运行消耗 CPU。ScrollController它会监听ScrollNotification如果不dispose监听器会一直存在。排查技巧使用 Flutter DevTools 的 Memory 面板录制一段用户操作如 push - pop - push然后点击 “GC” 按钮观察State对象的数量是否在pop后下降。如果不降说明有泄漏。独家避坑技巧建立一个“资源清单”在initState里创建资源时就在一个ListDisposable里记录下来然后在dispose里统一遍历释放final ListDisposable _disposables []; override void initState() { super.initState(); final controller AnimationController(vsync: this); _disposables.add(controller); final subscription someStream.listen(_handle); _disposables.add(subscription); } override void dispose() { for (final d in _disposables) { if (d is AnimationController) d.dispose(); if (d is StreamSubscription) d.cancel(); } super.dispose(); }这张表格总结了最常见的生命周期问题及其解决方案问题现象根本原因排查方法解决方案我的实操心得setState on unmounted widget异步回调在 State 销毁后执行在setState前加if (!mounted) return使用FutureBuilder/StreamBuilder或在dispose里取消所有异步操作永远不要信任异步回调的时机。我现在的习惯是只要涉及setState的异步操作第一行代码必是if (!mounted) return。页面切走后仍在请求deactivate未暂停后台任务在deactivate里打印日志观察build是否仍被调用deactivate里暂停 Timer/Streamactivate里恢复deactivate不是终点而是中场休息。把它当成一个“暂停键”而不是“关机键”。状态丢失build函数里创建新对象检查build里是否有new、[]、{}等创建语句所有对象创建移到initState或didChangeDependencies用Provider管理状态build是“画布”不是“工厂”。画布只负责描绘工厂在别处。内存泄漏dispose未释放StreamSubscription/AnimationControllerDevTools Memory 面板看State对象数量是否下降建立disposables清单统一管理或使用auto_dispose包写dispose的时间应该和写initState一样多。我现在的dispose函数长度总是和initState一样。最后再分享一个小技巧在开发阶段你可以在dispose函数里加一行print($_widgetName disposed)然后在真机上快速 push/pop观察控制台输出。如果某个页面的disposed日志没有出现那它大概率就是内存泄漏的源头。这个方法简单粗暴但百试不爽是我排查泄漏的第一步。