一、为什么需要Glide库
在项目中使用 Glide 的原因主要在于其强大的功能,包括高效的图片加载、缓存管理、图片变换以及代码封装和复用能力。
通过 GlideUtil
工具类,可以将图片从多种来源(如网络或文件路径)加载并显示在 ImageView
中。此外,它还通过自定义的 AppGlideModule
来配置和管理图片缓存,设定特定的缓存目录和大小,从而减少重复的网络请求并提升应用性能。
Glide 强大的图片变换功能是另一个关键原因,它不仅支持 TransformationUtils.centerCrop
和 TransformationUtils.circleCrop
等内置变换,还允许通过 CircleImageTransformer
和 GlideCircleBoardTransform
等自定义 BitmapTransformation
类实现带有渐变边框的圆形图片等复杂的视觉效果,避免了繁琐的手动绘制。最后,将这些复杂的加载和变换逻辑封装在 GlideUtil
中,形成统一的 API,极大地提高了代码的可重用性和可维护性。
二、在项目中Glide的具体应用
首先一个自定义的CustomGlideModule在应用级别配置Glide非常重要,使用它来覆盖默认的磁盘缓存行为。通过设置特定的缓存路径,开发者可以控制 Glide 存储缓存图片的位置,从而更易于管理和调试。fixme
注释表明开发者意识到这个通用缓存未来可能需要进行细分。
Kotlin
/*
CustomGlideModule.kt:
这个文件定义了一个名为 CustomGlideModule 的自定义 AppGlideModule。
该模块中的 applyOptions 方法用于自定义 Glide 的磁盘缓存。
它设置了一个特定的磁盘缓存目录 (ting/image_cache) 和默认的缓存大小。
这确保了图片被存储在设备上一个可预测的位置,从而避免了对同一图片重复进行网络请求。
*/
@GlideModule
class CustomGlideModule:AppGlideModule() {
companion object {
const val PATH_IMAGE_CACHE = "ting/image_cache"
}
override fun applyOptions(context: Context, builder: GlideBuilder) {
//fixme 图片缓存目前是公用的 没有按音频来源分开储存
val diskCacheSizeBytes = DiskCache.Factory.DEFAULT_DISK_CACHE_SIZE
builder.setDiskCache(InternalCacheDiskCacheFactory(context, PATH_IMAGE_CACHE, diskCacheSizeBytes.toLong()))
}
}
之后可以采用GlideUtil,这个对象充当一个集中式、高层级的 API,用于加载具有特定样式的图片。开发者无需在整个应用中重复编写相同的 Glide 请求(包含变换、占位符和错误处理),只需调用 GlideUtil.loadRoundImage
等方法即可。这提高了代码的可重用性和一致性。
Kotlin
object GlideUtil {
val itemBlurWithCornerTransformation by lazy {
BlurTransformation(20, 2)// 模糊效果
}
val letingRecommendItemBlurTransformation by lazy {
BlurTransformation(25, 2)
}
val coverBlurTransformation by lazy {
BlurTransformation(25, 3)
}
fun loadRoundImage(
context: Context,
uri: Any?,
imageView: ImageView,
isCircle: Boolean,
radius: Float?,
placeHolder: Drawable?,
options: RequestOptions?
) {
Glide.with(context)
.asBitmap()
.load(uri)
.placeholder(placeHolder)
.error(placeHolder)
.apply {
options?.let {
apply(it)
}
}
.into(object : BitmapImageViewTarget(imageView) {
override fun onLoadStarted(placeholder: Drawable?) {
val roundedDrawable = placeholder?.toRoundedDrawable(
context,
placeholder.intrinsicWidth,// 此时不能用imageView.width 经测试此时还获取不到
placeholder.intrinsicHeight,
radius,
isCircle
)
super.onLoadStarted(roundedDrawable)
}
override fun setResource(resource: Bitmap?) {
val roundedDrawable = resource?.toRoundedDrawable(
context,
radius,
isCircle
)
getView().setImageDrawable(roundedDrawable)
}
override fun onLoadFailed(errorDrawable: Drawable?) {
val roundedDrawable = errorDrawable?.toRoundedDrawable(
context,
getView().width,
getView().height,
radius,
isCircle
)
super.onLoadFailed(roundedDrawable)
}
})
}
fun loadRoundDoubleBorderImage(
context: Context,
path: String?,
imageView: ImageView,
placeHolder: Drawable?,
innerBorder: Int?,
innerColor: String?,
outBorder: Int?,
outColorStart: String?,
outColorEnd: String?
) {
Glide.with(context)
.asBitmap()
.load(path)
.placeholder(placeHolder)
.apply {
if (innerBorder != null && innerColor != null && outBorder != null
&& outColorStart != null && outColorEnd != null) {
transform(
CircleImageTransformer(
innerBorder,
innerColor,
outBorder,
outColorStart,
outColorEnd
)
)
} else {
IllegalArgumentException("loadRoundDoubleBorderImage,border参数不全").printStackTrace()
}
}
.into(imageView)
}
}
fun Drawable.toRoundedDrawable(context: Context, width: Int?, height: Int?, radius: Float?, isCircle: Boolean): Drawable? {
if ((radius ?: 0f) <= 0 && !isCircle) return this
val w = (width ?: 1).coerceAtLeast(1)
val h = (height ?: 1).coerceAtLeast(1)
val bitmap = this.toBitmapOrNull(w, h, Bitmap.Config.ARGB_8888) ?: return this
val circularBitmapDrawable = RoundedBitmapDrawableFactory.create(context.resources, bitmap)
when {
isCircle -> {
circularBitmapDrawable.isCircular = true
}
radius != null -> {
circularBitmapDrawable.isCircular = false
circularBitmapDrawable.cornerRadius = radius
}
else -> {
return this
}
}
return circularBitmapDrawable
}
fun Bitmap.toRoundedDrawable(context: Context, radius: Float?, isCircle: Boolean): Drawable {
if ((radius ?: 0f) <= 0 && !isCircle) return BitmapDrawable(context.resources, this)
val circularBitmapDrawable = RoundedBitmapDrawableFactory.create(context.resources, this)
when {
isCircle -> {
circularBitmapDrawable.isCircular = true
}
radius != null -> {
circularBitmapDrawable.isCircular = false
circularBitmapDrawable.cornerRadius = radius
}
else -> {
return BitmapDrawable(context.resources, this)
}
}
return circularBitmapDrawable
}
fun drawableToBitmap(drawable: Drawable): Bitmap? {
var bitmap: Bitmap? = null
if (drawable is BitmapDrawable) {
return drawable.bitmap
}
bitmap = if (drawable.intrinsicWidth <= 0 || drawable.intrinsicHeight <= 0) {
Bitmap.createBitmap(
1,
1,
Bitmap.Config.ARGB_8888
) // Single color bitmap will be created of 1x1 pixel
} else {
Bitmap.createBitmap(
drawable.intrinsicWidth,
drawable.intrinsicHeight,
Bitmap.Config.ARGB_8888
)
}
val canvas = Canvas(bitmap)
drawable.setBounds(0, 0, canvas.width, canvas.height)
drawable.draw(canvas)
return bitmap
}
class CircleImageTransformer(
private val innerBorder: Int,
private val innerColor: String,
private val outBorder: Int,
private val outColorStart: String,
private val outColorEnd: String
) : BitmapTransformation() {
companion object {
private const val VERSION = 1
private const val ID =
"com.bumptech.glide.load.resource.bitmap.CircleImageTransformer.$VERSION"
private val ID_BYTES = ID.toByteArray(Key.CHARSET)
}
private var mPaint: Paint? = null
init {
mPaint = Paint(Paint.ANTI_ALIAS_FLAG)
mPaint?.style = Paint.Style.STROKE
}
override fun transform(
pool: BitmapPool,
toTransform: Bitmap,
outWidth: Int,
outHeight: Int
): Bitmap {
val border = innerBorder + outBorder
//图片进行缩放,去掉外边框
val bitmap: Bitmap =
TransformationUtils.centerCrop(pool, toTransform, outWidth - border, outHeight - border)
//裁剪为圆型
val circleBitmap: Bitmap =
TransformationUtils.circleCrop(pool, bitmap, outWidth - border, outHeight - border)
// 获取半径
var destMinEdge = outWidth.coerceAtMost(outHeight);
var radius = (destMinEdge / 2f)
val result = pool.get(outWidth, outHeight, circleBitmap.config)
var canvas = Canvas(result)
//将裁剪的圆形头像绘制到整个bitmap的中心区域,中心区域为减掉外边框的区域
canvas.drawBitmap(
circleBitmap, null,
Rect(border, border, outWidth - border, outHeight - border),
null
)
//绘制内边框,外边框需要和内边框叠加,所以宽度为内外边框之和,注意绘制圆的时候的半径的长度,需要减去画笔的一半,绘制的圆正好在设置的半径上的话,会有一部分绘制到圆外
mPaint?.strokeWidth = border.toFloat()
mPaint?.color = Color.parseColor(innerColor)
canvas.drawCircle(radius, radius, radius - mPaint!!.strokeWidth / 2, mPaint!!)
//绘制外边框,需要渐变
mPaint?.strokeWidth = outBorder.toFloat()
val linearGradient = LinearGradient(
radius, 0f, radius, radius * 2,
Color.parseColor(outColorStart), Color.parseColor(outColorEnd),
Shader.TileMode.MIRROR
)
mPaint?.shader = linearGradient
canvas.drawCircle(radius, radius, radius - mPaint!!.strokeWidth / 2, mPaint!!)
bitmap.recycle()
circleBitmap.recycle()
return result;
}
override fun equals(o: Any?): Boolean {
return o is CircleImageTransformer
}
override fun hashCode(): Int {
return ID.hashCode()
}
override fun updateDiskCacheKey(messageDigest: MessageDigest) {
messageDigest.update(ID_BYTES)
}
}
该类主要作为一个工具类来使用,统一从中调用图片加载和显示,图片变换与效果的属性。
最后还是使用的是工具类型CircleImageTransformer
和 GlideCircleBoardTransform
用于实现 Glide 或常用变换库中不直接提供的非标准图片效果。
-
CircleImageTransformer
: 这个类用于创建带有两个不同边框的圆形图片,其中外边框具有特定的线性渐变。这种特定的视觉设计需要自定义的绘制实现。 -
GlideCircleBoardTransform
: 这个类处理更复杂的需求,即绘制具有特定颜色和径向渐变的三个边框。直接在ImageView
中创建这种自定义效果既繁琐又低效。通过将这些逻辑封装在 Glide 的BitmapTransformation
中,应用可以利用 Glide 内置的内存和磁盘缓存功能来缓存已变换的图片,从而提高性能和用户体验。
Kotlin
class GlideCircleBoardTransform : BitmapTransformation {
private var mPaint: Paint? = null
private var innerBorder = 0
private var innerColor: String? = null
private var centerBorder = 0
private var centerColorStart: String? = null
private var centerColorEnd: String? = null
private var outBorder = 9
private var outColorStart: String? = "#80000000"
private var outColorEnd: String? = "#00000000"
constructor() : super() {}
constructor(
innerBorder: Int,
innerColor: String?,
centerBorder: Int,
centerColorStart: String?,
centerColorEnd: String?
) : super() {
this.innerBorder = innerBorder
this.innerColor = innerColor
this.centerBorder = centerBorder
this.centerColorStart = centerColorStart
this.centerColorEnd = centerColorEnd
mPaint = Paint(Paint.ANTI_ALIAS_FLAG)
mPaint!!.isAntiAlias = true
mPaint!!.style = Paint.Style.STROKE
}
override fun transform(
pool: BitmapPool,
toTraansform: Bitmap,
outWidth: Int,
outHeight: Int
): Bitmap {
val border = innerBorder + centerBorder + outBorder
//图片进行缩放,去掉外边框
val bitmap = TransformationUtils.centerCrop(
pool,
toTraansform,
outWidth - border,
outHeight - border
)
//裁剪为圆型
val circleBitmap =
TransformationUtils.circleCrop(pool, bitmap, outWidth - border, outHeight - border)
// 获取半径
val destMinEdge = Math.max(outWidth, outHeight)
val radius = destMinEdge / 2
val resultBitmap = pool[outWidth, outHeight, circleBitmap.config]
val canvas = Canvas(resultBitmap)
//将裁剪的圆形头像绘制到整个bitmap的中心区域,中心区域为减掉外边框的区域
canvas.drawBitmap(
circleBitmap,
null,
Rect(border, border, outWidth - border, outHeight - border),
null
)
//绘制内边框,外边框需要和内边框叠加,所以宽度为内外边框之和,注意绘制圆的时候的半径的长度,需要减去画笔的一半,绘制的圆正好在设置的半径上的话,会有一部分绘制到圆外
mPaint!!.strokeWidth = border.toFloat()
mPaint!!.color = Color.parseColor(innerColor)
canvas.drawCircle(
radius.toFloat(),
radius.toFloat(),
radius - mPaint!!.strokeWidth / 2,
mPaint!!
)
//绘制中间边框,需要渐变
mPaint!!.strokeWidth = centerBorder.toFloat()
val centerGradient = RadialGradient(
radius.toFloat(),
radius.toFloat(),
radius.toFloat()-outBorder,
Color.parseColor(centerColorStart),
Color.parseColor(centerColorEnd),
Shader.TileMode.MIRROR
)
mPaint!!.shader = centerGradient
canvas.drawCircle(
radius.toFloat(),
radius.toFloat(),
radius - mPaint!!.strokeWidth /2 -outBorder,
mPaint!!
)
//绘制外边框,需要渐变
mPaint!!.strokeWidth = outBorder.toFloat()
val outGradient = RadialGradient(
radius.toFloat(),
radius.toFloat(),
radius.toFloat(),
Color.parseColor(outColorStart),
Color.parseColor(outColorEnd),
Shader.TileMode.MIRROR
)
mPaint!!.shader = outGradient
canvas.drawCircle(
radius.toFloat(),
radius.toFloat(),
radius - mPaint!!.strokeWidth /2,
mPaint!!
)
bitmap.recycle()
circleBitmap.recycle()
return resultBitmap
}
override fun updateDiskCacheKey(messageDigest: MessageDigest) {}
}
随后即是Glide的技术博客知识总结
三、为什么需要三级缓存?
在移动应用开发中,图片加载性能直接影响用户体验。传统图片加载方案存在以下痛点:
- 内存占用高:未压缩的大图直接占用大量内存
- 重复下载:相同图片多次从网络获取
- 弱网体验差:离线场景无法加载图片
因此下面出现的三级缓存就很好的处理了上述问题
1.内存缓存(Memory Cache)
Kotlin
import android.content.Context;
import com.bumptech.glide.Glide;
import com.bumptech.glide.GlideBuilder;
import com.bumptech.glide.load.engine.cache.LruResourceCache;
import com.bumptech.glide.module.AppGlideModule;
public class CustomGlideModule extends AppGlideModule {
@Override
public void applyOptions(Context context, GlideBuilder builder) {
// 获取设备的最大内存
int maxMemory = (int) Runtime.getRuntime().maxMemory();
// 计算内存缓存的大小,这里设置为最大内存的15%
int memoryCacheSize = maxMemory / 1024 / 1024 * 15;
// 创建LruResourceCache对象
builder.setMemoryCache(new LruResourceCache(memoryCacheSize));
}
}
解释:我们自定义了一个 AppGlideModule
类 CustomGlideModule
。在 applyOptions
方法里,先获取设备的最大内存,接着计算出内存缓存的大小,这里设定为最大内存的 15%,最后使用 LruResourceCache
来设置内存缓存。
- LruCache 实现:最大缓存容量为应用内存的 15%
LruCache
的全称是 "Least Recently Used Cache"(最近最少使用缓存)。它的核心思想是,当缓存达到其最大容量时,它会移除最近最少被使用的数据来为新数据腾出空间。这种策略假设最近使用过的数据在将来也更有可能被使用。
- 缓存键生成策略:包含 URL + 宽高 + 格式的复合键
Glide 的缓存键生成策略旨在确保每个唯一的图片请求都有一个唯一的标识符。仅仅使用图片的 URL 作为键是不够的,因为同一个 URL 的图片可能需要以不同的尺寸、不同的格式或应用不同的变换来加载。
- 内存泄漏防护:通过 Glide 生命周期绑定
在 Android 中,一个常见的内存泄漏问题是 Activity
或 Fragment
在被销毁后,仍然被某些后台任务或回调所引用,导致它们无法被垃圾回收。这只是简单结果,里面的为什么Glide可以绑定到Activity中又是一个问题。
2.磁盘缓存(Disk Cache)
Kotlin
import android.content.Context;
import com.bumptech.glide.Glide;
import com.bumptech.glide.GlideBuilder;
import com.bumptech.glide.load.engine.cache.DiskLruCacheFactory;
import com.bumptech.glide.module.AppGlideModule;
public class CustomDiskCacheGlideModule extends AppGlideModule {
@Override
public void applyOptions(Context context, GlideBuilder builder) {
// 设置磁盘缓存的路径
String diskCachePath = context.getCacheDir().getPath() + "/glide_cache";
// 设置磁盘缓存的大小为100MB
int diskCacheSize = 1024 * 1024 * 100;
// 创建DiskLruCacheFactory对象
builder.setDiskCache(new DiskLruCacheFactory(diskCachePath, diskCacheSize));
}
}
解释:我们同样自定义了一个 AppGlideModule
类 CustomDiskCacheGlideModule
。在 applyOptions
方法里,先设置磁盘缓存的路径,再将磁盘缓存的大小设置为 100MB,最后使用 DiskLruCacheFactory
来设置磁盘缓存。
- 磁盘缓存目录:应用专属缓存目录
通过 context.getCacheDir().getPath()
获取。这种做法是 Android 开发的最佳实践,它有几个优点:
-
安全和隔离: 每个应用的缓存目录都是独立的,其他应用无法直接访问,保护了应用的数据安全。
-
自动清理: 当用户卸载应用时,这些目录下的数据会被系统自动删除,避免了垃圾文件的残留。
-
系统管理: 在设备存储空间不足时,系统可能会自动清理这些缓存目录,为其他应用释放空间,但这也意味着缓存可能不会永久保留。
- 缓存淘汰算法:LRU 策略
磁盘缓存采用的是 LRU (Least Recently Used) 策略,这与内存缓存的原理相同。当磁盘缓存达到其设定的最大容量时,Glide 会根据文件最近的使用时间来决定淘汰哪些文件。
3.网络缓存(Network Cache)
Kotlin
import android.content.Context;
import com.bumptech.glide.Glide;
import com.bumptech.glide.GlideBuilder;
import com.bumptech.glide.integration.okhttp3.OkHttpUrlLoader;
import com.bumptech.glide.load.model.GlideUrl;
import com.bumptech.glide.module.AppGlideModule;
import okhttp3.Cache;
import okhttp3.OkHttpClient;
import java.io.InputStream;
public class CustomNetworkCacheGlideModule extends AppGlideModule {
@Override
public void registerComponents(Context context, Glide glide, Registry registry) {
// 设置网络缓存的路径
Cache cache = new Cache(context.getCacheDir(), 1024 * 1024 * 50);
/*
创建和配置 OkHttp 客户端:
在 registerComponents 方法中,首先创建了一个 Cache 对象,指定了缓存文件的存储路径(通常是应用的缓存目录)和最大缓存大小(这里是 50MB)。
然后,创建一个 OkHttpClient 实例,并将这个配置好的 cache 对象传递给它。这一步是核心,它告诉 OkHttp:"当你处理网络请求时,请使用这个缓存来存储和读取数据。"
*/
OkHttpClient client = new OkHttpClient.Builder()
.cache(cache)
.build();
// 注册OkHttpUrlLoader,让Glide使用OkHttp进行网络请求
/*
替换 Glide 的网络加载器:
registry.replace(...) 这一行是整个集成的关键。它告诉 Glide:"当你需要加载一个 GlideUrl
(即一个 URL 链接)并将其转换为 InputStream(即网络流)时,请不要使用你默认的加载器,而是使用我指定的 OkHttpUrlLoader.Factory。"
OkHttpUrlLoader.Factory 是一个专门为 Glide 和 OkHttp 之间搭桥的类。它接收我们配置好的 OkHttpClient,并在内部使用这个客户端来执行所有的网络请求。
*/
registry.replace(GlideUrl.class, InputStream.class, new OkHttpUrlLoader.Factory(client));
}
}
解释:自定义了 CustomNetworkCacheGlideModule
类,在 registerComponents
方法中,先设置网络缓存的路径和大小(这里是 50MB),然后创建 OkHttpClient
并设置缓存,最后注册 OkHttpUrlLoader
,使 Glide 采用 OkHttp 进行网络请求。
- 结合 OkHttp 实现 HTTP 缓存
当 OkHttp 被集成到 Glide 后,Glide 就不再自己管理网络层的缓存了,而是完全依赖 OkHttp 的 HTTP 缓存机制。这样一来,Glide 就能自动获得以下优势:
-
HTTP 缓存控制 :OkHttp 会自动读取服务器返回的
Cache-Control
等 HTTP 响应头。如果服务器返回了max-age=3600
,OkHttp 就会将图片缓存一小时。在这期间,即使 Glide 重复加载同一张图片,OkHttp 也会直接从本地缓存中返回,完全不会发起网络请求。 -
缓存验证 :如果服务器返回的是
no-cache
,OkHttp 会在缓存过期后,自动带上If-None-Match
(ETag
)或If-Modified-Since
请求头向服务器发送验证请求 。如果服务器响应 304 Not Modified,说明图片没有更新,OkHttp 就会使用本地缓存,从而节省了下载图片内容的流量。
为什么不将 OkHttp 的重试机制直接应用到 Glide?
-
Glide 的设计:Glide 在图片加载失败后,本身就有一套复杂的失败重试逻辑,可以配置不同的策略。
-
OkHttp 重试的场景:OkHttp 的重试机制通常是为了处理临时的网络波动或服务器瞬时故障。它更适合在网络请求的底层直接处理。
-
功能分离 :将缓存交给 OkHttp,将上层的图片加载和重试交给 Glide,是一种职责分离 的设计模式。这样可以保持代码的清晰和模块化,让每个组件专注于自己的核心功能。如果需要在 Glide 的网络层实现重试,通常会创建一个自定义的 OkHttp
Interceptor
,然后将这个带重试功能的OkHttpClient
传递给 Glide。
使用 Glide 加载图片
Kotlin
import android.os.Bundle;
import android.widget.ImageView;
import androidx.appcompat.app.AppCompatActivity;
import com.bumptech.glide.Glide;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ImageView imageView = findViewById(R.id.imageView);
String imageUrl = "https://example.com/image.jpg";
Glide.with(this)
.load(imageUrl)
// 设置磁盘缓存策略为缓存所有类型的图片
.diskCacheStrategy(DiskCacheStrategy.ALL)
.into(imageView);
}
}
在 MainActivity
中,我们使用 Glide 加载图片。通过 diskCacheStrategy(DiskCacheStrategy.ALL)
方法设置磁盘缓存策略,这里表示缓存所有类型的图片。

