转写说明
本文基于已校验的公开原文进行结构化转写与事实梳理,非原文转载。 转写保留可核验的技术事实,并将工程建议与来源观点明确分开。
- 原作者: 大龄秃头程序员
- 原始来源: https://juejin.cn/post/7677803387144388617
- 原文发布时间: Tue, 25 Aug 2026 09:59:34 GMT
核心结论
该问题根因并非图片解码耗时,而是 Dart 单线程事件循环被高频通知队列阻塞。WebSocket 未读数更新每秒触发多次全局 notifyListeners(),导致 UI 层的缩略图渲染通知被挤压到队列后方。实测场景中,Picker 返回后缩略图出现前的空档期达 17 秒,而同一时段未读数已跳动 10 次。
次生问题包括:架构拆分时中介者遗漏部分监听注册、筛选型 Notifier 在 build 中重复创建导致监听器泄漏、iOS 模拟器忽略 maxWidth 参数且 Flutter 隐式下采样行为不可靠。
能力机制
全链路埋点法:沿四层架构在每个跨层边界设置 Entry/Exit 日志,通过统一前缀过滤实现快速定位。调用链从 UI 点击延伸至系统相册调用,再回到 UI 渲染,共覆盖 9 个节点。
细粒度通知筛选:实现专用 ChangeNotifier 监听目标字段(selectedImages、语音状态等),隔离与 UI 渲染无关的状态变化,避免 WebSocket 未读数等高频事件触发不必要的 rebuild。
事件节流:使用 100ms 窗口合并通知,在首帧立即通知保证及时性,窗口内累积变更补发一次保证最终一致性。
显式图片下采样:弃用 Image.file 的 cacheWidth 参数,改用 ResizeImage 强制指定解码尺寸,不依赖引擎隐式行为。
快速开始
日志前缀约定:在 Debug Console 使用 Check-images-bug 前缀过滤全链路日志。
节流实现模板:
| |
显式下采样写法:
| |
Picker 侧设置 requestFullMetadata: false 可跳过 EXIF 读取,在模拟器场景节省约 1 秒。
适用边界
该方案针对 Dart 单线程模型下的事件队列竞争问题,适用于 Flutter 应用中同时存在高频状态更新(如 WebSocket 推送)与 UI 敏感操作(如图片预览)的场景。
iOS 模拟器配合 HEIC 格式时,pickMultiImage 的 maxWidth 参数可能不生效,仍返回全尺寸原图。Flutter 3.x 在 ImageCache 命中后存在绕过 instantiateImageCodec 下采样分支的情况,此时 cacheWidth 参数不可靠。
中介者模式拆分类时,必须确保 addListener 与 removeListener 成对出现,否则会导致通知链路断裂或监听器泄漏。筛选型 Notifier 应作为 State 字段管理生命周期,避免在 build 方法中创建。
核验清单
架构拆分完成后,检查 AiChatCoordinator 的 _setupCrossControllerListeners 方法中各子控制器的监听注册数量是否与 dispose 中的 removeListener 调用数量相等。
检查 InputBar 相关 Notifier 是否作为 State 字段单例存在,而非每次 build 时新建。若存在泄漏,可观察到同一通知被回调次数递增。
验证 SessionEventController 或类似高频通知源是否已实现节流机制。原始每秒 8-12 次通知经节流后应控制在 10 次以内。
验证缩略图加载路径是否使用 ResizeImage 显式指定尺寸,而非依赖 cacheWidth 参数隐式下采样。解码耗时目标应低于 50ms。
Picker 返回后缩略图出现前的空档期应控制在毫秒级,体感无等待。端到端耗时应控制在 5 秒以内,其中系统相册选图和文件拷贝时间不可控。
来源与核验
- 原始文章
- 页面事实以原始来源及其引用的官方资料为准;版本、星标和模型能力会随时间变化。
- AI Stack 不公开抓取到的全文快照,只发布独立转写与来源入口。