Flutter 图片库与 Dio 库优化实战指南

聚焦两大高频优化主题:图片加载/缓存全链路优化,以及 Dio 网络库的性能与工程化优化。每个点都给出"为什么有效"的原理依据,而不只是操作清单。


目录

Part 1:图片库优化

  1. 图片加载全链路总览

  2. 解码阶段优化:避免"大图小显"

  3. 内存缓存(ImageCache)优化

  4. 磁盘缓存与网络图片库选型

  5. 列表场景专项优化

  6. 格式与资源包体积优化

  7. 动图(GIF/APNG/Lottie)优化

  8. 内存泄漏排查

  9. 图片库优化优先级排序

Part 2:Dio 库优化

  1. Dio 请求生命周期与拦截器机制

  2. 连接层优化:连接池与 HTTP/2

  3. 缓存策略:减少重复请求

  4. 并发控制与请求合并/去重

  5. 超时、重试与降级策略

  6. 数据体积优化:压缩与序列化

  7. 错误处理与统一拦截器设计

  8. 大文件上传下载优化

  9. 安全与稳定性

  10. Dio 优化优先级排序

  11. 高频面试题 Q&A 速查


Part 1:图片库优化

1. 图片加载全链路总览

一张网络图片从请求到显示,经过的完整链路:

arduino 复制代码
网络请求(Dio/HttpClient)

   → 字节数据(Bytes)

   → 磁盘缓存(可选,如 flutter_cache_manager)

   → 解码(Codec,独立线程/IO 线程,产出 GPU 纹理)

   → 内存缓存(ImageCache,缓存的是解码后的 ui.Image/纹理)

   → 显示在 RenderObject 上(合成进 Layer 树)

   → 滑出视口/页面销毁 → 从 ImageCache 淘汰 → 纹理内存释放

核心认知 :图片优化本质是在链路的每一个环节减少不必要的字节传输、不必要的解码运算、不必要的内存占用。任何单点优化都只覆盖链路的一部分,需要组合使用。


2. 解码阶段优化:避免"大图小显"

2.1 cacheWidth/cacheHeight ------ 收益最大的单点优化

dart 复制代码
Image.network(

  url,

  cacheWidth: 200,

  cacheHeight: 200,

)

原理 :不加这两个参数时,Skia/Impeller 会按图片原始分辨率 解码生成纹理,哪怕布局上只显示成很小的尺寸(比如 width: 100)。一张 4000x3000 的照片按原始分辨率解码,可能占用几十 MB 的 GPU 纹理内存;加了 cacheWidth/cacheHeight 后,解码器在解码阶段就按目标尺寸做降采样,内存占用可以降低一个量级(按面积比例计算,缩小到 1/4 尺寸大约是原内存的 1/16)。

注意 :cacheWidth/cacheHeight 的单位是物理像素 ,需要结合 devicePixelRatio 换算,通常写法是:

dart 复制代码
final targetWidth = (displayWidth * MediaQuery.of(context).devicePixelRatio).round();

2.2 按显示尺寸请求网络图(服务端裁图)

如果后端/CDN 支持裁图参数(阿里云 OSS、腾讯云 COS 等常见图片处理服务都支持 URL 参数裁图),直接请求接近显示尺寸的图片,而不是下载原图再靠本地 cacheWidth 降采样------这样能同时节省网络流量和本地解码前的字节体积,收益比单纯本地降采样更大(本地降采样只省内存,不省流量和下载时间)。

2.3 解码是异步的,但纹理内存才是真正压力点

instantiateImageCodec 的解码过程被调度到独立线程(不阻塞 UI 主线程),CPU 耗时本身不是主要问题;真正的性能压力来自解码结果占用的 GPU 纹理内存------纹理内存耗尽会导致系统主动回收(可能造成掉帧甚至 App 因内存压力被系统杀死),这也是为什么优化重点始终围绕"减少同时存在的纹理数据总量",而不是"加快解码速度"。


3. 内存缓存(ImageCache)优化

3.1 默认容量与调整

