在Flutter加载图片与Glide一文中通过Glide
来实现了文件在磁盘中的缓存,但Flutter
加载图片、gif
、webp
等文件还是通过Image.file
来实现的,也就因此导致了一些问题。如下。
- 文件(图片、
gif
、webp
)的加载无法做到在Flutter
及Android
间的内存共享,从而增加内存消耗。 - 通过
Flutter
的Image
来加载gif
、动态webp
等动画时,存在内存抖动,尤其是在列表中加载这些动画时。
先来看问题一。根据闲鱼Flutter图片框架架构演进(超详细)一文,得知可以通过Texture
来实现内存在Android/iOS
与Flutter
间的复用,所以也就可以通过Texture
来解决问题一。
1、Texture的使用
在Flutter
中,Texture
的使用特别简单,基本上仅需要传递一个textureId
即可。代码实现如下。
Texture(
filterQuality: FilterQuality.none,
textureId: _textureId,
)
这里的textureId
是由Android/iOS
端通过PlatformChannel
传递过来的,至于Texture
的宽高是由父Widget
指定的。代码实现如下。
SizedBox(
width: _width,
height: _height,
child: Texture(
filterQuality: FilterQuality.none,
textureId: _textureId,
),
)
最后为了防止Texture
的自动拉伸导致图片变形,所以需要通过如下方式来使用Texture
。
Widget _getWidget() {
return Container(
width: widget.width,//widget的宽
height: widget.height,//widget的高
child: Center(
child: SizedBox(
width: _width,//image的真实宽
height: _height,//image的真实高
child: Texture(
filterQuality: FilterQuality.none,
textureId: _textureId,
),
),
),
);
}
}
2、Image的加载
在Android
端,本文还是以Glide
为例来加载图片,当然也可以把Glide
换成其他的图片加载库。
要想把图片通过Texture
在Flutter
中展示出来,需要经历如下三个步骤。
首先是SurfaceTextureEntry
对象的创建,该对象中的id
方法返回的就是Flutter
中Texture
所需要的textureId
。
private fun createSurfaceTextureEntry() {
if (surfaceTextureEntry == null) {
//缓存的engine对象
val engine = FlutterEngineCache.getInstance().get("xxx")
surfaceTextureEntry = engine!!.renderer.createSurfaceTexture()
}
}
其次就是通过Glide
来加载图片,想必该过程对于Android
开发者非常熟悉了。
private fun loadDrawable(url: String, size: DoubleArray?) {
if (size == null || size.size < 2) return
val width = size[0]
val height = size[1]
createSurfaceTextureEntry()
val requestManager = Glide.with(context)
var builder: RequestBuilder<Drawable>
builder = requestManager.load(url).override(width.toInt(), height.toInt())
//当调用dontAnimate后,gif仅会显示第一帧
if (!isGif(url)) {
builder = builder.dontAnimate()
}
loadFuture = builder.listener(object : RequestListener<Drawable> {
override fun onLoadFailed(e: GlideException?, model: Any?, target: Target<Drawable>?, isFirstResource: Boolean): Boolean {
mainHandler.post {
result?.error("", "", "")
//todo 图片加载失败还未处理
}
return false
}
override fun onResourceReady(resource: Drawable?, model: Any?, target: Target<Drawable>?, dataSource: DataSource?, isFirstResource: Boolean): Boolean {
Log.i(TAG, "url:" + model.toString() + "加载成果...")
// todo Glide加载gif还有优化的空间,例如采用giflib来实现解码,暂时先就用glide默认gif加载方式
if (resource is GifDrawable /*|| resource instanceof WebpDrawable*/) {
drawAnimatedDrawables(resource, calculateSize(resource.getIntrinsicWidth(), resource.getIntrinsicHeight(), width, height))
} else if (resource is BitmapDrawable) {//图片与静态webp的处理逻辑
drawDrawable(resource, calculateSize(resource.getIntrinsicWidth(), resource.getIntrinsicHeight(), width, height))
}
return false
}
}).submit()
}
最后就是将BitmapDrawable
展示在Flutter
中。
private fun drawDrawable(resource: Drawable, size: IntArray?) {
if (size == null || size.size < 2) return
if (surfaceTextureEntry != null) {
val jsonMap = HashMap<String, String>(4)
//Texture需要的textureId
jsonMap["id"] = surfaceTextureEntry!!.id().toString()
//图片的真实宽度
jsonMap["width"] = size[0].toString()
//图片的真实高度
jsonMap["height"] = size[1].toString()
val json = JSONObject(jsonMap as Map<String, String>).toString()
mainHandler.post { result?.success(json) }
val rect = Rect(0, 0, size[0], size[1])
val surfaceTexture = surfaceTextureEntry!!.surfaceTexture()
surfaceTexture.setDefaultBufferSize(size[0], size[1])
val surface = Surface(surfaceTexture)
resource.bounds = rect
val canvas = surface.lockCanvas(rect)
resource.draw(canvas)//图片的绘制
surface.unlockCanvasAndPost(canvas)
surface.release()
} else {
notImplemented()
}
}
经过上面三个步骤,就成功实现了图片与静态webp
加载时在Flutter
与Android
间的内存共享。
3、其他文件的支持
再来看对gif
的支持,它与图片的加载流程基本一致,但最终的处理流程稍有不同。
private fun drawAnimatedDrawables(resource: Drawable, size: IntArray?) {
if (size == null || size.size < 2) return
if (surfaceTextureEntry != null) {
val jsonMap = HashMap<String, String>(4)
//Texture需要的textureId
jsonMap["id"] = surfaceTextureEntry!!.id().toString() + ""
//图片的真实宽度
jsonMap["width"] = size[0].toString()
//图片的真实高度
jsonMap["height"] = size[1].toString()
val jsonObject = JSONObject(jsonMap as Map<String, String>)
val json = jsonObject.toString()
mainHandler.post { result?.success(json) }
val rect = Rect(0, 0, size[0], size[1])
val surfaceTexture = surfaceTextureEntry!!.surfaceTexture()
surfaceTexture.setDefaultBufferSize(size[0], size[1])
val surface = Surface(surfaceTexture)
resource.bounds = rect
if (drawableCallback == null) {
//由于Drawable中是一个弱引用来持有callback,所以必须得有一个强引用来持有callback,从而避免callback在使用时被gc回收
drawableCallback = DrawableCallback(surface, rect)
}
resource.callback = drawableCallback
startDrawableAnim(resource)
this.surface = surface
this.resource = resource
} else {
notImplemented()
}
}
由于gif
需要不停的绘制,所有这里利用了Drawable
的callback
属性,所以其具体绘制代码在DrawableCallback
中。
private inner class DrawableCallback internal constructor(private val surface: Surface?, private val rect: Rect) : Drawable.Callback {
@Volatile
private var isLock = false
// 将drawable绘制到canvas中
private fun draw(who: Drawable) {
if (who.callback == null) return
val canvas = surface!!.lockCanvas(rect)
// canvas.save();
//清除画布
canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR)
//绘制图像
who.draw(canvas)
// canvas.restore();
surface.unlockCanvasAndPost(canvas)
}
override fun invalidateDrawable(who: Drawable) {
if (who is GifDrawable) {
if (surface == null || !surface.isValid || !who.isRunning) return
}
//在profile或release下,如果不加isLock判断,下面代码可能会报错
if (isLock) return
isLock = true
draw(who)
isLock = false
}
override fun scheduleDrawable(who: Drawable, what: Runnable, `when`: Long) {
Log.i(TAG, "loadGif-scheduleDrawable")
}
override fun unscheduleDrawable(who: Drawable, what: Runnable) {
Log.i(TAG, "loadGif-unscheduleDrawable")
}
}
通过上面的代码,实现了gif
在Android
与Flutter
间的内存共享。
由于Glide
不支持动态webp
的加载,所以需要借助第三方库来实现,比如webpdecoder
。通过webpdecoder
加载动态webp
返回的是一个继承自GifDrawable
的WebpDrawable
。所以通过webpdecoder
来加载动态webp
的流程及代码实现与gif
一样。
4、总结
通过上面的内容,实现了图片、gif
及webp
加载时内存在Android
、Flutter
间的共享,也顺便解决了本文开始时所说的内存抖动问题。
由于将绘制交给Android
端,所以Flutter
的Texture
仅发挥展示的作用。这也导致了需要手动来控制生命周期,特别是加载gif
、动态webp
及在列表中使用上述方案时来加载图片、gif
、webp
时。比如在列表中,当gif
及动态webp
滑出可视区域后需要停止加载,当重新出现在可视区域时需要重新开始加载;当快速滑动时,未加载成功或还未下载成功时,需要停止;当退出Activity
时需要进行资源的释放等等。
如果是iOS
设备,也是可以通过Texture
来实现内存的共享。
Google提供的video_player就是使用Texture
来实现的视频播放,它是一个不错的学习例子。