四、自定义图片缓存框架
设计一个图片请求框架的缓存机制时,需要综合考虑缓存的性能、内存占用、数据持久化等因素。通常可以采用内存缓存和磁盘缓存相结合的多级缓存策略,以下是详细的设计思路和实现方案:
设计思路
- 内存缓存 :使用
LruCache
(Least Recently Used Cache,最近最少使用缓存)来实现内存缓存,它会自动回收最近最少使用的图片,以保证内存的合理使用。 - 磁盘缓存 :使用
DiskLruCache
来实现磁盘缓存,将图片持久化到本地磁盘,以便在网络不可用或需要重复使用图片时快速获取。 - 多级缓存策略:先从内存缓存中查找图片,如果找不到再从磁盘缓存中查找,最后才从网络请求图片。当从网络获取到图片后,同时将图片存入内存缓存和磁盘缓存。
代码实现
内存缓存(LruCache)
java
import android.graphics.Bitmap;
import android.util.LruCache;
// 内存缓存类,使用 LruCache 实现
public class MemoryCache {
// LruCache 实例,用于存储图片的键值对,键为图片的标识,值为图片的 Bitmap 对象
private LruCache<String, Bitmap> lruCache;
// 构造函数,初始化 LruCache
public MemoryCache() {
// 获取应用程序运行时的最大可用内存,单位为 KB
int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
// 使用最大可用内存的 1/8 作为 LruCache 的缓存大小
int cacheSize = maxMemory / 8;
// 初始化 LruCache,并重写 sizeOf 方法,用于计算每个图片对象占用的内存大小
lruCache = new LruCache<String, Bitmap>(cacheSize) {
@Override
protected int sizeOf(String key, Bitmap bitmap) {
// 返回图片占用的内存大小,单位为 KB
return bitmap.getByteCount() / 1024;
}
};
}
// 向内存缓存中添加图片
public void put(String key, Bitmap bitmap) {
// 先检查缓存中是否已经存在该图片,如果不存在则添加
if (get(key) == null) {
lruCache.put(key, bitmap);
}
}
// 从内存缓存中获取图片
public Bitmap get(String key) {
return lruCache.get(key);
}
// 从内存缓存中移除指定的图片
public void remove(String key) {
lruCache.remove(key);
}
}
磁盘缓存(DiskLruCache)
java
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Environment;
import com.jakewharton.disklrucache.DiskLruCache;
import java.io.*;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
// 磁盘缓存类,使用 DiskLruCache 实现
public class DiskCache {
// 应用版本号,用于 DiskLruCache 的初始化
private static final int APP_VERSION = 1;
// 每个键对应的值的数量,这里设置为 1
private static final int VALUE_COUNT = 1;
// 磁盘缓存的最大大小,这里设置为 10MB
private static final long CACHE_SIZE = 10 * 1024 * 1024;
// DiskLruCache 实例,用于将图片持久化到本地磁盘
private DiskLruCache diskLruCache;
// 构造函数,初始化 DiskLruCache
public DiskCache(Context context) {
try {
// 获取磁盘缓存的目录
File cacheDir = getDiskCacheDir(context, "bitmap");
// 如果目录不存在,则创建该目录
if (!cacheDir.exists()) {
cacheDir.mkdirs();
}
// 打开 DiskLruCache 实例
diskLruCache = DiskLruCache.open(cacheDir, APP_VERSION, VALUE_COUNT, CACHE_SIZE);
} catch (IOException e) {
e.printStackTrace();
}
}
// 向磁盘缓存中添加图片
public void put(String key, Bitmap bitmap) {
DiskLruCache.Editor editor = null;
try {
// 获取 DiskLruCache 的编辑器,用于写入数据
editor = diskLruCache.edit(hashKeyForDisk(key));
if (editor != null) {
// 获取编辑器的输出流
OutputStream outputStream = editor.newOutputStream(0);
// 将图片以 JPEG 格式压缩并写入输出流
if (bitmap.compress(Bitmap.CompressFormat.JPEG, 100, outputStream)) {
// 提交写入操作
editor.commit();
} else {
// 取消写入操作
editor.abort();
}
// 关闭输出流
outputStream.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
// 从磁盘缓存中获取图片
public Bitmap get(String key) {
try {
// 获取 DiskLruCache 的快照,用于读取数据
DiskLruCache.Snapshot snapshot = diskLruCache.get(hashKeyForDisk(key));
if (snapshot != null) {
// 获取快照的输入流
InputStream inputStream = snapshot.getInputStream(0);
// 将输入流解码为 Bitmap 对象
return BitmapFactory.decodeStream(inputStream);
}
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
// 从磁盘缓存中移除指定的图片
public void remove(String key) {
try {
diskLruCache.remove(hashKeyForDisk(key));
} catch (IOException e) {
e.printStackTrace();
}
}
// 获取磁盘缓存的目录
private File getDiskCacheDir(Context context, String uniqueName) {
String cachePath;
// 判断外部存储是否可用
if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())
|| !Environment.isExternalStorageRemovable()) {
// 如果可用,使用外部缓存目录
cachePath = context.getExternalCacheDir().getPath();
} else {
// 如果不可用,使用内部缓存目录
cachePath = context.getCacheDir().getPath();
}
// 返回缓存目录的文件对象
return new File(cachePath + File.separator + uniqueName);
}
// 对键进行 MD5 哈希处理,确保键的唯一性
private String hashKeyForDisk(String key) {
String cacheKey;
try {
// 获取 MD5 消息摘要实例
final MessageDigest mDigest = MessageDigest.getInstance("MD5");
// 更新消息摘要
mDigest.update(key.getBytes());
// 将消息摘要转换为十六进制字符串
cacheKey = bytesToHexString(mDigest.digest());
} catch (NoSuchAlgorithmException e) {
// 如果不支持 MD5 算法,使用键的哈希码作为缓存键
cacheKey = String.valueOf(key.hashCode());
}
return cacheKey;
}
// 将字节数组转换为十六进制字符串
private String bytesToHexString(byte[] bytes) {
StringBuilder sb = new StringBuilder();
for (byte b : bytes) {
String hex = Integer.toHexString(0xFF & b);
if (hex.length() == 1) {
sb.append('0');
}
sb.append(hex);
}
return sb.toString();
}
}
多级缓存管理类
java
import android.content.Context;
import android.graphics.Bitmap;
// 多级缓存管理类,管理内存缓存和磁盘缓存
public class ImageCacheManager {
// 内存缓存实例
private MemoryCache memoryCache;
// 磁盘缓存实例
private DiskCache diskCache;
// 构造函数,初始化内存缓存和磁盘缓存
public ImageCacheManager(Context context) {
memoryCache = new MemoryCache();
diskCache = new DiskCache(context);
}
// 向多级缓存中添加图片,同时添加到内存缓存和磁盘缓存
public void put(String key, Bitmap bitmap) {
memoryCache.put(key, bitmap);
diskCache.put(key, bitmap);
}
// 从多级缓存中获取图片,先从内存缓存中查找,若找不到再从磁盘缓存中查找
public Bitmap get(String key) {
Bitmap bitmap = memoryCache.get(key);
if (bitmap == null) {
bitmap = diskCache.get(key);
if (bitmap != null) {
// 如果从磁盘缓存中找到图片,将其添加到内存缓存中
memoryCache.put(key, bitmap);
}
}
return bitmap;
}
// 从多级缓存中移除指定的图片,同时从内存缓存和磁盘缓存中移除
public void remove(String key) {
memoryCache.remove(key);
diskCache.remove(key);
}
}
使用示例
java
import android.content.Context;
import android.graphics.Bitmap;
// 图片请求框架类,使用多级缓存管理图片
public class ImageRequestFramework {
// 多级缓存管理实例
private ImageCacheManager imageCacheManager;
// 构造函数,初始化多级缓存管理实例
public ImageRequestFramework(Context context) {
imageCacheManager = new ImageCacheManager(context);
}
// 从缓存或网络获取图片
public Bitmap getImage(String url) {
// 生成图片的唯一缓存键
String key = generateKey(url);
// 从多级缓存中获取图片
Bitmap bitmap = imageCacheManager.get(key);
if (bitmap == null) {
// 如果缓存中没有图片,从网络下载图片
bitmap = downloadImageFromNetwork(url);
if (bitmap != null) {
// 如果下载成功,将图片添加到多级缓存中
imageCacheManager.put(key, bitmap);
}
}
return bitmap;
}
// 生成图片的唯一缓存键
private String generateKey(String url) {
// 这里简单地使用 URL 作为缓存键,实际应用中可以根据需求进行处理
return url;
}
// 从网络下载图片,这里只是示例,需要实现具体的网络请求逻辑
private Bitmap downloadImageFromNetwork(String url) {
// 实现网络请求图片的逻辑
return null;
}
}
整体总结:
Glide作为Android生态中广泛使用的图片加载库,其核心价值在于通过精心设计的三级缓存机制和高度可扩展的架构解决了图片加载的性能和体验问题。
该库采用内存缓存、磁盘缓存和网络缓存相结合的多级策略,首先从内存快速获取图片,其次查询本地磁盘缓存,最后才发起网络请求,这种设计不仅大幅减少了重复的网络流量消耗,还能在弱网或离线环境下保持应用的正常使用。
更重要的是,Glide提供了丰富的图片变换功能,开发者可以通过内置的裁剪、圆角等效果或自定义BitmapTransformation来实现复杂的视觉需求,而将这些功能封装成统一的工具类后,进一步简化了代码调用并提高了项目的可维护性。正是由于在缓存效率、内存管理、生命周期集成以及API易用性方面的全面优化,Glide成为处理复杂图片场景的首选解决方案。