Flutter 全局唯一的 ImageCache(PaintingBinding.instance.imageCache)有默认上限(张数上限 + 字节上限双重限制,具体数值随 SDK 版本可能调整,以当前版本文档为准)。图片密集场景很容易触顶,触顶后按 LRU(最近最少使用)淘汰,淘汰后如果又被访问到就要重新走一遍解码流程,造成"缓存抖动"(本该命中缓存却频繁重新解码)。

dart 复制代码
PaintingBinding.instance.imageCache.maximumSize = 200;          // 张数上限

PaintingBinding.instance.imageCache.maximumSizeBytes = 50 << 20; // 字节上限,50MB

调整策略:

  • 图片"小而多"(表情包、图标):优先调大 maximumSize(张数上限),字节上限影响不大。

  • 图片"少而大"(相册大图、详情图):优先关注 maximumSizeBytes,张数上限可能不需要调整。

3.2 缓存 Key 与重复解码陷阱

ImageCache 是按 ImageProvider 的 key(通常包含 URL + 缩放参数等)做区分的。同一张图片如果在两处使用了不同的 **cacheWidth** / **cacheHeight **(比如列表缩略图用了 cacheWidth: 100,详情页大图用了 cacheWidth: 800),会被当成两个不同的缓存条目分别缓存、分别占用内存------不会互相复用。这是实际项目里常被忽略的"隐性重复解码"来源,排查内存异常时要重点检查同一资源是否被多套不同参数重复请求。

3.3 listen: false / precache 的合理使用

precacheImage(imageProvider, context) 可以提前触发解码并写入 ImageCache,用于"即将进入视口"的图片预加载,减少用户滑动到时的加载闪烁。但不要无限预加载------预加载的图片如果短期内不会被展示,等于提前占用了缓存容量,反而可能把真正即将展示的图片挤出缓存,需要根据实际视口滑动速度评估预加载范围(通常预取下 1~2 屏即可)。


4. 磁盘缓存与网络图片库选型

4.1 原生 Image.network 没有磁盘缓存

Image.network 底层用的是 NetworkImage,每次实例化都会重新走网络请求(HTTP 响应头里的缓存策略在部分平台/场景下可能生效,但不稳定、不可控),没有开发者可控的磁盘缓存机制。生产项目几乎必须引入第三方库补齐这一层。

4.2 常用库对比

| 库 | 核心能力 | 适用场景 |

|---|---|---|

| cached_network_image | 基于 flutter_cache_manager,网络图自动落盘缓存,二次加载直接读磁盘;支持 placeholder/errorWidget/淡入动画/自定义 CacheManager | 最通用,绝大多数项目首选 |

| flutter_cache_manager | cached_network_image 的底层依赖,也可单独使用做通用文件缓存(不限于图片) | 需要自定义缓存策略/缓存文件不限图片时 |

| extended_image | 功能更全,支持手势缩放、编辑、失败重试策略,也内置磁盘缓存 | 需要图片查看器/编辑器等复杂交互 |

4.3 为什么不建议自己写磁盘缓存

自己实现磁盘缓存容易漏掉几个关键细节:

  • 并发去重:同一 URL 被多个 Widget 同时请求时,应该合并成一次网络请求(下载中的请求被多方共享结果),而不是并发发起多份下载,浪费带宽。

  • 缓存淘汰策略:磁盘缓存也需要容量上限和 LRU 淘汰,否则长期使用后缓存文件无限增长。

  • 缓存有效期与校验 :需要处理 HTTP 缓存头(ETag/Cache-Control)或自定义过期策略,判断缓存是否需要重新拉取。

成熟社区库已经处理好这些边界情况,自己重写的性价比很低。


5. 列表场景专项优化

5.1 懒加载是基础,但要注意保活策略

ListView.builder/GridView.builder 天然懒加载(只为视口附近 item 创建 Widget/Element/RenderObject),但默认开启的 AutomaticKeepAlive 会让滑出视口的 item 状态被保留,图片对应的纹理内存可能没有被真正释放。图片密集的长列表,评估 addAutomaticKeepAlives: false 是否合适(代价是滑回来需要重新构建/重新加载)。

