学会设计模式之策略模式(图片加载框架)

1,064 阅读5分钟

1、前言

系列文章

5分钟学会设计模式之策略模式(Strategy Pattern)

上一篇,我们举例一个View的例子,可能不是很清晰,所以根据公司用的图片加载Glide来封装一个图片加载框架,非常的简洁易懂~

有时候Glide已经不能完全满足需求,或者出现了一些性能或稳定性问题。这时候,如果我们在代码中直接使用了特定的框架,那么我们需要对整个应用程序进行修改,这将会带来很大的风险和困难。对于图片加载框架来说,我们需要封装一些抽象的接口,使得我们可以在不影响应用程序其他部分的情况下,轻松更换底层的图片加载库。同时,我们还需要将图片加载的逻辑封装在一个独立的模块中,使得我们可以单独对其进行更新和维护。

如果您有任何疑问、对文章写的不满意、发现错误或者有更好的方法,欢迎在评论、私信或邮件中提出,非常感谢您的支持。🙏

2、抽象行为(策略接口)

IImageEngine接口就是策略接口,定义了加载图片的三个方法:loadResource、loadUrl和loadBitmap。它抽象出了不同的图片加载库所需实现的核心功能,并为具体策略提供了统一的方法签名。

interface IImageEngine {
    /**
     * 给定的资源 ID 加载一个图片资源
     */
    fun loadResource(
        view: ImageView?,
        @DrawableRes resourceId: Int,
        radius: Int = 0,
        callback: ImageLoadCallback?
    )
    /**
     * 指定的 URL 加载一张图片
     */
    fun loadUrl(view: ImageView?, url: String, radius: Int = 0, callback: ImageLoadCallback?)
​
    /**
     * 从指定的 URL 加载一张图片,并返回一个 Bitmap 对象。
     */
    fun loadBitmap(context: Context, url: String, radius: Int = 0, callback: ImageLoadCallback?)
}

3、加载回调

当然,所有的加载,不会总是一帆风顺,所以我们需要一个加载回调

interface ImageLoadCallback {
    fun onSuccess(resource: Bitmap?) {}
    fun onLoadStarted(placeholder: Drawable?) {}
    fun onLoadFailed(errorDrawable: Drawable?) {}
}

4、GlideEngine实现

GlideEngine是实现了IImageEngine接口的具体实现类,我们通过阅读代码就可以得知

internal object GlideEngine : IImageEngine {
    override fun loadResource(
        view: ImageView?,
        resourceId: Int,
        radius: Int,
        callback: ImageLoadCallback?
    ) {
        view?.run {
            Glide.with(this)
                .load(resourceId).also {
                    if (radius > 0) {
                        it.apply(RequestOptions().transform(RoundedCorners(radius)))
                    }
                }.into(createTarget(view, callback))
        }
    }
​
    override fun loadUrl(
        view: ImageView?,
        url: String,
        radius: Int,
        callback: ImageLoadCallback?
    ) {
        view?.run {
            Glide.with(context).load(url).also {
                if (radius > 0) {
                    it.apply(RequestOptions().transform(RoundedCorners(radius)))
                }
            }.into(createTarget(view, callback))
        }
    }
​
    override fun loadBitmap(
        context: Context,
        url: String, radius: Int,
        callback: ImageLoadCallback?
    ) {
        Glide.with(context).asBitmap().load(url).also {
            if (radius > 0) {
                it.apply(RequestOptions().transform(RoundedCorners(radius)))
            }
        }.into(createBitmapTarget(callback))
    }
​
    private fun createTarget(
        view: ImageView?,
        callback: ImageLoadCallback?
    ): CustomTarget<Drawable> {
        return object : CustomTarget<Drawable>() {
            override fun onResourceReady(resource: Drawable, transition: Transition<in Drawable>?) {
                view?.setImageDrawable(resource)
                callback?.onSuccess(null)
            }
​
            override fun onLoadStarted(placeholder: Drawable?) {
                callback?.onLoadStarted(placeholder)
            }
​
            override fun onLoadFailed(errorDrawable: Drawable?) {
                callback?.onLoadFailed(errorDrawable)
            }
​
            override fun onLoadCleared(placeholder: Drawable?) {}
        }
    }
​
    private fun createBitmapTarget(callback: ImageLoadCallback?): CustomTarget<Bitmap> {
        return object : CustomTarget<Bitmap>() {
            override fun onResourceReady(resource: Bitmap, transition: Transition<in Bitmap>?) {
                callback?.onSuccess(resource)
            }
            override fun onLoadStarted(placeholder: Drawable?) {
                callback?.onLoadStarted(placeholder)
            }
            override fun onLoadFailed(errorDrawable: Drawable?) {
                callback?.onLoadFailed(errorDrawable)
            }
            override fun onLoadCleared(placeholder: Drawable?) {}
        }
    }
}

5、COilEngine实现

COilEngine是实现了IImageEngine接口的具体实现类,我们通过阅读代码就可以得知

