Kuikly跨端模式接入资源管理

目前我们基于Kuikly开发大多数应该都是主工程+Kuikly跨端的模式,那么我们在使用Kuikly改写原有业务的时候,肯定会存在图片资源管理的问题,那么主工程+Kuikly跨端模式下,如何可以复用主工程的图片资源实现Kuikly的业务呢?

Kuikly给我们提供了一个 KRImageAdapter.kt ,我们就以项目中的 KRImageAdapter.kt 这份具体的代码为蓝本,深入聊聊Kuikly是如何搭建起跨端与原生之间的资源桥梁的。

核心原则:依赖接口,而非实现

翻开代码,我们首先看到 KRImageAdapter 实现了一个接口:IKRImageAdapter。这是整个设计的基石。Kuikly的核心引擎并不关心图片到底从哪来、用什么库加载,它只认这个接口契约。

这种做法的好处是显而易见的:

  • 绝对解耦:Kuikly渲染引擎和原生图片加载库(这里是Glide)老死不相往来。引擎只管发号施令,适配器负责干活。
  • 自由替换 :今天我们用Glide,明天团队决定换成Coil,没问题,只需要一个新的Adapter实现,Kuikly核心代码一字不动。
  • 职责清晰:原生工程负责提供"能力",跨端模块负责"消费"。界限分明,维护轻松。

KRImageAdapter:聪明的资源调度员

KRImageAdapter 的核心方法 fetchDrawable 就像一个聪明的资源调度员。它通过检查图片路径 src 的特征,决定把任务派发给谁。

1. 原生资源 (res:):借力打力

当路径以 res: 开头(例如 "res:logo"),适配器知道这是想用App里的原生资源。

kotlin 复制代码
if (imageLoadOption.src.startsWith("res:")) {
    // 本地资源
    val resourceName = imageLoadOption.src.substring(4)
    var resId = context.resources.getIdentifier(resourceName, "mipmap", context.packageName)
    if (resId == 0) {
        // 如果在mipmap中找不到,尝试在drawable中查找
        resId = context.resources.getIdentifier(resourceName, "drawable", context.packageName)
    }

    if (resId != 0) {
        callback.invoke(ResourcesCompat.getDrawable(context.resources, resId, context.theme))
    } else {
        Log.w("KRImageAdapter", "Resource not found for: ${imageLoadOption.src}")
        callback.invoke(null)
    }
}

它没有自己去实现一套资源查找逻辑,而是直接调用了Android系统的 getIdentifier 方法。这是最聪明的做法,因为它无缝利用了Android强大的资源系统,包括对不同屏幕密度(mdpi, hdpi)的自动适配。跨端层只需要提供一个名字,剩下的交给原生。

2. 网络/本地图片:把专业的事交给专业的工具

对于httpassets或本地文件,适配器把任务甩给了Glide。

kotlin 复制代码
private fun requestImage(...) {
    // ...
    Glide.with(context).load(src).into(...)
}

这再次体现了"不重复造轮子"的原则。Glide拥有强大的缓存、生命周期管理和解码能力。KRImageAdapter 在这里扮演了一个"翻译官"的角色,把Kuikly的加载参数转换成Glide的API调用,用最短的代码实现了最强大的功能。

3. Base64字符串:性能优先

当图片数据是Base64字符串时,情况变得特殊。这通常意味着图片是动态生成的。loadFromBase64 方法展示了处理这类数据的两个关键点:

  • 放到子线程 :解码Base64和创建Bitmap是耗时操作,execOnSubThread 保证了UI线程的流畅。
  • 内存优化 :在真正解码图片前,通过 calculateInSampleSize 计算了一个合适的采样率。这意味着如果一个3000x3000像素的图片只需要显示在100x100的视图里,就没必要把完整的图片都加载到内存里,从而有效避免了OOM(内存溢出)。

写在最后

回过头来看 KRImageAdapter,它的代码并不复杂,但其背后体现的架构思想却十分清晰:

  • 定义清晰的边界 :用接口(IKRImageAdapter)作为跨端与原生的沟通桥梁。
  • 建立简单的协议 :用URI Scheme(如res:)作为双方都能听懂的"暗号"。
  • 拥抱原生生态:将专业任务(图片加载、资源查找)委托给最成熟的原生工具(Glide、Android SDK)。

它告诉我们,优秀的跨端方案,不是要重新发明一切,而是要懂得如何站在原生生态的肩膀上,优雅地"借力"。

完整源码
kotlin 复制代码
class KRImageAdapter(val context: Context) : IKRImageAdapter {

    /**
     * 根据加载选项获取Drawable对象。
     * 此方法是图片加载的入口,它会判断图片来源(本地资源、Base64、网络等),
     * 并调用相应的私有方法进行处理。
     *
     * @param imageLoadOption 图片加载选项,包含图片URL、尺寸等信息。
     * @param callback 加载完成后的回调,返回一个可空的Drawable对象。
     */
    override fun fetchDrawable(
        imageLoadOption: HRImageLoadOption,
        callback: (drawable: Drawable?) -> Unit,
    ) {
        if (imageLoadOption.src.startsWith("res:")) {
            // 本地资源
            val resourceName = imageLoadOption.src.substring(4)
            var resId = context.resources.getIdentifier(resourceName, "mipmap", context.packageName)
            if (resId == 0) {
                // 如果在mipmap中找不到,尝试在drawable中查找
                resId = context.resources.getIdentifier(resourceName, "drawable", context.packageName)
            }

            if (resId != 0) {
                callback.invoke(ResourcesCompat.getDrawable(context.resources, resId, context.theme))
            } else {
                Log.w("KRImageAdapter", "Resource not found for: ${imageLoadOption.src}")
                callback.invoke(null)
            }
        } else if (imageLoadOption.isBase64()) {
            loadFromBase64(imageLoadOption, callback)
        } else if (imageLoadOption.isWebUrl() || imageLoadOption.isAssets() || imageLoadOption.isFile()) {
            // http/assets/file 图片使用 glide 加载
            requestImage(imageLoadOption, callback)
        }
    }