5.2 预加载与及时释放的平衡

  • 对即将进入视口的图片(下一屏的前几张)用 precacheImage 提前触发解码,减少滑动时的加载闪烁。

  • 但预加载范围要克制(通常 1~2 屏),过度预加载会和"及时释放已划出视口的图片内存"这个目标相冲突。

5.3 快速滑动时取消无用请求

网络图片库通常在对应 Widget dispose() 时自动取消未完成的下载请求(cached_network_image 内部已处理)。如果自己封装了图片加载逻辑,必须 在 dispose() 里主动取消未完成的请求/监听,否则用户快速滑动列表会积累大量无意义的并发下载,同时占用带宽和内存。


6. 格式与资源包体积优化

  • 本地资源图优先用 WebP:同等视觉质量下体积通常明显小于 PNG/JPEG,直接压缩 App 包体积,尤其是大量图标/插画资源的场景收益显著。

  • 按目标机型分布精简多倍率图 :assets 下 1x/2x/3x 三套图全部打包会造成体积浪费------如果目标用户机型大多是 2x/3x 屏,1x 图基本用不上;反过来目标机型不需要极致高清时,也不必强上 3x 图。

  • **图标类资源优先用矢量图( **flutter_svg** ) **:一份 SVG 适配任意分辨率,避免多倍率图片资源膨胀包体积;代价是运行时有一次矢量解析开销,图标量级下基本可以忽略。


7. 动图(GIF/APNG/Lottie)优化

  • GIF 逐帧解码内存开销大:GIF 播放本质是把每一帧都解码成独立的位图,帧数多、分辨率高的 GIF 占用内存可能远超预期,能用 Lottie(矢量动画,基于 JSON 描述而非逐帧位图)替代的场景优先用 Lottie。

  • 自己实现帧动画播放器时注意监听释放 :ImageStream/ImageStreamListener 监听如果没有在 dispose() 里移除,底层 Codec 和帧缓冲会持续被引用无法释放,是动图相关内存泄漏的高发点。

  • 避免同时播放大量高分辨率 GIF/Lottie:列表里如果每个 item 都有一个自动播放的动图,同时解码/渲染多个动画序列对内存和 CPU 都是持续压力,考虑"仅当前可见项播放,其余暂停"的策略。


8. 内存泄漏排查

8.1 常见泄漏点

  • 自定义 ImageProvider/逐帧解码逻辑中,ImageStreamListener 没有在 State dispose() 时正确移除。

  • 全局单例/静态变量意外持有了 ImageProvider/解码后的 ui.Image,导致其无法被 ImageCache 的 LRU 正常淘汰路径回收。

  • 页面销毁后,异步图片加载回调里的闭包依然持有已 dispose 的 State(间接导致相关资源无法释放)。

8.2 排查工具

  • DevTools 的 Memory 面板 :观察整体堆内存曲线,尤其关注切换页面/离开列表后曲线是否正常回落;ImageCache 相关的对象分类统计能直接定位是否是图片缓存问题。

  • ****** **debugPrintImageCacheStats** (或直接读取 **imageCache.currentSize** / **currentSizeBytes** ) **:在关键节点打点观察缓存实际占用,判断是否符合预期。


9. 图片库优化优先级排序

按常见项目实测经验,收益从高到低:

  1. cacheWidth/cacheHeight 按显示尺寸解码 ------ 改动最小,收益最大

  2. 引入带磁盘缓存的网络图片库(cached_network_image) ------ 解决重复下载

  3. 调整 ImageCache 容量上限匹配业务场景

  4. 长列表的预加载/及时释放平衡

  5. 资源格式优化(WebP/SVG/精简倍率) ------ 影响包体积,不影响运行时内存

  6. 动图播放的可见性控制 ------ 特定场景(大量动图)才需要


Part 2:Dio 库优化

10. Dio 请求生命周期与拦截器机制

10.1 一次请求的完整链路

