如何在Flutter中渲染单个像素?

13

设置

我正在使用自定义RenderBox进行绘制。
下面代码中的canvas对象来自于paint方法中的PaintingContext

绘图

我正在尝试通过使用Canvas.drawRect逐个渲染像素。
需要指出的是,这些像素有时比它们实际占据的屏幕像素大,有时则更小。

for (int i = 0; i < width * height; i++) {
  // in this case the rect size is 1
  canvas.drawRect(
      Rect.fromLTWH(index % (width * height), 
          (index / (width * height)).floor(), 1, 1), Paint()..color = colors[i]);
}

存储

我将像素存储为 List<List<Color>>(上面的代码中为colors)。我之前尝试过不同嵌套的列表,但在性能方面没有引起任何明显的差异。 我的Android模拟器测试设备的内存在用999x999的图像填充列表时增加了282.7MB。请注意,它只是暂时增加了282.7MB。大约半分钟后,增加会降至153.6MB并保持不变(没有任何用户交互)。

渲染

使用999x999的分辨率,上面的代码会导致GPU最大值250.1 ms/frameUI最大值1835.9 ms/frame,这显然是不可接受的。尝试绘制999x999的图像时,UI会冻结两秒钟,这应该很容易(我猜想),因为4k视频在同一设备上运行顺畅。

CPU

我不确定如何使用Android Profiler正确跟踪此问题,但在填充或更改列表时,即绘制像素时(上面的指标也是如此),CPU使用率从0%增加到最高60%。以下是AVD性能设置:

原因

我不知道从哪里开始,因为我甚至不确定我的代码的哪个部分导致了冻结。是内存使用还是绘图本身?
一般来说,我该怎么做?我做错了什么?我应该如何存储这些像素。

努力

我尝试了很多方法都没有帮助,我只想指出最显著的几个:

我尝试将 List<List<Color>> 转换为 dart:ui 库中的 Image,希望使用 Canvas.drawImage。为了做到这一点,我尝试编码自己的 PNG,但是我无法渲染超过一行。然而,它看起来并没有提高性能。当尝试转换一个 9999x9999 的图像时,我遇到了内存不足异常。现在,我想知道视频是如何呈现的,因为任何 4k 视频都会比一个 9999x9999 的图像占用更多的内存,如果其中几秒钟在内存中。
我尝试实现 image 包。然而,在完成之前我停止了,因为我注意到它不是用于 Flutter 而是用于 HTML。我不会从那里获得任何东西。
这对于我将要得出的结论非常重要:我尝试 仅仅绘制而不存储像素,即使用 Random.nextInt 生成随机颜色。当尝试随机生成一个 999x999 的图像时,这导致了 GPU 最大值1824.7 ms/framesUI 最大值2362.7 ms/frame,这甚至更糟,特别是在 GPU 部分。

结论

在尝试使用Canvas.drawImage进行渲染之前,我得出的结论是:Canvas.drawRect不适用于此任务,因为它甚至无法绘制简单的图像。

在Flutter中如何实现这一点?

注释

  • 这基本上就是我两个月前尝试询问的内容(是的,我已经尝试了那么长时间来解决这个问题),但我认为当时我没有正确表达自己,并且对实际问题的了解更少。

  • 我可以正确呈现的最高分辨率约为10k像素。我需要至少1m

  • 我正在考虑放弃Flutter并转向原生可能是我的唯一选择。然而,我希望相信我只是完全错误地处理这个问题。我已经花了大约三个月的时间来尝试解决这个问题,但我没有找到任何有用的信息。


instantiateImageCodec 表示支持 BMP 格式,而从原始 ARGB 数据进行编码相对较容易 - 尽管我不知道这样的 ARGB->BMP 编码和 BMP->Image 解码有多快... - pskink
还有:你的“Layer”树有多大? - pskink
[+9 ms] 要在运行时热重载更改,请按“r”。要进行热重启(并重建状态),请按“R”。 [+2ms] 在SM T110上可用的 Observatory 调试器和分析器位于:http://127.0.0.1:41854/。 [ ] 要获取更详细的帮助信息,请按“h”。要分离,请按“d”;要退出,请按“q”。 [+129309 ms] 正在初始化热重载... - pskink
1个回答

16

解决方案

dart:ui提供了一个函数,可以轻松地将像素转换为ImagedecodeImageFromPixels

我在创建这个答案时并不知道这一点,这就是为什么我写了“替代方案”部分的原因。

替代方案

感谢@pslink提醒我,在我写完自己的PNG之后,我失败了。
我之前研究过它,但我认为它看起来太复杂了,没有足够的文档说明。现在,我找到了这篇好文章,解释了必要的BMP头,并通过复制来自“BMP文件格式”维基百科文章中的示例2,实现了32位BGRA(ARGB但BGRA是默认掩码的顺序)。我查阅了所有资料,但没有找到这个示例的原始来源。也许维基百科文章的作者自己写的。

结果

使用Canvas.drawImage和我的通过BMP字节列表转换而来的999x999像素的图像,我得到了GPU max9.9毫秒/帧UI max7.1毫秒/帧非常棒

|  ms/frame |  Before (Canvas.drawRect) | After (Canvas.drawImage) |
|-----------|---------------------------|--------------------------|
|  GPU max  | 1824.7                    | 9.9                      |
|  UI max   | 2362.7                    | 7.1                      |

结论

Canvas操作(例如Canvas.drawRect)不应该被这样使用。

说明

首先,这很简单,但是你需要正确填充字节列表,否则你会收到一个错误,指出你的数据格式不正确,并且看不到结果,这可能非常令人沮丧。

在绘图之前,你需要准备好你的图片因为你不能在paint调用中使用async操作。

在代码中,你需要使用Codec将你的字节列表转换成图像。

final list = [
            0x42, 0x4d, // 'B', 'M'
            ...];
// make sure that you either know the file size, data size and data offset beforehand
//        or that you edit these bytes afterwards

final Uint8List bytes = Uint8List.fromList(list);

final Codec codec = await instantiateImageCodec(bytes));

final Image image = (await codec.getNextFrame()).image;

您需要将此图像传递给您的绘图小部件,例如使用FutureBuilder
现在,您只需在绘制调用中使用Canvas.drawImage即可。


啊,我以前用过很多次,但没有看到任何数字(只有图表)-现在我也看到了数字,非常感谢。顺便说一句,如果你想获得最大的可用性能,你可以看一下examples/layers/raw/spinning_square.dart或该文件夹中的任何其他源代码。 - pskink

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接