目前我们基于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. 网络/本地图片:把专业的事交给专业的工具
对于http
、assets
或本地文件,适配器把任务甩给了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