一、簡單介紹一下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相關的知識就先分享到這里了。同志們,再見!