    /**
     * 使用Glide请求网络、assets或文件图片。
     *
     * @param imageLoadOption 图片加载选项。
     * @param callback 加载完成后的回调。
     */
    private fun requestImage(
        imageLoadOption: HRImageLoadOption,
        callback: (drawable: Drawable?) -> Unit,
    ) {
        val src = if (imageLoadOption.isAssets()) {
            val assetPath = imageLoadOption.src.substring(HRImageLoadOption.SCHEME_ASSETS.length)
            "file:///android_asset/$assetPath"
        } else {
            imageLoadOption.src
        }
        val requestBuilder = Glide.with(context).load(src)

        if (imageLoadOption.needResize) {
            requestBuilder.override(imageLoadOption.requestWidth, imageLoadOption.requestHeight)
            when (imageLoadOption.scaleType) {
                ImageView.ScaleType.CENTER_CROP -> requestBuilder.centerCrop()
                ImageView.ScaleType.FIT_CENTER -> requestBuilder.fitCenter()
                else -> Unit // No-op for other scale types
            }
        }
        requestBuilder
            .into(object : CustomTarget<Drawable>() {

                override fun onLoadCleared(placeholder: Drawable?) {
                    callback.invoke(null)
                }

                override fun onLoadFailed(errorDrawable: Drawable?) {
                    super.onLoadFailed(errorDrawable)
                    callback.invoke(null)
                }

                override fun onResourceReady(
                    resource: Drawable,
                    transition: Transition<in Drawable>?,
                ) {
                    callback.invoke(resource)
                }
            })
    }

    /**
     * 从Base64字符串加载图片。
     * 该方法会在子线程中解码Base64字符串,并根据请求的尺寸进行采样,以避免内存溢出。
     *
     * @param imageLoadOption 图片加载选项。
     * @param callback 加载完成后的回调。
     */
    private fun loadFromBase64(
        imageLoadOption: HRImageLoadOption,
        callback: (drawable: Drawable?) -> Unit,
    ) {
        // 假设 execOnSubThread 是一个用于在后台执行的工具函数
        execOnSubThread {
            val result: Drawable? = try {
                val base64String = imageLoadOption.src.substringAfter(',')
                val bytes = Base64.decode(base64String, Base64.DEFAULT)

                val options = BitmapFactory.Options().apply {
                    inJustDecodeBounds = true
                }
                BitmapFactory.decodeByteArray(bytes, 0, bytes.size, options)

                options.inSampleSize = calculateInSampleSize(
                    options,
                    imageLoadOption.requestWidth,
                    imageLoadOption.requestHeight
                )
                options.inJustDecodeBounds = false
                options.inPreferredConfig = Bitmap.Config.ARGB_8888

                val bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size, options)
                BitmapDrawable(context.resources, bitmap)
            } catch (e: Exception) {
                // 捕获通用异常以提高健壮性(OOM、非法Base64字符串等)
                Log.e("KRImageAdapter", "Failed to load image from Base64", e)
                null
            }

            // 注意:回调现在将在后台线程上执行
            callback.invoke(result)
        }
    }

    /**
     * 计算BitmapFactory.Options的inSampleSize值。
     * 用于在解码图片时进行降采样,以节省内存。
     *
     * @param options 包含图片原始尺寸的BitmapFactory.Options对象。
     * @param reqWidth 期望的宽度。
     * @param reqHeight 期望的高度。
     * @return 计算出的inSampleSize值。
     */
    private fun calculateInSampleSize(
        options: BitmapFactory.Options,
        reqWidth: Int,
        reqHeight: Int,
    ): Int {
        // 图片的原始高宽
        val (height: Int, width: Int) = options.run { outHeight to outWidth }
        var inSampleSize = 1

        if (reqWidth <= 0 || reqHeight <= 0) {
            return inSampleSize
        }

        if (height > reqHeight || width > reqWidth) {
            val halfHeight: Int = height / 2
            val halfWidth: Int = width / 2

            // 计算最大的 inSampleSize,该值是2的幂,并确保最终图像的高宽都大于请求的高宽。
            while (halfHeight / inSampleSize >= reqHeight && halfWidth / inSampleSize >= reqWidth) {
                inSampleSize *= 2
            }
        }
        return inSampleSize
    }
}

power by Kuikly 2.2.0

相关推荐
tianchang3 小时前
深入理解 JavaScript 异步机制:从语言语义到事件循环的全景图
前端·javascript
旺仔牛仔QQ糖3 小时前
Vue3.0 Hook 使用好用多多
前端
~无忧花开~3 小时前
CSS学习笔记(五):CSS媒体查询入门指南
开发语言·前端·css·学习·媒体
程序猿小D3 小时前
【完整源码+数据集+部署教程】【零售和消费品&存货】价格标签检测系统源码&数据集全套:改进yolo11-RFAConv
前端·yolo·计算机视觉·目标跟踪·数据集·yolo11·价格标签检测系统源码
吴鹰飞侠4 小时前
AJAX的学习
前端·学习·ajax
JNU freshman4 小时前
vue 技巧与易错
前端·javascript·vue.js
落一落,掉一掉4 小时前
第十二周 waf绕过和前端加密绕过
前端
Asort4 小时前
JavaScript设计模式(十六)——迭代器模式:优雅遍历数据的艺术
前端·javascript·设计模式
Coffeeee4 小时前
Labubu很难买?那是因为还没有用Compose来画一个
前端·kotlin·android jetpack