Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

图片加载与缓存

★ 图片是移动端内存消耗的第一大户,也是 OOM 的万恶之源。面试题通常从“怎么用 Glide”深入到“如果让你手写一个图片框架,你会怎么设计三级缓存”。

一、Bitmap 内存模型与大小计算

一张 Bitmap 占多大内存? 公式分辨率宽 × 分辨率高 × 每个像素占用的字节数(还要考虑放错 drawable 文件夹带来的缩放系数)。

  • ARGB_8888(默认):每个像素 4 字节。高质量,带透明度。
  • RGB_565:每个像素 2 字节。无透明度,省一半内存。

注意:Android 8.0 之后,Bitmap 的像素数据移到了 Native 内存中,减少了对 JVM 堆的压力,降低了 Java 层的 OOM,但总内存占用并没变。

二、采样压缩:inSampleSize 的奥秘

加载 4K 高清图到 100x100 的 ImageView,直接加载必爆内存。必须使用 BitmapFactory.Options 进行采样压缩。

// 1. 只读取图片宽高,不将像素加载到内存
val options = BitmapFactory.Options().apply { inJustDecodeBounds = true }
BitmapFactory.decodeResource(resources, R.id.my_image, options)

// 2. 根据目标 View 尺寸计算采样率 (inSampleSize,必须是 2 的幂)
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight)

// 3. 关闭只读边界,真正加载图片
options.inJustDecodeBounds = false
val bitmap = BitmapFactory.decodeResource(resources, R.id.my_image, options)

三、经典三级缓存设计

  1. 内存缓存 (LruCache):速度极快,空间小。基于强引用,利用 LinkedHashMap 记录访问顺序,满时淘汰最近最少使用的图片。
  2. 磁盘缓存 (DiskLruCache):速度中等,空间大。保存下载的原始文件或转换后的图。
  3. 网络获取:最慢,消耗流量。内存和磁盘都没命中时才走网络。

流程:读(内存 → 磁盘 → 网络),写(网络回来后写入磁盘和内存)。

四、Glide 的多级缓存机制剖析

Glide 的缓存比经典三级缓存更精细:

  1. 活动资源 (ActiveResources):当前正在屏幕上显示的图片。使用弱引用持有,防止 GC 时被回收导致闪烁,同时分担 LruCache 压力。
  2. 内存缓存 (LruCache):刚被移出屏幕,但可能马上要用到的图片。
  3. 磁盘缓存 (DiskCache)
    • Resource:缓存经过转换、裁剪后的图(直接拿来就能显示)。
    • Data:缓存网络下载的原始全尺寸数据。

五、列表滑动与超长图优化

  • RecyclerView 滑动卡顿优化
    • onBindViewHolder 里绝对不能在主线程 decode 图片。
    • 滑动时暂停加载 (Glide.with(context).pauseRequests()),停止滑动时恢复加载。
    • 给 ImageView 固定宽高,避免多次 measure。
  • 巨图加载 (长图/清明上河图)
    • 使用 BitmapRegionDecoder 局部解码,结合手势监听,滑到哪里只加载那一部分的内存。

高频面试题

Q1:如何设计一个图片加载框架?(架构题) 答:可以拆分为四大模块:

  1. 请求封装层:对外提供流式 API (如 with().load().into()),封装 Request 对象。
  2. 任务调度层:管理线程池,负责将加载任务派发给 IO 线程,把结果回调到主线程。并与生命周期绑定,在 Activity 销毁时取消任务。
  3. 缓存拦截层:实现 ActiveResource -> LruCache -> DiskCache -> Network 的责任链/拦截器模式。
  4. 解码/变换层:负责网络流到 Bitmap 的解码,处理 inSampleSize 采样,以及圆角、模糊等 Transformation。

Q2:排查线下和线上 OOM 崩溃,你的思路是什么? 答:线下通常通过 Android Profiler 监控内存,或者集成 LeakCanary,当触发 dump 后,通过 MAT 或 Shark 分析堆栈,找出是哪个组件持有大对象。 线上 OOM 通常缺乏具体堆栈。我们会分析 APM 收集的机型和发生页面,重点排查该页面的大图加载是否有采样压缩、列表图片是否使用了 ARGB_8888、是否有未关闭的动画或大 Bitmap 引用泄露。可以上报 OOM 时的内存使用统计,辅助定位。

Q3:Glide 的生命周期管理是怎么做的?为什么 Activity 销毁时它能停止加载? 答:Glide 通过向当前传入的 Context(Activity/Fragment)偷偷注入一个无 UI 的隐藏 Fragment。因为系统会回调这个 Fragment 的生命周期方法,Glide 借此感知到了宿主的 onStartonStoponDestroy,从而自动暂停、恢复或取消图片加载任务,防止内存泄漏和无效流量浪费。

易错点 / 追问

  • 混淆 drawable 文件夹的缩放规则:把大图放在 drawable-mdpi,在高密度屏幕手机上加载时,系统为了保持物理尺寸一致,会对其进行放大采样,导致内存成倍暴增。图片应该尽量提供 xxhdpi,或者放在 drawable-nodpi 中。
  • 误解 LruCache 的原理:追问 LruCache 底层数据结构时,必须答出是 LinkedHashMap 并且开启了 accessOrder=true 模式,每次 getput 都会把元素移到双向链表尾部。
  • 忽略 Bitmap 回收机制:虽然 Bitmap 数据在 Native,但 Java 层的 wrapper 对象仍靠 GC。追问优化时可以提 inBitmap 属性,即复用已分配的旧 Bitmap 内存块来加载新图片,避免频繁申请和销毁内存。