【Stream—5】MemoryStream相关知识分享
一、简单介绍一下MemoryStream
MemoryStream是内存流,为系统内存提供读写操作,由于MemoryStream是通过无符号字节数组组成的,可以说MemoryStream的性能可以算比较出色,所以它担当起了一些其他流进行数据交互安时的中间工作,同时可降低应用程序中对临时缓冲区和临时文件的需求,其实MemoryStream的重要性不亚于FileStream,在很多场合,我们必须使用它来提高性能
二、MemoryStream和FileStream的区别
前文中也提到了,FileStream主要对文件的一系列操作,属于比较高层的操作,但是MemoryStream却很不一样,他更趋向于底层内存的操作,这样能够达到更快速度和性能,也是他们的根本区别,很多时候,操作文件都需要MemoryStream来实际进行读写,最后放入相应的FileStream中,不仅如此,在诸如XmlWriter的操作中也需要使用MemoryStream提高读写速度
三、分析MemoryStream最常见的OutOfMemory异常
先看一下下面一段简单的代码
1 //测试byte数组 假设该数组容量是256M
2 var testBytes = new byte[256 * 1024 * 1024];
3 var ms = new MemoryStream();
4 using (ms)
5 {
6 for (int i = 0; i < 1000; i++)
7 {
8 try
9 {
10 ms.Write(testBytes, 0, testBytes.Length);
11 }
12 catch
13 {
14 Console.WriteLine("该内存流已经使用了{0}M容量的内存,该内存流最大容量为{1}M,溢出时容量为{2}M",
15 GC.GetTotalMemory(false) / (1024 * 1024),//MemoryStream已经消耗内存量
16 ms.Capacity / (1024 * 1024), //MemoryStream最大的可用容量
17 ms.Length / (1024 * 1024));//MemoryStream当前流的长度(容量)
18 break;
19 }
20 }
21 }
22 Console.ReadLine();
输出结果:
从输出结果来看,MemoryStream默认可用最大容量是1024M,发生异常时正好是其最大容量。
问题来了,假设我们需要操作比较大的文件,该怎么办呢?其实有2种方法可以搞定,一种是分段处理,我们将Byte数组分成等份进行处理,还有一种方式便是增加MomoryStream的最大可用容量(字节),我们可以在声明MomoryStream的构造函数时利用它的重载版本:MemoryStream(int capacity)
到底使用哪种方法比较好呢?其实笔者认为具体项目具体分析,前者分段处理的确能够解决大数据量操作的问题,但是牺牲了性能和时间(多线程暂时不考虑),后者可以得到性能上的优势,但是其允许最大容量是 int.Max,所以无法给出一个明确的答案,大家在做项目时,按照需求自己定制即可,最关键的还是要取到性能和开销的最佳点位,还有一种更恶心的溢出方式,往往会让大家抓狂,就是不定时溢出,就是MemoryStream处理的文件可能只有40M或更小时,也会发生OutOfMemory的异常,关于这个问题,终于在老外的一篇文章中得到了解释,运气还不错,可以看看这篇博文:一种MemoryStream的替代方案,由于涉及到windows的内存机制,包括内存也,进程的虚拟地址空间等,比较复杂,所以大家看他的文章前,我先和大家简单的介绍一下页和进程的虚拟地址:
内存页:内存页分为:文件页和计算页
内存中的文件页是文件缓存区,即文件型的内存页,用于存放文件数据的内存页(也称永久页),作用在于读写文件时可以减少对磁盘的访问,如果它的大小设置的太小,会引起系统频繁的访问磁盘,增加磁盘I/O,设置太大,会浪费内存资源。
内存中的计算页也称为计算型的内存页,主要用于存放程序代码和临时使用的数据。
进程的虚拟地址:每一个进程被给予它的非常自由的虚拟地址空间。对于32位的进程,地址空间是4G,因为一个32位指针能够从0x00000000到0xffffffff之间的任意值,这个范围允许指针从4294967296个值得一个,覆盖了一个进程得4G范围,对于64位进程,地址空间是16eb因为一个64位指针能够指向18,446,744,073,709,551,616个值中的一个,覆盖一个进程的16eb范围,这是十分宽广的范围,上述概念都在自windows核心编程这本书,其实这本书对于我们程序员来说很重要,对于内存的操作,本人也是小白。
四、MemoryStream的构造函数
1、MemoryStream()
MemoryStream允许不带参数的构造
2、MemoryStream(byte[] byte)
Byte数组是包含了一定数据的byte数组,这个构造很重要,初学者或者用的不是很多的程序员会忽略这个构造函数导致后面读取或写入数据时发现MemoryStream中没有byte数据,会导致很郁闷的感觉,大家注意一下就行,有时候也可能无需这样,因为很多方法返回值已经是MemoryStream了。
3、MemoryStream(int capacity)
这个是重中之重,为什么这么说呢?我在本文探讨关于OutMemory异常中也提到了,如果你想额外提高MemoryStream的吞吐量,也只能靠这个方法提升一定的吞吐量,最多也只能到int.Max,这个方法也是解决OutOfMemory的一个可行方案。
4、MemoryStream(byte[] byte,bool writeable)
writeable参数定义该流是否可写
5、MemoryStream(byte[] byte,int index,int count)
index:参数定义从byte数组中的索引index
count:参数是获取的数据量的个数
6、MemoryStream(byte[] byte,int index,int count,bool writeable,bool publiclyVisible)
publiclyVisible:参数表示true可以启用GetBuffer方法,它返回无符号字节数组,流从该数组创建,否则为false。(大家一定觉得这个很难理解,别急,下面的方法中我会详细的讲一下这个东西)
五、MemoryStream的属性
Memory的属性大致都是和其父类很相似,这些功能在我的这篇文章中已经详细讨论过,所以我简单列举以下其属性:
其独有的属性:
Capacity:这个前文其实已经提及,它表示该流的可支配容量(字节),非常重要的一个属性。
六、MemoryStream的方法
对于重写的方法,这里不再重复说明,大家可以去关于Stream的知识分享看一下
以下是MemoryStream独有的方法
1、virtual byte[] GetBuffer()
这个方法使用时需要小心,因为这个方法返回无符号字节数组,也就是说,即使我只输入几个字符例如“HellowWorld”我们只希望返回11个数据就行,可是这个方法会把整个缓冲区的数据,包括那些已经分配但是实际上没有用到的字符数据都返回回来了,如果想启用这个方法那必须使用上面最后一个构造函数,将publiclyVisible属性设置成true就行,这也是上面那个构造函数的错用所在。
2、virtual void WriteTo(Stream stream)
这个方法的目的其实在本文开始的时候讨论性能问题时已经指出,MemoryStream常用起中间流的作用,所以读写在处理完后将内存吸入其他流中。
七、示例:
1、XmlWriter中使用MemoryStream
1 public static void UseMemoryStreamInXmlWriter()
2 {
3 var ms = new MemoryStream();
4 using (ms)
5 {
6 //定义一个XmlWriter
7 using (XmlWriter writer= XmlWriter.Create(ms))
8 {
9 //写入xml头
10 writer.WriteStartDocument(true);
11 //写入一个元素
12 writer.WriteStartElement("Content");
13 //为这个元素增加一个test属性
14 writer.WriteStartAttribute("test");
15 //设置test属性的值
16 writer.WriteValue("萌萌小魔王");
17 //释放缓冲,这里可以不用释放,但是在实际项目中可能要考虑部分释放对性能带来的提升
18 writer.Flush();
19 Console.WriteLine($"此时内存使用量为:{GC.GetTotalMemory(false)/1024}KB,该MemoryStream已使用容量为:{Math.Round((double)ms.Length,4)}byte,默认容量为:{ms.Capacity}byte");
20 Console.WriteLine($"重新定位前MemoryStream所在的位置是{ms.Position}");
21 //将流中所在当前位置往后移动7位,相当于空格
22 ms.Seek(7, SeekOrigin.Current);
23 Console.WriteLine($"重新定位后MemoryStream所存在的位置是{ms.Position}");
24 //如果将流所在的位置设置位如下示的位置,则XML文件会被打乱
25 //ms.Position = 0;
26 writer.WriteStartElement("Content2");
27 writer.WriteStartAttribute("testInner");
28 writer.WriteValue("萌萌小魔王2");
29 writer.WriteEndElement();
30 writer.WriteEndElement();
31 //再次释放
32 writer.Flush();
33 Console.WriteLine($"此时内存使用量为:{GC.GetTotalMemory(false) / 1024}KB,该MemoryStream已使用容量为:{Math.Round((double)ms.Length, 4)}byte,默认容量为:{ms.Capacity}byte");
34 //建立一个FileStream 文件创建目的地是f:\test.xml
35 var fs=new FileStream(@"f:\test.xml",FileMode.OpenOrCreate);
36 using (fs)
37 {
38 //将内存流注入FileStream
39 ms.WriteTo(fs);
40 if (ms.CanWrite)
41 {
42 //释放缓冲区
43 fs.Flush();
44 }
45 }
46 Console.WriteLine();
47 }
48 }
49 }
运行结果:
咱看一下XML文本是什么样的?
2、自定义处理图片的HttpHandler
有时项目里我们必须将图片进行一定的操作,例如:水印,下载等,为了方便和管理我们可以自定义一个HttpHandler来负责这些工作
后台代码:
1 public class ImageHandler : IHttpHandler
2 {
3 /// <summary>
4 /// 实现IHttpHandler接口中ProcessRequest方法
5 /// </summary>
6 /// <param name="context"></param>
7 public void ProcessRequest(HttpContext context)
8 {
9 context.Response.Clear();
10 //得到图片名
11 var imageName = context.Request["ImageName"] ?? "小魔王";
12 //得到图片地址
13 var stringFilePath = context.Server.MapPath($"~/Image/{imageName}.jpg");
14 //声明一个FileStream用来将图片暂时放入流中
15 FileStream stream=new FileStream(stringFilePath,FileMode.Open);
16 using (stream)
17 {
18 //通过GetImageFromStream方法将图片放入Byte数组中
19 var imageBytes = GetImageFromStream(stream, context);
20 //上下文确定写道客户端时的文件类型
21 context.Response.ContentType = "image/jpeg";
22 //上下文将imageBytes中的数组写到前端
23 context.Response.BinaryWrite(imageBytes);
24 }
25 }
26
27 public bool IsReusable => true;
28
29 /// <summary>
30 /// 将流中的图片信息放入byte数组后返回该数组
31 /// </summary>
32 /// <param name="stream">文件流</param>
33 /// <param name="context">上下文</param>
34 /// <returns></returns>
35 private byte[] GetImageFromStream(FileStream stream, HttpContext context)
36 {
37 //通过Stream到Image
38 var image = Image.FromStream(stream);
39 //加上水印
40 image = SetWaterImage(image, context);
41 //得到一个ms对象
42 MemoryStream ms = new MemoryStream();
43 using (ms)
44 {
45 //将图片保存至内存流
46 image.Save(ms,ImageFormat.Jpeg);
47 byte[] imageBytes = new byte[ms.Length];
48 ms.Position = 0;
49 //通过内存流放到imageBytes
50 ms.Read(imageBytes, 0, imageBytes.Length);
51 //ms.Close();
52 //返回imageBytes
53 return imageBytes;
54 }
55 }
56
57 private Image SetWaterImage(Image image, HttpContext context)
58 {
59 Graphics graphics = Graphics.FromImage(image);
60 Image waterImage = Image.FromFile(context.Server.MapPath("~/Image/logo.jpg"));
61 graphics.DrawImage(waterImage, new Point { X = image.Size.Width - waterImage.Size.Width, Y = image.Size.Height - waterImage.Size.Height });
62 return image;
63 }
64 }
别忘了,还要再web.config中进行配置,如下:
这样前台就能使用了
让我们来看一下输出结果:
哈哈,还不错。
好了,MemoryStream相关的知识就先分享到这里了。同志们,再见!