arduino 复制代码
业务代码调用 dio.get/post(...)

   → RequestInterceptor(请求拦截器链,可修改 header/参数、做统一鉴权注入)

   → 底层 HttpClient 发起真实网络请求(基于 dart:io 的 HttpClient,或配置的 Adapter)

   → ResponseInterceptor(响应拦截器链,可做统一数据解析/埋点/日志)

   → ErrorInterceptor(发生异常时进入,可做统一错误处理/重试)

   → 返回 Response 或抛出 DioException 给业务代码

关键原理 :Dio 的拦截器是链式 执行的,多个拦截器按添加顺序依次处理请求,每一层都可以选择"继续往下传递(handler.next())"、"直接返回结果(handler.resolve())"、"直接抛出错误(handler.reject())",这套机制是几乎所有 Dio 工程化优化(统一鉴权、统一缓存、统一重试、统一日志)的实现基础。

10.2 拦截器设计原则

  • 单一职责拆分:不要把鉴权、日志、缓存、重试全部塞进一个巨大的拦截器,拆成多个各司其职的拦截器按顺序注册,便于单独启用/禁用/测试。

  • 注意拦截器的执行顺序敏感性:比如"统一注入 Token"的拦截器要在"统一日志打印 header"的拦截器之前注册,否则日志打印时 Token 还没被注入。


11. 连接层优化:连接池与 HTTP/2

11.1 复用底层连接,避免重复握手

Dio 默认基于 dart:io 的 HttpClient,HttpClient 内部维护连接池,同一 host 的多次请求会复用已建立的 TCP 连接 (Keep-Alive),避免每次请求都重新走一遍 TCP 三次握手 + TLS 握手的开销。这个复用是默认行为,但如果自定义了 HttpClientAdapter 或频繁创建新的 Dio 实例(每个实例可能对应独立的底层 HttpClient),可能意外破坏了连接复用,是常见的隐性性能坑。

优化建议 :全局尽量复用同一个 **Dio** 实例 (或按业务模块少量复用几个实例),不要在每次请求时 Dio() 重新创建,否则每次都相当于重新建立连接池。

11.2 HTTP/2 支持

原生 dart:io HttpClient 对 HTTP/2 的支持历史上有限制,很多项目会通过 dio_http2_adapter 之类的第三方 Adapter 替换底层实现以启用 HTTP/2。HTTP/2 的核心优势是****多路复用(Multiplexing)** **------同一条 TCP 连接上可以并行处理多个请求/响应,不需要像 HTTP/1.1 那样受"每个 host 有限连接数"的并发限制(HTTP/1.1 靠开多条 TCP 连接实现并发,HTTP/2 靠一条连接内的多个流实现并发),对高并发小请求场景(比如同时拉取多个小接口)有明显的整体耗时优化。

11.3 连接超时与 DNS

  • 合理设置 connectTimeout(连接建立超时)与 receiveTimeout(接收数据超时)分开配置,不要用一个大而统一的超时笼统覆盖所有阶段,否则"DNS 解析慢"和"服务端处理慢"这两种完全不同的问题会被同一个超时值掩盖,不利于问题定位。

  • 弱网场景可以考虑接入 DNS 预解析/IP 直连方案(配合 HTTPDNS 服务),减少 DNS 解析耗时和 DNS 劫持风险,这属于更偏工程基础设施的优化,一般大厂 App 才会单独做。


12. 缓存策略:减少重复请求

12.1 HTTP 缓存拦截器

用 dio_cache_interceptor 这类库实现标准 HTTP 缓存语义(遵循 Cache-Control/ETag/Last-Modified 等响应头),对内容变化不频繁的接口(配置类、字典类接口)能显著减少重复网络请求:

dart 复制代码
dio.interceptors.add(DioCacheInterceptor(options: cacheOptions));

原理:拦截器在请求发出前先查本地缓存,如果缓存未过期直接返回缓存内容(不发网络请求);如果缓存过期但服务端支持 ETag 校验,发起一个"带条件的请求"(If-None-Match),服务端如果内容没变返回 304 Not Modified(几乎不占带宽),拦截器据此继续复用本地缓存内容。