class COilEngine : IImageEngine {
​
    override fun loadResource(
        view: ImageView?,
        @DrawableRes resourceId: Int,
        radius: Int,
        callback: ImageLoadCallback?
    ) {
        view?.setImageResource(resourceId)
        callback?.onSuccess(null)
    }
​
    override fun loadUrl(
        view: ImageView?,
        url: String,
        radius: Int,
        callback: ImageLoadCallback?
    ) {
        view?.let { imageView ->
            val request = createImageRequest(url, radius, callback)
            imageLoader().execute(request) {
                it.drawable?.let { drawable ->
                    imageView.setImageDrawable(drawable)
                    callback?.onSuccess(null)
                } ?: run {
                    callback?.onLoadFailed(null)
                }
            }
        }
    }
​
    override fun loadBitmap(
        context: Context,
        url: String,
        radius: Int,
        callback: ImageLoadCallback?
    ) {
        val request = createImageRequest(url, radius, callback)
        imageLoader().execute(request) {
            it.toBitmap()?.let { bitmap ->
                callback?.onSuccess(bitmap)
            } ?: run {
                callback?.onLoadFailed(null)
            }
        }
    }
​
    private fun createImageRequest(
        url: String,
        radius: Int,
        callback: ImageLoadCallback?
    ): ImageRequest {
        val builder = ImageRequest.Builder(context)
            .data(url)
            .parameters(Parameters.Builder().set("radius", radius).build())
​
        if (callback != null) {
            builder.target(object : Target {
                override fun onStart(placeholder: Drawable?) {
                    callback.onLoadStarted(placeholder)
                }
                override fun onError(error: Drawable?) {
                    callback.onLoadFailed(error)
                }
                override fun onSuccess(result: Drawable) {}
​
                override fun onCancel() {}
            })
        }
        return builder.build()
    }
​
    private fun imageLoader(): coil.ImageLoader {
        return Coil.loader()
    }
}

6、上下文类的定义

ImageLoader类则是上下文类,负责根据调用方的需要,选择不同的图片加载库。它把具体的策略实现作为一个参数,通过构造函数或者其他方式注入到类中。在执行图片加载时,ImageLoader对象调用传入的具体策略实现的方法。

一般而言,我们会这么写,通过一个单例,来调用传入的IImageEngine实现类

object ImageLoader {
    private var imageEngine: IImageEngine? = null
    fun <T : IImageEngine> init(ie: T) {
        imageEngine = ie
    }
​
    fun loadImage(
        view: ImageView?,
        url: String,
        radius: Int = 0,
        callback: ImageLoadCallback? = null
    ) {
        imageEngine?.loadUrl(view, url, radius, callback)
    }
​
    fun loadResource(
        view: ImageView?,
        resourceId: Int,
        radius: Int = 0,
        callback: ImageLoadCallback? = null
    ) {
        imageEngine?.loadResource(view, resourceId, radius, callback)
    }
​
    fun loadBitmap(
        context: Context,
        url: String,
        radius: Int = 0,
        callback: ImageLoadCallback? = null
    ) {
        imageEngine?.loadBitmap(context, url, radius, callback)
    }
}

但是得益于Kotlin的语法糖,我们可以用委托的方法,省略许多的代码细节

object ImageLoader : IImageEngine by GlideEngine

其实编译出来的代码是一模一样的

image-20230219164327319

你也可以皮一点

object ImageLoader1 : IImageEngine by GlideEngine
object ImageLoader2 : IImageEngine by CoilEngine

这样调用也是非常简单了

 ImageLoader.loadUrl(imageView, url, radius, callback)

7、扩展函数

当然一个懒狗,是不会想多传一个ImageView对象的,用上Kotlin语法糖

fun ImageView.loadUrl(url: String, radius: Int = 0, callback: ImageLoadCallback?) {
    ImageLoader.loadUrl(this, url, radius, callback)
}
fun ImageView.loadRes(@DrawableRes resourceId: Int, radius: Int = 0, callback: ImageLoadCallback?) {
    ImageLoader.loadResource(this, resourceId, radius, callback)
}

这样你就可以直接把它当做ImageView的方法使用了

imageView.loadRes(xxx)

8、总结

作为一个加载图片的框架,我们需要考虑以下几点:

  1. 图片加载的来源:本地图片、网络图片、Bitmap 等。
  2. 加载图片的方式:同步加载、异步加载,图片压缩等。
  3. 图片加载的性能和效率:需要考虑内存占用、加载速度、缓存等。
  4. 对于不同的加载场景,如列表、详情页等,需要采用不同的加载策略。

到这儿我们都只考虑第一点的第一部分,不过因为主要是为了讲策略模式的应用,所以嘻嘻嘻。不过还是实现了对不同图片加载库的无缝切换

对于Glide、Coil的使用不做赘述啦

就到这)-(

如果您有任何疑问、对文章写的不满意、发现错误或者有更好的方法,欢迎在评论、私信或邮件中提出,非常感谢您的支持。🙏

“开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 5 天,点击查看活动详情