Skip to content

图片加载 / Image Loading ​

一句话概览(总览首段) ​

客户端图片治理贯穿 “打包内置资源” 与 “在线动态加载” 两大场景。不论图片来自本地 APK/Bundle 还是网络,底层本质都是统一的生命周期:取字节 → 解码(含降采样) → 多级缓存(内存/磁盘) → 绑定生命周期并绘制。但是在资源放置、匹配与展示环节,各平台因为密度机制与内存归属的不同,演化出了截然不同的底层逻辑。


一、 图片业务场景与资源全景分类 ​

在移动端开发中,图片按照来源与用途可以精确划分为两大部分,各部分的优化侧重点完全不同:

1. APP 本身打包的内置资源 (Assets / Drawable / XCAssets) ​

打包在客户端安装包内的静态资源,直接影响 APK / IPA 包体大小以及启动/首页渲染性能。

  • (a) UI 小图标 / 控件图 (Icons / Buttons / Tabs):

    • 特点:尺寸小(如 24×24dp)、数量多、在多处 UI 中重复出现(如返回键、爱心、Tab 栏图标)。
    • Android 放置策略:
      • 首选矢量图 (VectorDrawable):全面使用 XML 矢量图,一套代码适配所有分辨率,不占多余包体且永不模糊,支持 Tint 着色复用。
      • 位图降级方案:若使用 PNG 位图,推荐只保留一个高密度桶(如 drawable-xxhdpi 或 drawable-xxxhdpi 单桶),让低密度设备在运行时自动向下缩小(缩小免费且清晰)。
    • iOS 放置策略:
      • 严格按苹果规范在 Asset Catalog 中提供 @2x 和 @3x 两套切图,对应离散的 scale 体系,确保 1:1 点阵精确匹配。
    • Flutter 放置策略:
      • 在 pubspec.yaml 中声明 2.0x/、3.0x/ 资源目录变体,运行时由 AssetImage 按 devicePixelRatio 匹配。
  • (b) 本地大背景 / 运营静态图 (Backgrounds / Banners / Placeholders):

    • 特点:像素面积大(如 1080×1920 甚至 4K),极易引发启动或首页内存暴涨(OOM)。
    • Android 放置策略:
      • 统一放入 drawable-nodpi 目录。跳过系统的密度桶自动预缩放(避免低桶在旗舰机上被二次放大导致内存平方级暴涨),由代码配合 inSampleSize 在解码期按 View 真实尺寸进行降采样。
    • iOS 放置策略:
      • 不分 @2x/@3x 多个文件,使用 Asset Catalog 的 Single Scale(单源大图) 放一张高清大图,代码中配合 contentMode = .scaleAspectFill 等比铺满自适应。
    • Flutter 放置策略:
      • 通常只提供一张高清原图,代码中控制逻辑宽高,并结合 cacheWidth / cacheHeight 在解码期降采样。

2. 在线/动态加载图片 (Network / Local File / Storage) ​

  • 场景:网络请求图片、用户相册、本地缓存文件、动态 Banner。
  • 核心治理:
    • 异步线程与并发:避免阻塞主线程(UI 线程),建立线程池并发调度。
    • 三级/多级缓存:内存缓存(内存 LRU + 弱引用)→ 磁盘缓存(DiskLruCache)→ 网络请求。
    • 生命周期绑定:请求必须绑定 Activity / Fragment / Composable / Widget 的生命周期,页面销毁或滑出屏幕时自动取消网络连接与解码任务。
    • UI 渐进体验:占位图(Placeholder)、BlurHash 高斯模糊渐进式加载、错误重试与加载动画。

二、 图片展示阶段的 4 大共同痛点(跨端底层机制) ​

无论图片来自本地还是网络,当字节流被送到解码器、解压成 Bitmap 上屏展示时,所有平台都会面临相同的底层物理规则:

1. 内存与色彩格式(Pixel Configuration & BPP) ​

图片在内存中占用的物理大小不看压缩文件(JPG/PNG)的磁盘大小,而由解码后的像素总面积与每像素字节数(BPP, Bytes Per Pixel) 决定:

占用内存(Byte)=解码后像素宽×解码后像素高×每像素字节数

色彩格式与内存开销对照: ​

  • ARGB_8888(默认):4 字节/像素(Alpha 8bit, R 8bit, G 8bit, B 8bit)。品质最高,占用内存最大。
  • RGB_565:2 字节/像素(R 5bit, G 6bit, B 5bit)。不带 Alpha 透明通道的图片(如背景图、JPG 照片)使用该格式,内存开销直接减半(-50%) 。
  • HARDWARE Config (Android 8.0+ / API 26+):Bitmap 像素数据直接存储在 GPU 显存(Hardware Buffer) 中,App 堆内存占用为 0,极大降低 JVM / Native 堆 OOM 风险。代价是无法直接在 CPU 端修改像素(如不能直接绘图或做 CPU 滤波)。

