使用MemoryStream代替byte[]作为资源文件添加的方式?

10

我有一个.NET程序集,其中添加了许多文件作为资源(每个文件大于500KB)。我之前一直使用自动生成的Resources类上的ResourceManager.GetObject()方法来访问这些资源,该方法返回一个byte[]

出于性能和语法原因,我更愿意将这些二进制资源作为流进行操作,而不是作为字节数组。我发现,通过手动编辑.resx文件,并将<value>元素中的类名从System.Byte[]更改为System.IO.MemoryStream,我可以成功地将ResourceManager.GetStream()方法用于访问资源流,例如:

<data name="MyFile" type="System.Resources.ResXFileRef, System.Windows.Forms">
    <value>..\Resources\MyFile.ext;System.Byte[], mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</data>

变成:

<data name="MyFile" type="System.Resources.ResXFileRef, System.Windows.Forms">
    <value>..\Resources\MyFile.ext;System.IO.MemoryStream, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</data>

这种方法唯一的缺点是Visual Studio始终会以byte[]形式添加新文件资源。有没有办法让它自动将类型设置为MemoryStream

1个回答

7

您可以在ResourceManager上创建扩展方法。

public static class ResourceExtensions
{
    public static MemoryStream GetMemoryStream(this ResourceManager resourceManager, String name) {
        object resource = resourceManager.GetObject(name);

        if (resource is byte[]) {
            return new MemoryStream((byte[])resource);
        }
        else {
            throw new System.InvalidCastException("The specified resource is not a binary resource.");
        }
    }
}

调用

ResourceManager resourceManager = Properties.Resources.ResourceManager;
MemoryStream stream = resourceManager.GetMemoryStream("binaryResource");

虽然如此,这似乎也没什么问题。
MemoryStream stream = new MemoryStream(Properties.Resources.SomeBinaryResource);

我不确定是否应该修改资源文件,因为它们容易改变,而且我确信在某些情况下,Visual Studio会覆盖更改。

根据您的关注点,问题在于这会将数据复制到内存中,从而创建内存占用。对于短暂的轻量级资源,这不是问题,但可能是一个很大的问题。

答案很简单:使用ResourceManager无法避免内存占用。问题在于ResourceManager.GetObject(String)ResourceManager.GetStream(String)都会创建数据副本。即使GetStream(String)返回一个UnmanagedMemoryStream,它实际上也会在内部调用GetObject(String),并且仍然会创建一个副本。如果您调试应用程序并分析内存,则会看到仍然分配了内存。

我尝试使用指针在unsafe上下文中和反射等多种方法来解决此问题,但都没有成功。 ResourceManager并不是那么灵活或优化。

然而,我找到了一种解决方案,但需要您使用嵌入式资源。这不会改变任何内容,除了将资源文件的构建操作设置为嵌入式资源。通过使用这种方法,您可以使用反射创建一个不创建数据副本的UnmanagedMemoryStream

private UnmanagedMemoryStream GetUnmanagedMemoryStream(String embeddedResourceName) {
    Assembly assembly = Assembly.GetExecutingAssembly();

    string[] resourceNames = assembly.GetManifestResourceNames();
    string resourceName = resourceNames.SingleOrDefault(resource => resource.EndsWith(embeddedResourceName, StringComparison.InvariantCultureIgnoreCase));

    if (resourceName != null) {
        return (UnmanagedMemoryStream)assembly.GetManifestResourceStream(resourceName);
    }
    else {
        throw new System.ArgumentException("The specified embedded resource could not be found.", "embeddedResourceName");
    }
}

我没有进行过广泛测试,但是它确实可以工作。我的测试数据是一个小17兆字节的文件。我的测试应用程序的工作集内存从大约50兆字节开始,在将资源检索到流中后,不会改变。当使用 ResourceManager时,它会立即增加工作集的大小达到资源的大小。

您可能需要替换调用 EndsWith 的名称检查,以检查清单中正确的资源名称,因为资源的名称与直接通过 ResourceManager 访问它略有不同。

我实际上很失望,无法使用现有的 ResourceManager 找到解决方案,因为它不够灵活。

编辑 我在这里撰写了一篇深入的博客文章,介绍了这个主题。


3
我担心使用你的方法时,在构建和返回流之前整个byte[]将被读入内存。这就是我希望能够调用GetStream而不是GetObject的原因;以避免先将整个文件读入内存。 - Bradley Smith
1
我以为我做到了,但还需要再努力一点。我会在有可用的东西时及时更新给你。 - David Anderson
1
更新了我的回答。简单来说,你不能通过ResourceManager避免占位符。 - David Anderson
1
我很感激你在探索问题和测试解决方案方面所付出的努力。考虑到这一点,我可能会转而使用嵌入式资源。 - Bradley Smith
@DavidAnderson 我认为你现在可以使用UnmanagedMemoryStream来防止二次字节分配,只要你能够在内存中指向源(ReadOnlyMemory<byte>),假设你能找到它。例如。)https://github.com/houseofcat/Tesseract/blob/6a3da2615b38b8e5526f5ccde568179df5a8ab9b/src/HouseofCat.Compression/GzipProvider.cs#L89以上将允许一次且仅一次的分配,直到开发人员在我的示例中使用ArraySegment<byte>.ToArray()调用,但可以返回流而不是数组。因此,唯一的额外内存是原始资源加载。九年后哈哈。 - HouseCat
显示剩余2条评论

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