Flutter的图片体系根据Image的一套架构,可是这东西的功用,实在不敢恭维,感觉还停留在Native开发至少5年前的水平,尽管运用上非常简单,一个Image.network走全国,可是不管是解码功用仍是加载速度,抑或是内存占用和缓存逻辑,都远远不如Native的图片库,特别是Glide。尽管Google一向在有计划优化Flutter Image的功用,但现阶段,体验最佳的图片加载办法,仍是经过插件,运用Glide来进行加载。

所以,在混编的大环境下,将Flutter的图片加载功用保管给原生,是最合理且功用最佳的计划。

那么关于桥接到原生的计划来说,主要有两个方向,一个是经过Channel来传递加载的图画的二进制数据流,然后在Flutter内解析二进制流后来解析图画,另一个则是经过外接纹路的办法,来共享图画内存,明显,第二种计划是更好的解决计划,不管从内存耗费仍是传输功用上来说,外接纹路的计划,都是Flutter桥接Native图片架构的最佳选择。

尽管说外接纹路计划比较好,可是网络上关于这个计划的研讨却不是很多,比较典型的是Flutter官方Plugins中的视频烘托的计划,地址如下所示。

github.com/flutter/plu…

这是咱们研讨外接纹路的第一手计划,除此之外,闲鱼开源的PowerImage,也是根据外接纹路的计划来完成的,一起他们也给出了根据外接纹路的一系列计划的预研和技术基础研讨,这些也算是咱们了解外接纹路的最佳途径,可是,根据阿里的一贯风格,咱们不太敢直接大范围运用PowerImage,研讨研讨外接纹路,来完成一套自己的计划,其实是最好的。

www.infoq.cn/article/MLM…

/post/684490…

外接纹路的基本概念

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

首先,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来注册上面说到的几个办法调用。

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。

Texture(textureId: _textureID),

运用前面创立的Channel,来完结流程的加载。

@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,这是体系提供给咱们运用外接纹路的入口。

override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
    channel = MethodChannel(flutterPluginBinding.binaryMessenger, "texture_image")
    channel.setMethodCallHandler(this)
    context = flutterPluginBinding.applicationContext
    textureRegistry = flutterPluginBinding.textureRegistry
}

接下来,咱们需要对Channel进行处理,分别完成前面说到的三个办法。

"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属性。

"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,做后续的一些处理。

"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的功用

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