【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相關的知識就先分享到這裡了。同志們,再