在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来实现的视频播放,它是一个不错的学习例子。