2. 解码采样率与尺寸匹配(Downsampling) ​

严禁“大图小用”(例如将一张 4000×3000 的相机原图直接解压并设置到 100×100 的头像框中,这会直接消耗约 48MB 内存,引发 GC 抖动甚至 OOM)。

  • Android / Native:解码时设置 BitmapFactory.Options.inSampleSize = n(采样率为 1/n),解码尺寸降为 1/n,内存开销下降为 1/n2。
  • Compose 侧:依靠 Landscapist / Coil 原生 AsyncImage 的约束驱动采样(Constraint-based sampling),自动获取 Composable 的 Constraints 动态计算最佳降采样尺寸(详见 Landscapist:Compose 图片加载封装)。
  • Flutter 侧:使用 ImageProvider 的 cacheWidth / cacheHeight(或 memCacheWidth),通过 ResizeImage 在 dart:ui 阶段把图片解压成缩略图(详见 Flutter 图片加载)。
  • iOS 侧:使用 CGImageSourceCreateThumbnailAtIndex API,绕过全图解码,直接从文件读取字节生成指定尺寸的缩略图(详见 iOS 图片加载(UIImage / UIKit / ImageIO))。

3. 内存稳定与内存池复用(Memory Stability & Reuse) ​

  • Android inBitmap / BitmapPool 机制:
    • 在 Android 4.4+,解码新位图时可以复用已不再使用的旧位图(inBitmap)的内存空间,避免频繁创建/销毁 Bitmap 引发的堆内存碎片化与 GC 抖动(卡顿)。Glide 和 Coil 内部均内置了 LruBitmapPool。
  • Flutter 内存机制 (ImageCache):
    • Flutter 走 dart:ui 自绘,ImageCache 是位于 Dart VM 堆上的双限 LRU(默认 1000 张 / ~100MB)。Flutter 没有 native BitmapPool 内存池,超大图内存必须严格靠 cacheWidth 解码期降采样来控制。
  • iOS 内存机制 (NSCache):
    • 系统 NSCache 负责内存缓存,具备自动响应系统内存警告(didReceiveMemoryWarning)并释放被逐出对象的能力。

4. 超大图 / 巨图分块解码(Region Decoding) ​

  • 针对几万像素的长图(如长文章截图、清明上河图),即使降采样也会失真,若原图整体解压则必定 OOM。
  • 解决方案:使用 BitmapRegionDecoder(Android) 或区域切片渲染技术,仅解码和绘制当前屏幕可视区域(Rect 视窗)内的像素字节,手势滑动时动态平移解码窗口。

三、 跨端密度机制:连续 vs 离散 & 缺失不缩放 ​

不同平台在处理屏幕密度与图片缩放时存在根本性设计差异:

平台密度机制匹配与缩放行为
Android连续密度 (任意 dpi,如 2.625)桶 + 解码缩放:选最接近的桶,解压时二次缩放 (inTargetDensity / inDensity) 至精确物理尺寸
iOS离散密度 (固定 @1x/@2x/@3x)离散精确匹配:1:1 贴图,无重采样,极清晰
Flutter离散变体 (1.0x/2.0x/3.0x)AssetImage 变体选择:挑选 scale ≥ dpr 的第一张变体;缺失时不重采样解码;固有逻辑尺寸 = 源像素 ÷ scale,原图低于 dpr 需物理像素时由 GPU 放大渲染致模糊(详见 12/01 §5.1)

1. Android 的连续密度与“解码缩放” ​

  • Android 设备厂商众多,屏幕密度是连续且任意的(如 420dpi 对应 scale = 2.625)。
  • Android 系统引入 “密度桶 (Density Buckets)”(mdpi=1x, hdpi=1.5x, xhdpi=2x, xxhdpi=3x, xxxhdpi=4x)。
  • 底层解码原理:系统把桶密度设为 inDensity,设备密度设为 inTargetDensity,BitmapFactory 在解压成内存位图时强制缩放:解码尺寸=原图尺寸×设备 dpi桶 dpi,内存占用倍率=(设备 dpi桶 dpi)2
  • 桶放反的后果:如果把一张 480×480 的图错放在 mdpi(1x)低桶,运行在 xxhdpi(3x 屏)手机上,系统会将其强制放大解码为 1440×1440 像素,内存占用剧烈飙升 9 倍(32=9),且边缘充斥插值锯齿与严重模糊!

2. iOS 的离散密度与“1:1 像素匹配” ​

  • 苹果硬件生态完全自控,设备密度是离散且锁定的(只有 @1x、@2x、@3x 三档 scale)。
  • 系统直接根据设备的 scale 选择对应的切图,在屏幕上实现 1:1 物理像素点阵渲染,不做多余的内存重采样,因此显示极为锐利清晰。