12.2 业务层自定义缓存策略

对于不遵循标准 HTTP 缓存语义、但业务上明确知道"这个接口结果可以缓存 N 分钟"的场景,可以在拦截器层自己维护一个简单的内存/磁盘 Key-Value 缓存(Key 通常是 URL + 请求参数的哈希),比接入完整的 HTTP 缓存库更轻量可控。


13. 并发控制与请求合并/去重

13.1 相同请求去重(Request Deduplication)

场景:用户快速点击刷新按钮/快速切换 Tab,导致同一个接口在短时间内被多次触发,实际只需要最后一次结果,前面的请求属于浪费。

实现思路:在拦截器层维护一个"进行中请求"的 Map(Key 是请求签名),新请求发出前检查是否已有相同签名的请求在途:

  • 如果有,直接复用那个进行中请求的 Future(多个调用方共享同一次网络请求结果),而不是各自发起。

  • 或者取消旧请求 (Dio 支持 CancelToken),只保留最新一次的请求结果,避免旧的慢响应覆盖新的请求结果(经典的"请求竞态"问题,比如搜索框输入联想词时前一个字符的慢响应覆盖了后一个字符的结果)。

13.2 CancelToken 的正确使用

页面销毁时,应该主动取消该页面发出的所有未完成请求,避免请求完成后回调里访问已销毁的 State/Controller:

dart 复制代码
final cancelToken = CancelToken();

dio.get(url, cancelToken: cancelToken);

  


@override

void dispose() {

  cancelToken.cancel();

  super.dispose();

}

13.3 并发数量控制

对于需要并发发起大量请求的场景(比如批量拉取列表里每一项的详情),不加限制的并发可能瞬间打满连接池、给服务端造成压力。可以用 Future.wait 配合手动分批,或引入 pool(如 package:pool)控制同时在途的请求数量上限。


14. 超时、重试与降级策略

14.1 统一重试拦截器

弱网场景下部分请求失败是正常现象,用拦截器统一实现"失败后按策略重试",避免每个业务调用点都手写重试逻辑:

dart 复制代码
dio.interceptors.add(RetryInterceptor(

  dio: dio,

  retries: 2,

  retryDelays: const [Duration(seconds: 1), Duration(seconds: 2)],

));

注意事项:

  • 只对幂等请求做自动重试(GET、以及明确设计成幂等的 POST/PUT),非幂等的写操作(比如"提交订单")盲目重试可能导致重复提交,需要业务层配合幂等 Token 机制或直接排除在自动重试范围外。

  • 重试间隔建议用****指数退避(Exponential Backoff)** **而不是固定间隔,避免弱网/服务端过载场景下所有客户端同时在固定时间点集中重试造成"重试风暴"。

14.2 降级策略

关键接口失败时,考虑是否有本地兜底数据可用(比如上次成功缓存的结果、默认配置),而不是直接展示错误页面------尤其是首页/配置类接口,"展示旧数据"通常比"展示空白/报错"的用户体验更好。


15. 数据体积优化:压缩与序列化

15.1 请求/响应压缩

确保请求头带上 Accept-Encoding: gzip(dart:io HttpClient 默认会自动处理 gzip 解压,但自定义 Adapter 时要确认没有意外关闭这个能力),服务端返回 gzip 压缩后的响应体可以显著减少传输字节数,尤其是 JSON 这种文本格式压缩率较高。

15.2 序列化性能

  • 避免在主 Isolate 里做大 JSON 的同步 **jsonDecode **:响应体较大时(比如几百 KB 以上的列表接口),同步解析会阻塞主线程造成掉帧,用 compute() 把解析工作丢到独立 Isolate。

  • 考虑用 **json_serializable** / **freezed** 生成的模型类而不是手写 **Map** 解析:生成的代码通常比手写反射式解析更高效,也减少手写解析逻辑的出错概率。

  • 对性能极度敏感的场景(大数据量、高频接口),可以评估 Protobuf 等二进制序列化协议替代 JSON,减少序列化/反序列化开销和传输体积,但这属于需要前后端协同改造的较大改动,一般只有对性能要求特别高的模块才值得投入。


