Flutter混编工程之打通纹理之路

2022-12-12 11:46:14 浏览数 (2)

Flutter的图片系统基于Image的一套架构,但是这东西的性能,实在不敢恭维,感觉还停留在Native开发至少5年前的水平,虽然使用上非常简单,一个Image.network走天下,但是不管是解码性能还是加载速度,抑或是内存占用和缓存逻辑,都远远不如Native的图片库,特别是Glide。虽然Google一直在有计划优化Flutter Image的性能,但现阶段,体验最佳的图片加载方式,还是通过插件,使用Glide来进行加载。

所以,在混编的大环境下,将Flutter的图片加载功能托管给原生,是最合理且性能最佳的方案。

那么对于桥接到原生的方案来说,主要有两个方向,一个是通过Channel来传递加载的图像的二进制数据流,然后在Flutter内解析二进制流后来解析图像,另一个则是通过外接纹理的方式,来共享图像内存,显然,第二种方案是更好的解决方案,不管从内存消耗还是传输性能上来说,外接纹理的方案,都是Flutter桥接Native图片架构的最佳选择。

虽然说外接纹理方案比较好,但是网络上对于这个方案的研究却不是很多,比较典型的是Flutter官方Plugins中的视频渲染的方案,地址如下所示。

https://github.com/flutter/plugins/tree/main/packages/video_player

这是我们研究外接纹理的第一手方案,除此之外,闲鱼开源的PowerImage,也是基于外接纹理的方案来实现的,同时他们也给出了基于外接纹理的一系列方案的预研和技术基础研究,这些也算是我们了解外接纹理的最佳途径,但是,基于阿里的一贯风格,我们不太敢直接大范围使用PowerImage,研究研究外接纹理,来实现一套自己的方案,其实是最好的。

https://www.infoq.cn/article/MLMK2bx8uaNb5xJm13SW

https://juejin.cn/post/6844903662548942855

外接纹理的基本概念

其实上面两篇闲鱼的文章,已经把外接纹理的概念讲解的比较清楚了,下面我们就简单的总结一下。

首先,Flutter的渲染机制与Native渲染完全隔离,这样的好处是Flutter可以完全控制Flutter页面的绘制和渲染,但坏处是,Flutter在获取一些Native的高内存数据时,通过Channel来进行传递就会导致浪费和性能压力,所以Flutter提供了外接纹理,来处理这种场景。

在Flutter中,系统提供了一个特殊的Widget——Texture Widget。Texture在Flutter的Widget Tree中是一个特殊的Layer,它不参与其它Layer的绘制,它的数据全部由Native提供,Native会将动态渲染数据,例如图片、视频等数据,写入到PixelBuffer,而Flutter Engine会从GPU中拿到相应的渲染数据,并渲染到对应的Texture中。

Texture实战

Texture方案来加载图片的过程实际上是比较长的,涉及到Flutter和Native的双端合作,所以,我们需要创建一个Flutter Plugin来完成这个功能的调用。

我们创建一个Flutter Plugin,Android Studio会自动帮我们生成对应的插件代码和Example代码。

整体流程

Flutter和Native之间,通过外接纹理的方式来共享内存数据,它们之间相互关联的纽带,就是一个TextureID,通过这个ID,我们可以分别关联到Native侧的内存数据,也可以关联到Flutter侧的Texture Widget,所以,一切的故事,都是从TextureID开始的。

Flutter加载图片的起点,从Texture Widget开始,Widget初始化的时候,会通过Channel请求Native,创建一个新的TextureID,并将这个TextureID返回给Flutter,将当前Texture Widget与这个ID进行绑定。

接下来,Flutter侧将要加载的图片Url通过Channel请求Native,Native侧通过TextureID找到对应的Texture,并在Native侧通过Glide,用传递的Url进行图片加载,将图片资源写入Texture,这个时候,Flutter侧的Texture Widget就可以实时获取到渲染信息了。

最后,在Flutter侧的Texture Widget回收时,需要对当前的Texture进行回收,从而将这部分内存释放。

以上就是整个外接纹理方案的实现过程。

Flutter侧

首先,我们需要创建一个Channel来注册上面提到的几个方法调用。

代码语言:javascript复制
class MethodChannelTextureImage extends TextureImagePlatform {
  @visibleForTesting
  final methodChannel = const MethodChannel('texture_image');

  @override
  Future<int?> initTextureID() async {
    final result = await methodChannel.invokeMethod('initTextureID');
    return result['textureID'];
  }

  @override
  Future<Size> loadByTextureID(String url, int textureID) async {
    var params = {};
    params["textureID"] = textureID;
    params["url"] = url;
    final size = await methodChannel.invokeMethod('load', params);
    return Size(size['width']?.toDouble() ?? 0, size['height']?.toDouble() ?? 0);
  }

  @override
  Future<int?> disposeTextureID(int textureID) async {
    var params = {};
    params["textureID"] = textureID;
    final result = await methodChannel.invokeMethod('disposeTextureID', params);
    return result['textureID'];
  }
}

接下来,回到Flutter Widget中,封装一个Widget用来管理Texture。

在这个封装的Widget里面,你可以对尺寸作调整,或者是对生命周期进行管理,但核心只有一个,那就是创建一个Texture。

代码语言:javascript复制
Texture(textureId: _textureID),

使用前面创建的Channel,来完成流程的加载。

代码语言:javascript复制
@override
void initState() {
  initTextureID().then((value) {
    _textureID = value;
    _textureImagePlugin.loadByTextureID(widget.url, _textureID).then((value) {
      if (mounted) {
        setState(() => bitmapSize = value);
      }
    });
  });
  super.initState();
}

