前不久闲鱼团队的公众号发了一篇文章讲了闲鱼团队在Flutter图片框架的演进过程文章,里面讲到了使用外接纹理的方式来实现图片下载功能:闲鱼Flutter图片框架架构演进(超详细),本文的用意就是动手实现闲鱼的这个外接纹理图片下载功能。
在刚学Flutter的时候我们的图片下载功能一般都是直接使用Flutter官方提供的api来加载网络图片,如:
Image(
image: NetworkImage("https://www.xxx.com/xx.jpg")
)
这样子已经可以实现了界面需要展示的图片了,可是为什么闲鱼团队不这么写还要"折腾"什么图片下载框架呢?在闲鱼的文章中抛出的三点其实就可以解释了为什么还要对Flutter的图片下载功能进行进一步封装实现:
特别是在混合Flutter开发的时候,很多时候都在想如何才能更高效的复用原生已加载的图片显得特别头疼。在看了闲鱼的文章之后外接纹理的思路确实让我眼前一亮,似乎找到了点复用的头绪。
什么是外接纹理?
文章中一直在讲的外接纹理是个什么概念?
其实外接纹理在代码上叫做Texture
,这个类在Flutter中的代码量非常少一共没几行:
class Texture extends LeafRenderObjectWidget {
/// Creates a widget backed by the texture identified by [textureId].
const Texture({
Key key,
@required this.textureId,
}) : assert(textureId != null),
super(key: key);
/// The identity of the backend texture.
final int textureId;
@override
TextureBox createRenderObject(BuildContext context) => TextureBox(textureId: textureId);
@override
void updateRenderObject(BuildContext context, TextureBox renderObject) {
renderObject.textureId = textureId;
}
}
虽然代码少,但是功能确实强大,整个Flutter渲染流程中需要的东西都提供了。在我看来Texture
跟原生的结合的关键就是textureId
,大致的理解就是Flutter端的TextTure
和原生端的Surface
两者通过textureId
完成相互绑定,从而达到原生绘制给Flutter端显示效果。
如何实现这一过程?
因为在Flutter端的
Texture
中需要传入textureId
从而达到跟原生Surface
绑定,所以第一步就是需要生成textureId
,这里原生主要采用自定义MethodChannel
的方式:
TextureRegistry textureRegistry = registrar.textures();
TextureRegistry.SurfaceTextureEntry surfaceTextureEntry = textureRegistry.createSurfaceTexture();
long textureId = surfaceTextureEntry.id();
Map<String, Object> reply = new HashMap<>();
reply.put("textureId", textureId);
textureSurfaces.put(String.valueOf(textureId), surfaceTextureEntry);
result.success(reply);
这里的关键就是通过Flutter提供的SurfaceTextureEntry
来获取值,并通过Channel的方式传递给Flutter端,然后在Flutter端进行调用从而得到这个textureId
的值:
init() async {
var response = await _channel.invokeMethod("load");
_textureId = response["textureId"];
}
当我们得到了需要的textureId
之后就可以初始化一个Texture
对象了:
Widget build(BuildContext context) {
return Texture(textureId: _textureId);
}
上面完成了第一步,接下来就是如何复用原生的图片下载功能了从而将得到图片传给Flutter端显示。
在Android原生开发中基本上大家都在使用Fresco
或者Glide
来加载图片(当然有自家的图片库),不过最终的目的都是得到图片,然后通过textureId
传给Flutter端。我这里直接展示图片下载成功后的回调,代码实现:
int textureId = call.argument("textureId");
final String url = call.argument("url");
int imageWidth = bitmap.getWidth();
int imageHeight = bitmap.getHeight();
TextureRegistry.SurfaceTextureEntry surfaceTextureEntry = textureSurfaces.get(String.valueOf(textureId));
Rect rect = new Rect(0, 0, 200, 200);
surfaceTextureEntry.surfaceTexture().setDefaultBufferSize(imageWidth, imageHeight);
Surface surface = new Surface(surfaceTextureEntry.surfaceTexture());
Canvas canvas = surface.lockCanvas(rect);
canvas.drawBitmap(bitmap, null, rect, null);
bitmap.recycle();
surface.unlockCanvasAndPost(canvas);
result.success(0);
这里的原生代码最关键的就是获取Surface
,有了它就可以获取到Canvas
自然就可以画出想要的效果,Flutter端就可以显示了。
而Flutter端调用的时候传入相关的textureId
以及图片地址给原生,如:
var params = Map();
params["textureId"] = _textureId;
params["url"] = url;
result = await _channel.invokeMethod("start", params);
value = result;
整个demo差不多就这样子结束了,运行起来看到的效果如下:
这样子实现有什么好处?
在完成了图片下载功能后,自然要跟Flutter自带的
Image
方式进行比较。首先在滑动的过程中两者实现方式都差不多,闲鱼的文章中对比了下内存优化了不少,我这里也对比下内存:自带的
Image
的内存表现:采用
Texture
的内存表现:Flutter自带的
Image
加载图片的时候在AS中查看Graphics的内存表现会飙升,我这里一共加载20张图片滑动到底部后相比确实差了不少。由于两种实现方式的图片存在于内存的位置不同,如果从总得内存占有量来讲Texture
肯定表现得更加优秀点。
但是,当你在混合开发中一张图片已经加载完成原生会直接复用,可能是从内存读取也有可能是从sdcard读取,不仅加载速度快而且也能为用户省不少流量,闲鱼文章也提到采用外接纹理的方式确实能有效的复用原生图片,总得来讲可以有效的解决了闲鱼文章开头的三个问题。