什么是 MemoryStream ?
MemoryStream 是内存流,为系统内存提供读写操作,由于MemoryStream是通过无符号字节数组组成的,可以说MemoryStream的性能可以算比较出色,所以它担当起了一些其他流进行数据交换时的中间工作,同时可降低应用程序中对临时缓冲区和临时文件的需要,其实MemoryStream的重要性不亚于FileStream,在很多场合我们必须使用它来提高性能。
MemoryStream和FileStream的区别
FileStream主要对文件进行一系列的操作,属于比较高层的操作,但是MemoryStream却很不一样,它更趋向于底层内存的操作,这样能够达到更快的速度和性能,这也是他们的根本区别,很多时候,操作文件都需要MemoryStream来实际进行读写,最后放入到相应的FileStream中,不仅如此,在诸如XmlWriter的操作中也需要使用到MemoryStream提高读写速度。
通过部分源码深入了解下MemoryStream
public override void Write(byte[] buffer, int offset, int count)
{
if (!_isOpen) __Error.StreamIsClosed();
if (!_writable) __Error.WriteNotSupported();
if (buffer==null)
throw new ArgumentNullException("buffer", Environment.GetResourceString("ArgumentNull_Buffer"));
if (offset < 0)
throw new ArgumentOutOfRangeException("offset", Environment.GetResourceString("ArgumentOutOfRange_NeedNonNegNum"));
if (count < 0)
throw new ArgumentOutOfRangeException("count", Environment.GetResourceString("ArgumentOutOfRange_NeedNonNegNum"));
if (buffer.Length - offset < count)
throw new ArgumentException(Environment.GetResourceString("Argument_InvalidOffLen"));
int i = _position + count;
// Check for overflow
if (i < 0)
throw new IOException(Environment.GetResourceString("IO.IO_StreamTooLong"));
if (i > _length) {
bool mustZero = _position > _length;
if (i > _capacity) {
bool allocatedNewArray = EnsureCapacity(i);
if (allocatedNewArray)
mustZero = false;
}
if (mustZero)
Array.Clear(_buffer, _length, i - _length);
_length = i;
}
if (count <= 8)
{
int byteCount = count;
while (--byteCount >= 0)
_buffer[_position + byteCount] = buffer[offset + byteCount];
}
else
Buffer.InternalBlockCopy(buffer, offset, _buffer, _position, count);
_position = i;
return;
}
这里主要分析下MemoryStream最关键的Write()方法,自上而下,最开始的一系列判断大家很容易看明白,以后对有可能发生的异常应该了如指掌,判断后会取得这段数据的长度 int i=_position+count ,接下来会去判断该数据的长度是否超过了该流的长度,如果超过再去检查是否在流的可支配容量(字节)之内,(注意下EnsureCapacity方法,该方法会自动扩容stream的容量,但是前提条件是你使用了memoryStream的第二个构造函数,也就是带有参数是Capaciy)如果超过了流的可支配容量则将尾巴删除(将超过部分的数据清除),接下来大家肯定会问,为什么要判断count<=8,其实8这个数字在流中很关键,个人认为微软为了性能需要而这样写:当字节小于8时则一个个读,当字节大于八时则用block拷贝的方式,在这个范围内递减循环将数据写入流中的缓冲_buffer中,这个缓冲_buffe是memoryStream的一个私有byte数组类型,流通过读取外部byte数据放入内部那个缓冲buffer中,如果流的长度超过了8,则用Buffer.InternalBloackCopy方法进行数组复制,不同于Array.Copy 前者是采用内存位移而非索引位移所以性能上有很大的提升。其实这个方法的原形是属于c++中的。
分析MemorySteam最常见的OutOfMemory异常
static void Main(string[] args)
{
// 测试byte数组 假设该数组容量是256M
byte[] testBytes = new byte[256 * 1024 * 1024];
MemoryStream ms = new MemoryStream();
using (ms)
{
for (int i = 0; i < 1000; i++)
{
try
{
ms.Write(testBytes, 0, testBytes.Length);
}
catch
{
Console.WriteLine("该内存流已经使用了{0}M容量的内存,该内存流最大容量为{1}M,溢出时容量为{2}M",
GC.GetTotalMemory(false) / (1024 * 1024),// MemoryStream 已经消耗内存量
ms.Capacity / (1024 * 1024), // MemoryStream 最大的可用容量
ms.Length / (1024 * 1024));// MemoryStream 当前流的长度(容量)
break;
}
}
}
Console.ReadLine();
}
假设我们需要操作比较大的文件,该怎么办呢?其实有2种方法能够搞定,一种是前文所说的分段处理,我们将byte数组分成等份进行处理,还有一个方法便是尽量增加MemoryStream的最大可用容量(字节),我们可以在声明MemoryStream构造函数时利用它的重载版本:
MemoryStream(int capacity)
到底怎么使用哪种方法比较好呢?具体项目具体分析,前者分段处理的确能够解决大数据量操作的问题,但是牺牲了性能和时间(多线程暂时不考虑),后者可以得到性能上的优势但是其允许的最大容量是 int.MAX,所以无法给出一个明确的答案,大家在做项目按照需求自己定制即可,最关键的还是要取到性能和开销的最佳点位。
MemoryStream 的构造
MemoryStream()
MemoryStream 允许不带参数的构造
MemoryStream(byte[] byte)
Byte数组是包含了一定的数据的byte数组,这个构造很重要,初学者或者用的不是很多的程序员会忽略这个构造导致后面读取或写入数据时发现memoryStream中没有byte数据,会导致很郁闷的感觉,大家注意下就行,有时也可能无需这样,因为很多方法返回值已经是MemoryStream了。
MemoryStream(int capacity)
这个是重中之重,为什么这么说呢?我在本文探讨关于OutOfMemory异常中也提到了,如果你想额外提高MemoryStream的吞吐量(字节),也只能靠这个方法提升一定的吞吐量,最多也只能到int.Max,这个方法也是解决OutOfMemory的一个可行方案。
MemoryStream(byte[] byte, bool writeable)
Writeable参数定义该流是否可写
MemoryStream(byte[] byte, int index, int count)
Index 参数定义从byte数组中的索引index,
Count参数是获取的数据量的个数
MemoryStream(byte[] byte,int index, int count, bool writeable, bool publiclyVisible)
publiclyVisible 参数表示true 可以启用 GetBuffer方法,它返回无符号字节数组,流从该数组创建;否则为 false,(大家一定觉得这很难理解,别急下面的方法中
我会详细讲下这个东东)。
MemoryStream 的属性
其独有的属性:
Capacity:这个前文其实已经提及,它表示该流的可支配容量(字节),非常重要的一个属性
MemoryStream 的方法
以下是memoryStream独有的方法
virtual byte[] GetBuffer()
这个方法使用时需要小心,因为这个方法返回无符号字节数组,也就是说,即使我只输入几个字符例如”HellowWorld”我们只希望返回11个数据就行,可是这个方法会把整个缓冲区的数据,包括那些已经分配但是实际上没有用到的字节数据都返回出来,如果想启用这个方法那必须使用上面最后一个构造函数,将publiclyVisible属性设置成true就行,这也是上面那个构造函数的作用所在。
virtual void WriteTo(Stream stream)
这个方法的目的其实在本文开始时讨论性能问题时已经指出,memoryStream常用起中间流的作用,所以读写在处理完后将内存流写入其他流中。
示例1:XmlWriter 中使用 MemoryStream
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
using System.Xml;
using System.Threading;
using System.Windows.Forms;
using System.Globalization;
namespace ConsoleApplication1
{
class Program
{
static void Main(string[] args)
{
UseMemoryStreamInXMLWriter();
}
/// <summary>
/// 演示在xmlWriter中使用MemoryStream
/// </summary>
public static void UseMemoryStreamInXMLWriter()
{
MemoryStream ms = new MemoryStream();
using (ms)
{
using (XmlWriter writer = XmlWriter.Create(ms)) // 这个读写器是只进的
{
// 写入xml头
writer.WriteStartDocument(true);
// 写入一个元素
writer.WriteStartElement("Content");
// 为这个元素新增一个test属性
writer.WriteStartAttribute("test");
// 设置test属性的值
writer.WriteValue("逆时针的风");
// 释放缓冲,这里可以不用释放,但是在实际项目中可能要考虑部分释放对性能带来的提升
writer.Flush();
Console.WriteLine("此时内存使用量为:{2}KB,该MemoryStream的已经使用的容量为{0}byte,默认容量为{1}byte",
Math.Round((double)ms.Length, 4), ms.Capacity, GC.GetTotalMemory(false) / 1024);
Console.WriteLine("重新定位前MemoryStream所在的位置是{0}", ms.Position);
// 将流中所在的当前位置往后移动7位,相当于空格
ms.Seek(7, SeekOrigin.Current);
Console.WriteLine("重新定位后MemoryStream所在的位置是{0}", ms.Position);
// 如果将流所在的位置设置为如下所示的位置则xml文件会被打乱
// ms.Position = 0;
writer.WriteStartElement("Content2");
writer.WriteStartAttribute("testInner");
writer.WriteValue("逆时针的风Inner");
writer.WriteEndElement();
writer.WriteEndElement();
// 再次释放
writer.Flush();
Console.WriteLine("此时内存使用量为:{2}KB,该MemoryStream的已经使用的容量为{0}byte,默认容量为{1}byte",
Math.Round((double)ms.Length, 4), ms.Capacity, GC.GetTotalMemory(false) / 1024);
// 建立一个FileStream 文件创建目的地是d:\test.xml
FileStream fs = new FileStream(@"d:\test.xml", FileMode.OpenOrCreate);
using (fs)
{
// 将内存流注入 FileStream
ms.WriteTo(fs);
if (ms.CanWrite)
{
// 释放缓冲区
fs.Flush();
}
}
}
}
}
}
}
示例2:给图片加上水印的 HttpHandler
后台代码:
using System;
using System.Collections;
using System.Data;
using System.Linq;
using System.Web;
using System.Web.Services;
using System.Web.Services.Protocols;
using System.Xml.Linq;
using System.IO;
using System.Drawing;
using System.Drawing.Imaging;
namespace WebApplication1
{
/// <summary>
/// $codebehindclassname$ 的摘要说明
/// </summary>
[WebService(Namespace = "http://tempuri.org/")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
public class Handler1 : IHttpHandler
{
public void ProcessRequest(HttpContext context)
{
context.Response.Clear();
// 得到图片名
var imageName = context.Request["ImageName"] == null ? "jack"
: context.Request["ImageName"].ToString();
// 得到图片ID,这里只是演示,实际项目中不是这么做的
var id = context.Request["Id"] == null ? "01" : context.Request["Id"].ToString();
// 得到图片地址
var stringFilePath = context.Server.MapPath(string.Format("~/Image/{0}{1}.jpg", imageName, id));
// 声明一个FileStream用来将图片暂时放入流中
FileStream stream = new FileStream(stringFilePath, FileMode.Open);
using (stream)
{
// 透过GetImageFromStream方法将图片放入byte数组中
byte[] imageBytes = this.GetImageFromStream(stream, context);
// 上下文确定写到客户短时的文件类型
context.Response.ContentType = "image/jpeg";
// 上下文将imageBytes中的数据写到前段
context.Response.BinaryWrite(imageBytes);
stream.Close();
}
}
public bool IsReusable
{
get
{
return false;
}
}
/// <summary>
/// 将流中的图片信息放入byte数组后返回该数组
/// </summary>
/// <param name="stream">文件流</param>
/// <param name="context">上下文</param>
private byte[] GetImageFromStream(FileStream stream, HttpContext context)
{
// 通过stream得到Image
Image image = Image.FromStream(stream);
// 加上水印
image = SetWaterImage(image, context);
// 得到一个ms对象
MemoryStream ms = new MemoryStream();
using (ms)
{
// 将图片保存至内存流
image.Save(ms, ImageFormat.Jpeg);
byte[] imageBytes = new byte[ms.Length];
ms.Position = 0;
// 通过内存流读取到imageBytes
ms.Read(imageBytes, 0, imageBytes.Length);
ms.Close();
// 返回imageBytes
return imageBytes;
}
}
/// <summary>
/// 为图片加上水印,这个方法不用在意,只是演示,所以没加透明度
/// </summary>
/// <param name="image">需要加水印的图片</param>
/// <param name="context">上下文</param>
private Image SetWaterImage(Image image, HttpContext context)
{
Graphics graphics = Graphics.FromImage(image);
Image waterImage = Image.FromFile(context.Server.MapPath("~/Image/jack01.jpg"));
// 在大图右下角画上水印图就行
graphics.DrawImage(waterImage,
new Point
{
X = image.Size.Width - waterImage.Size.Width,
Y = image.Size.Height - waterImage.Size.Height
});
return image;
}
}
}
Web.Config中进行HttpHandler的配置,别忘记verb和path属性,否则会报错
<httpHandlers>
<add verb="*" type="WebApplication1.Handler1" path="Handler1.ashx"/>
</httpHandlers>
前台代码:
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="WebForm1.aspx.cs" Inherits="WebApplication1.WebForm1" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
<title>无标题页</title>
</head>
<body>
<form id="form1" runat="server">
<asp:Image ID="Image1" runat="server" ImageUrl="Handler1.ashx?ImageName=jack&Id=02" />
</form>
</body>
</html>
最终效果: