设计图片加载框架
问题
如何从零设计一个 Android 图片加载框架?
答案
整体架构
核心模块
| 模块 | 职责 | 关键技术 |
|---|---|---|
| 请求管理 | URL 去重、队列管理 | 并发 Map、协程 |
| 内存缓存 | LRU 缓存 Bitmap | LruCache(maxSize = 堆内存 1/8) |
| 磁盘缓存 | 持久化原始/处理后的图片 | DiskLruCache |
| 网络加载 | 下载图片 | OkHttp |
| 解码器 | 按目标尺寸采样解码 | BitmapFactory.Options.inSampleSize |
| 转换器 | 圆角、模糊等变换 | Bitmap.createBitmap + Canvas |
| 生命周期 | 与 View / Activity 生命周期绑定 | Lifecycle 观察者 |
三级缓存
class ImageCache {
// L1:内存缓存(LruCache)
private val memoryCache = object : LruCache<String, Bitmap>(maxMemory / 8) {
override fun sizeOf(key: String, bitmap: Bitmap): Int {
return bitmap.allocationByteCount
}
}
// L2:磁盘缓存
private val diskCache = DiskLruCache.open(cacheDir, 1, 1, 100 * 1024 * 1024) // 100MB
fun get(key: String): Bitmap? {
// 先查内存
memoryCache.get(key)?.let { return it }
// 再查磁盘
diskCache.get(key)?.let { snapshot ->
val bitmap = BitmapFactory.decodeStream(snapshot.getInputStream(0))
memoryCache.put(key, bitmap) // 回填内存缓存
return bitmap
}
return null
}
}
采样解码(避免 OOM)
fun decodeSampledBitmap(filePath: String, reqWidth: Int, reqHeight: Int): Bitmap {
// 第一步:只读取尺寸
val options = BitmapFactory.Options().apply { inJustDecodeBounds = true }
BitmapFactory.decodeFile(filePath, options)
// 第二步:计算采样率
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight)
options.inJustDecodeBounds = false
// 第三步:按采样率解码
return BitmapFactory.decodeFile(filePath, options)
}
fun calculateInSampleSize(options: BitmapFactory.Options, reqW: Int, reqH: Int): Int {
val (height, width) = options.outHeight to options.outWidth
var inSampleSize = 1
if (height > reqH || width > reqW) {
val halfH = height / 2
val halfW = width / 2
while (halfH / inSampleSize >= reqH && halfW / inSampleSize >= reqW) {
inSampleSize *= 2
}
}
return inSampleSize
}
设计要点
- 请求去重:同一 URL 并发请求只发起一次网络加载,其他请求等待结果
- 取消机制:View 被复用时取消旧请求,避免图片显示错乱
- 线程安全:缓存读写需要同步(
LruCache自身线程安全,磁盘操作需加锁) - Bitmap 复用:使用
inBitmap复用已有的 Bitmap 内存块
常见面试问题
Q1: Glide 如何避免列表中的图片闪烁?
答案:
- 生命周期感知:Glide 绑定到 Fragment/Activity 的生命周期,页面不可见时暂停加载
- Tag 标记:Glide 将请求绑定到 ImageView 的 Tag 上,RecyclerView 复用时自动取消旧请求
- 内存缓存:已加载的图片优先从内存缓存读取,无需等待网络
- 占位图:
placeholder()在加载期间显示占位图,避免空白闪烁