16. 错误处理与统一拦截器设计

16.1 统一错误处理拦截器

dart 复制代码
dio.interceptors.add(InterceptorsWrapper(

  onError: (DioException e, handler) {

    // 统一处理:Token 过期跳转登录、网络错误统一提示、埋点上报

    handler.next(e);

  },

));

收益:避免每个业务调用点都重复写"判断状态码、判断网络异常类型、弹提示"这套逻辑,统一在一处维护,也方便后续统一调整错误处理策略。

16.2 区分错误类型精细化处理

DioException.type 区分了连接超时、发送超时、接收超时、响应错误、请求取消等多种类型,业务侧应该区分对待------比如"请求被主动取消"(DioExceptionType.cancel)通常不需要弹错误提示(是正常的页面销毁行为),如果不加区分统一弹 Toast,会出现"用户快速返回页面却看到一堆网络错误提示"的体验问题。


17. 大文件上传下载优化

17.1 流式上传/下载

大文件(图片、视频、日志文件)应该用流式方式处理,避免一次性把整个文件读入内存:

dart 复制代码
dio.download(url, savePath, onReceiveProgress: (received, total) {

  // 更新进度

});

Dio.download 内部是流式写入本地文件,不会把整个文件内容加载进内存,这对大文件场景是必须的,否则内存占用会随文件大小线性增长甚至导致 OOM。

17.2 断点续传

配合 HTTP Range 请求头实现断点续传,网络中断后不需要重新下载已完成的部分,对大文件下载场景(比如离线包、视频缓存)体验提升明显,需要服务端支持 Range 请求(大多数 CDN/对象存储默认支持)。

17.3 上传进度与取消

dart 复制代码
dio.post(url, data: formData, onSendProgress: (sent, total) {

  // 更新上传进度

}, cancelToken: cancelToken);

长时间的大文件上传务必提供取消能力(CancelToken),避免用户离开页面后上传任务还在后台无意义地占用带宽和内存。


18. 安全与稳定性

  • HTTPS 证书校验不要随意关闭 :调试阶段为了抓包方便可能配置了 badCertificateCallback 直接返回 true(跳过证书校验),必须确保这类代码不会带进生产环境,否则存在中间人攻击风险。

  • **证书锁定(Certificate Pinning) **:对安全要求较高的 App(涉及支付、敏感数据),可以配置证书锁定,只信任特定的证书/公钥,防止恶意根证书被安装后的流量劫持风险,可通过自定义 HttpClientAdapter 结合证书校验逻辑实现。

  • 敏感参数不要打进日志拦截器:统一日志拦截器方便调试,但要注意脱敏处理(Token、密码、身份证号等敏感字段不应完整打印到日志/上报系统)。


19. Dio 优化优先级排序

按常见项目实测经验,收益从高到低:

  1. 全局复用 Dio 实例,避免重复创建导致连接池失效 ------ 改动小,是很多项目的"隐性坑"

  2. **请求去重/取消(CancelToken) ** ------ 解决竞态问题和无意义的重复请求

  3. 统一错误处理 + 区分错误类型 ------ 直接影响用户体验和排障效率

  4. HTTP 缓存拦截器 ------ 针对不常变化的接口,减少重复网络请求

  5. 大文件流式上传下载 + 断点续传 ------ 大文件场景必做

  6. HTTP/2 + 连接层深度优化 ------ 高并发场景才需要投入

  7. 二进制序列化协议替代 JSON ------ 只有极致性能场景才值得改造成本


20. 高频面试题 Q&A 速查

**Q1:为什么不建议每次请求都 **new Dio()** ? **

A:Dio 内部持有的底层 HttpClient 维护了连接池,同一个实例的多次请求可以复用已建立的 TCP 连接(Keep-Alive),避免重复的 TCP/TLS 握手开销;每次新建实例相当于重新创建连接池,失去了连接复用的收益。