3. Flutter 的“缺失不缩放”与变体陷阱 ​

  • Flutter 变体选择逻辑 (AssetImage._chooseAsset):
    1. 运行时获取设备的 devicePixelRatio(dpr,如 2.625 或 3.0)。
    2. 遍历资源变体目录,选择第一张 scale ≥ dpr 的变体。
    3. 如果一张 scale ≥ dpr 的变体都没有,就退回选择已配置的最高分辨率变体。
  • 与 Android 的核心区别:
    • Android 原生:即使放错桶,系统也会在内存解码阶段强制进行重采样放大/缩小,保证物理尺寸(dp)符合预期(代价是低桶放大暴内存且模糊)。
    • Flutter 机制:Flutter 不会在内存解码阶段帮帮你做这种跨密度的“自动重采样放大”。如果你只放了一张 1.0x 的图片,而在 3.0x 的高分屏手机上运行:
      • 无宽高约束时:图片会按照原始 1x 像素渲染,在屏幕上显得非常小(只有 1/3 大小) 。
      • UI 约束强行拉伸时:图片像素点填不满逻辑坑位,UI 引擎直接将位图硬拉伸铺满,导致肉眼可见的严重模糊与马赛克。

四、 本模块要回答的问题与硬核推导 ​

  • 为什么小图标放错桶或偷懒只放 xhdpi 会导致高配手机卡顿/发虚?

    • 一句话答:位图「缩小免费且清晰,放大必糊」。若只放 xhdpi(2x),在 3x/4x 手机上会被系统强制放大 1.5~2 倍解码,内存按面积翻 2.25~4 倍且边缘失真。
    • 解决原则:图标优先用 VectorDrawable;若用位图,放 xxhdpi/xxxhdpi 最高桶只让系统缩小,不让系统放大(详见 12/01)。
  • drawable-nodpi 中的背景大图,分辨率到底应该给高还是给低?

    • 一句话答:原图分辨率低于设备物理像素会被拉伸放大导致模糊;原图分辨率高于设备物理像素则只缩不放、保持清晰,但会多占解码内存。
    • 工程实践:提供高分辨率单图(如 WebP 格式),配合代码层 inSampleSize(Android)或 cacheWidth(Flutter)按 View 的真实物理尺寸动态降采样。
  • Glide / Coil / Fresco 与 Compose AsyncImage、Flutter Image.network 的体系演进?

    • 一句话答:Glide(Request 链 + LruBitmapPool)、Coil(Kotlin 协程 + 责任链)、Fresco(Producers 链 + Native 内存)是 Android 视图层的三大成熟引擎;Compose 侧的 AsyncImage 是 Coil 的声明式封装;Flutter 则是独立的 dart:ui 自绘体系(ImageProvider + ImageCache),与 Native 引擎不互通(详见 01 / 02 / 04)。

五、 子主题与全链路文档索引 ​

序号主题对应文档核心落脚点
01Landscapist: Compose 图片加载Landscapist:Compose 图片加载封装约束驱动采样、插件架构、KMP 支持
02图片加载方式总览与原理图片加载方式总览与原理Glide/Coil/Fresco 三大引擎架构与四级缓存
03横向对比:概念·考点·优势·性能图片加载方式:概念 · 考点 · 优势 · 性能对比六类加载方式综合性能与选型矩阵
04Flutter 图片加载原理Flutter 图片加载ImageProvider + ImageCache 双限 LRU、CachedNetworkImage
05iOS 图片加载(UIKit / ImageIO)iOS 图片加载(UIImage / UIKit / ImageIO)@2x/@3x 精确匹配、CGImageSource 解码期降采样
跨模块Bitmap 内存与 GC 优化07/08 Bitmap 内存内存计算公式、RGB_565、inBitmap 复用、HARDWARE Config
跨模块dp/dpi 与图片资源放置12/01 资源放置连续/离散密度桶放置算法、Flutter Asset 变体缺失实验

六、 复习检查题 ​

  1. 为什么图片加载在 Android 与 Flutter 里是“平行的两套世界观”?

    • 答:Android 走 Native Bitmap(Glide BitmapPool / inBitmap 复用,内存可存 Native/Hardware 堆);Flutter 走 dart:ui 自绘,图片经 Codec 解码后存在 Dart VM 堆上的 ImageCache(LRU 双限)。两者的内存归属、复用机制、缓存 API 完全不互通。
  2. 为什么背景大图放 drawable-nodpi 可以防止 OOM?

    • 答:放进普通密度桶(如 mdpi),在 3x/4x 旗舰机上会被系统误判为“低密度资源”而强制放大 3~4 倍解码,导致内存暴涨 9~16 倍而 OOM;放 nodpi 会阻止系统自动预缩放,配合 inSampleSize 降采样可以精准按屏幕物理尺寸解码。

站点构建时间:2026/8/24 23:43:17