Future<int> initTextureID() async {
  int textureID;
  try {
    textureID = await _textureImagePlugin.initTextureID() ?? -1;
  } on PlatformException {
    textureID = -1;
  }
  return textureID;
}

@override
void dispose() {
  if (_textureID != -1) {
    _textureImagePlugin.disposeTextureID(_textureID);
  }
  super.dispose();
}

这样整个Flutter侧的流程就完成了——创建TextureID——>绑定TextureID和Url——>回收TextureID。

Native侧

Native侧的处理都集中在Plugin的注册类中,在注册时,我们需要创建TextureRegistry,这是系统提供给我们使用外接纹理的入口。

代码语言:javascript复制
override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
    channel = MethodChannel(flutterPluginBinding.binaryMessenger, "texture_image")
    channel.setMethodCallHandler(this)
    context = flutterPluginBinding.applicationContext
    textureRegistry = flutterPluginBinding.textureRegistry
}

接下来,我们需要对Channel进行处理,分别实现前面提到的三个方法。

代码语言:javascript复制
"initTextureID" -> {
    val surfaceTextureEntry = textureRegistry?.createSurfaceTexture()
    val textureId = surfaceTextureEntry?.id() ?: -1
    val reply: MutableMap<String, Long> = HashMap()
    reply["textureID"] = textureId
    textureSurfaces[textureId] = surfaceTextureEntry
    result.success(reply)
}

initTextureID方法,核心功能就是从TextureRegistry中创建一个surfaceTextureEntry,textureId就是它的id属性。

代码语言:javascript复制
"load" -> {
    val textureId: Int = call.argument("textureID") ?: -1
    val url: String = call.argument("url") ?: ""
    if (textureId >= 0 && url.isNotBlank()) {
        Glide.with(context).load(url).skipMemoryCache(true).into(object : CustomTarget<Drawable>() {
            override fun onResourceReady(resource: Drawable, transition: Transition<in Drawable>?) {
                if (resource is BitmapDrawable) {
                    val bitmap = resource.bitmap
                    val imageWidth: Int = bitmap.width
                    val imageHeight: Int = bitmap.height
                    val surfaceTextureEntry: SurfaceTextureEntry = textureSurfaces[textureId.toLong()]!!
                    surfaceTextureEntry.surfaceTexture().setDefaultBufferSize(imageWidth, imageHeight)
                    val surface =
                        if (surfaceMap.containsKey(textureId.toLong())) {
                            surfaceMap[textureId.toLong()]
                        } else {
                            val surface = Surface(surfaceTextureEntry.surfaceTexture())
                            surfaceMap[textureId.toLong()] = surface
                            surface
                        }
                    val canvas: Canvas = surface!!.lockCanvas(null)
                    canvas.drawBitmap(bitmap, 0F, 0F, null)
                    surface.unlockCanvasAndPost(canvas)
                    val reply: MutableMap<String, Int> = HashMap()
                    reply["width"] = bitmap.width
                    reply["height"] = bitmap.height
                    result.success(reply)
                }
            }

            override fun onLoadCleared(placeholder: Drawable?) {
            }
        })
    }
}

load方法,就是我们熟悉的Glide了,通过Glide来获取对应Url的图片数据,再通过SurfaceTextureEntry,来创建Surface对象,并将Glide返回的数据,写入到Surface中,最后,将图像的宽高回传给Flutter,做后续的一些处理。

代码语言:javascript复制
"disposeTextureID" -> {
    val textureId: Int = call.argument("textureID") ?: -1
    val textureIdLong = textureId.toLong()
    if (surfaceMap.containsKey(textureIdLong) && textureSurfaces.containsKey(textureIdLong)) {
        val surfaceTextureEntry: SurfaceTextureEntry? = textureSurfaces[textureIdLong]
        val surface = surfaceMap[textureIdLong]
        surfaceTextureEntry?.release()
        surface?.release()
        textureSurfaces.remove(textureIdLong)
        surfaceMap.remove(textureIdLong)
    }
}

disposeTextureID方法,就是对dispose的Texture进行回收,否则的话,Texture一直在申请新的内存,就会导致Native内存一直上涨而不会被回收,所以,在Flutter侧调用dispose后,我们需要对相应TextureID对应的资源进行回收。

以上,我们就完成了Native的处理,通过和Flutter侧配合,借助Glide的高效加载能力,我们就完成就一次完美的图片加载过程。

总结

通过外接纹理来加载图片,我们可以有下面这些优点。

  • 复用Native的高效、稳定的图片加载机制,包括缓存、编解码、性能等
  • 降低多套方案的内存消耗,降低App的运行内存
  • 打通Native和Flutter,图片资源可以进行内存共享

但是,当前这个方案也并不是「完美的」,只能说,上面的方案是一个「可用」的方案,但还远远没有达到「好用」的级别,为了更好的实现外接纹理的方案,我们还需要处理一些细节。

  • 复用、复用,还是TMD复用,对于同Url的图片、加载过的图片,在Native端和Flutter端,都应该再做一套缓存机制
  • 对于Gif和Webp的支持,目前为止,我们都是处理的静态图片,还未添加动态内容的处理,当然这一定是可以的,只不过我们还没支持
  • Channel的Batch调用,对于一个列表来说,可能一帧中会同时产生大量的图片请求,虽然现在Channel的性能有了很大的提升,但是如果能对Channel的调用做一个缓冲区,那么对于特别频繁的调用来说,会优化一部分Channel的性能

所以这只是第一篇,后面我们会继续针对上面的问题进行优化,请各位拭目以待。

0 人点赞