**Q2:Dio 拦截器链的执行顺序是怎样的? **

A:请求方向按拦截器注册顺序 依次执行 onRequest;响应方向和错误方向按注册顺序的逆序 依次执行 onResponse/onError(类似"先进后出"的洋葱模型),设计统一鉴权、日志等拦截器时要注意这个顺序敏感性。

**Q3:如何避免搜索框场景下"慢响应覆盖快响应"的竞态问题? **

A:每次发起新请求前,用 CancelToken 取消上一次还未完成的同类请求,只保留最新一次请求的结果;或者在业务层给每次请求打一个自增序号,回调时判断序号是否是最新一次,不是最新的直接丢弃结果。

**Q4:为什么 **cacheWidth** / **cacheHeight** 能显著降低图片内存占用? **

A:不设置时解码器按图片原始分辨率生成纹理,设置后解码器在解码阶段就按目标尺寸做降采样,内存占用大致按面积比例降低(缩小到 1/2 尺寸,内存降到约 1/4),是解决"大图小显"内存问题最直接有效的手段。

**Q5:ImageCache 的缓存 Key 是怎么区分的,为什么同一张图可能被缓存两份? **

A:缓存 Key 通常包含 URL 和解码参数(如 cacheWidth/cacheHeight),如果同一 URL 在不同地方使用了不同的解码参数,会被当成不同的缓存条目分别占用内存,互不复用,是常见的隐性重复解码来源。

**Q6:为什么大文件下载要用流式方式而不是一次性读取? **

A:一次性读取会把整个文件内容加载进内存,文件越大内存占用越高,容易导致内存暴涨甚至 OOM;流式处理边接收边写入磁盘,内存占用只与缓冲区大小相关,不随文件总大小线性增长。

**Q7:HTTP 缓存拦截器和业务层自定义缓存的区别? **

A:HTTP 缓存拦截器遵循标准的 Cache-Control/ETag 等响应头语义,通用性强但需要服务端配合返回正确的缓存头;业务层自定义缓存更灵活(比如"这个接口结果缓存 5 分钟"这种业务规则),不依赖服务端配合,但需要自己维护缓存失效逻辑。

**Q8:为什么重试要用指数退避而不是固定间隔? **

A:固定间隔重试在大量客户端同时遇到服务端过载/弱网时,会导致所有客户端在几乎相同的时间点集中重试,反而加剧服务端压力(重试风暴);指数退避让重试间隔逐次拉长且通常加入随机抖动,分散重试时间点,减轻这个问题。


附:延伸建议

  • 图片链路优化建议配合 DevTools Memory 面板实测验证,不要凭经验判断内存是否真的降低。

  • Dio 相关的连接层优化(HTTP/2、连接池调参)收益因业务并发量级差异很大,建议先用 Charles/Proxyman 之类工具抓包观察当前实际的连接复用情况和请求耗时分布,再决定是否需要投入。

相关推荐
卸任15 小时前
Vibe coding工具使用心得
前端·ai编程
brien916 小时前
为什么模型标称 1M,Google Antigravity 聊到 256K 却突然失忆?我们给它做了个原生行内监控圆环
前端
修炼的dance16 小时前
录屏关键帧间隔多少合适?六档实测,2秒到10秒小了75.6%
前端
Lank_M16 小时前
关键帧从1秒拉到10秒,录屏小87.3%,摄像头小10.8%
前端
补码缩写16 小时前
用 ffprobe 拆录屏,八成字节在 29 个关键帧上
前端
百思不得小李16 小时前
一个复杂的动态表单是如何实现的
前端·设计模式
陈随易16 小时前
Cmder 停更 17 个月,2.0 支持 Windows Terminal
前端·后端·程序员
风骏时光牛马17 小时前
前沿视界
前端
kyriewen17 小时前
我扫了 814 个依赖,97 个还敢要 Node 10
前端·javascript·程序员
陈随易17 小时前
3000 万人在用的翻译插件,重写一遍就是一门生意
前端·后端·程序员