C# Stream篇(五) -- MemoryStream


MemoryStream

目錄:

1 簡單介紹一下MemoryStream

2 MemoryStream和FileStream的區別

3 通過部分源碼深入了解下MemoryStream

4 分析MemorySteam最常見的OutOfMemory異常

5 MemoryStream 的構造

6 MemoryStream 的屬性

7 MemoryStream 的方法

8 MemoryStream 簡單示例 :  XmlWriter中使用MemoryStream

9 MemoryStream 簡單示例 :自定義一個處理圖片的HttpHandler

10 本章總結

 

 

 

簡單介紹一下MemoryStream

MemoryStream是內存流,為系統內存提供讀寫操作,由於MemoryStream是通過無符號字節數組組成的,可以說MemoryStream的性能可以

算比較出色,所以它擔當起了一些其他流進行數據交換時的中間工作,同時可降低應用程序中對臨時緩沖區和臨時文件的需要,其實MemoryStream

的重要性不亞於FileStream,在很多場合我們必須使用它來提高性能

 

MemoryStream和FileStream的區別

前文中也提到了,FileStream主要對文件的一系列操作,屬於比較高層的操作,但是MemoryStream卻很不一樣,它更趨向於底層內存的操作,這樣

能夠達到更快的速度和性能,也是他們的根本區別,很多時候,操作文件都需要MemoryStream來實際進行讀寫,最后放入到相應的FileStream中,

不僅如此,在諸如XmlWriter的操作中也需要使用到MemoryStream提高讀寫速度

 

通過部分源碼深入了解下MemoryStream

 由於篇幅關系,本篇無法詳細說明其源碼,還請大家海涵,這里我就簡單介紹下Write()方法的源碼

 
  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的源碼大家可以自己學習,這里主要分析下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異常

先看下下面一段很簡單的測試代碼

 
         //測試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();
 

由於我們設定了一個256M的byte(有點恐怖),看下溢出時的狀態

 

從輸出結果看,MemoryStream默認可用最大容量是512M  發生異常時正好是其最大容量,聰明的你肯定會問:如果同時使用2個MemoryStream甚至於多個內存

是怎么分配的?很好,還是用代碼來看下輸出結果,可以明顯看出內存平均分給了2個MemoryStream但是最大容量還是512M

 

但是問題來了,假設我們需要操作比較大的文件,該怎么辦呢?其實有2種方法能夠搞定,一種是前文所說的分段處理,我們將byte數組分成等份進行

處理,還有一個方法便是盡量增加MemoryStream的最大可用容量(字節),我們可以在聲明MemoryStream構造函數時利用它的重載版本:

MemoryStream(int capacity)

到底怎么使用哪種方法比較好呢?其實筆者認為具體項目具體分析,前者分段處理的確能夠解決大數據量操作的問題,但是犧牲了性能和時間(多線程暫

時不考慮),后者可以得到性能上的優勢但是其允許的最大容量是 int.MAX,所以無法給出一個明確的答案,大家在做項目按照需求自己定制即可,最關鍵

的還是要取到性能和開銷的最佳點位

         還有一種更惡心的溢出方式,往往會讓大家抓狂,就是不定時溢出,就是MemoryStream處理的文件可能只有40M或更小時也會發生OutOfMemory

的異常,關於這個問題,終於在老外的一篇文章中得到了解釋,運氣不錯,陳彥銘大哥在他的博客中正好翻譯了下,免去我翻譯的工作^^,由於這個牽涉到

windows的內存機制,包括 內存頁,進程的虛擬地址空間等,比較復雜,所以大家看他的這篇文章前,我先和大家簡單介紹下頁和進程的虛擬地址

內存頁:內存頁分為:文件頁和計算頁
內存中的文件頁是文件緩存區,即文件型的內存頁,用於存放文件數據的內存頁(也稱永久頁),作用在於讀寫文件時可以減少對磁盤的訪問,如果它的大小

設置得太小,會引起系統頻繁地訪問磁盤,增加磁盤I/O;設置太大,會浪費內存資源。內存中的計算頁也稱為計算型的內存頁,主要用於存放程序代碼和臨

時使用的數據

進程的虛擬地址:每一個進程被給予它的非常私有的虛擬地址空間。對於32位的進程,地址空間是4G因為一個32位指針能夠有從0x00000000到0xffffffff之

間的任意值。這個范圍允許指針有從4294967296個值的一個,覆蓋了一個進程的4G范圍。對於64位進程,地址空間是16eb因為一個64位指針能夠指向

18,446,744,073,709,551,616個值中的一個,覆蓋一個進程的16eb范圍。這是十分寬廣的范圍。

上述概念都來自windows核心編程 這本書,其實這本書對我們程序員來說很重要,對於內存的操作,本人也是小白,看來這本書非買不可了。。。。

 

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 的屬性

Memory 的屬性大致都是和其父類很相似,這些功能在我的這篇中已經詳細討論過,所以我簡單列舉一下其屬性:  

 

其獨有的屬性:

Capacity:這個前文其實已經提及,它表示該流的可支配容量(字節),非常重要的一個屬性

 

MemoryStream 的方法

對於重寫的方法這里不再重復說明,大家可以參考我寫的第一篇

以下是memoryStream獨有的方法

virtual byte[] GetBuffer()

這個方法使用時需要小心,因為這個方法返回無符號字節數組,也就是說,即使我只輸入幾個字符例如”HellowWorld”我們只希望返回11個數據就行,

可是這個方法會把整個緩沖區的數據,包括那些已經分配但是實際上沒有用到的字節數據都返回出來,如果想啟用這個方法那必須使用上面最后一個構

造函數,將publiclyVisible屬性設置成true就行,這也是上面那個構造函數的作用所在

 

virtual void WriteTo(Stream stream)

這個方法的目的其實在本文開始時討論性能問題時已經指出,memoryStream常用起中間流的作用,

所以讀寫在處理完后將內存流寫入其他流中

 

 簡單示例 XmlWriter中使用MemoryStream

 
        /// <summary>
        /// 演示在xmlWriter中使用MemoryStream
        /// </summary>
        public static void UseMemoryStreamInXMLWriter()
        {
            MemoryStream ms = new MemoryStream();
            using (ms)
            {
                //定義一個XMLWriter
                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();
                    }
                }
            }
        }
 

      輸出結果:

 


簡單示例:自定義一個處理圖片的HttpHandler

 有時項目里我們必須將圖片進行一定的操作,例如水印,下載等,為了方便和管理我們可以自定義一個HttpHander 來負責這些工作

后台:

 
  public class ImageHandler : IHttpHandler
    {
        #region IHttpHandler Members

        public bool IsReusable
        {
            get { return true; }
        }

        /// <summary>
        /// 實現IHTTPHandler后必須實現的方法
        /// </summary>
        /// <param name="context">HttpContext上下文</param>
        public void ProcessRequest(HttpContext context)
        {
            context.Response.Clear();
            //得到圖片名
            var imageName = context.Request["ImageName"] == null ? "逆時針的風"
                : 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();
            }
        }

        /// <summary>
        /// 將流中的圖片信息放入byte數組后返回該數組
        /// </summary>
        /// <param name="stream">文件流</param>
        /// <param name="context">上下文</param>
        /// <returns></returns>
        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>
        /// <returns></returns>
        private Image SetWaterImage(Image image,HttpContext context) 
        {
            Graphics graphics = Graphics.FromImage(image);
            Image waterImage = Image.FromFile(context.Server.MapPath("~/Image/逆時針的風01.jpg"));
            //在大圖右下角畫上水印圖就行
            graphics.DrawImage(waterImage,
                new Point { 
                    X = image.Size.Width - waterImage.Size.Width,
                    Y = image.Size.Height - waterImage.Size.Height 
                });
            return image;
        }

        #endregion
    }
 

別忘了還要在Web.Config中進行配置,別忘記verb和path屬性,否則會報錯

    <httpHandlers>
      <add type="ImageHandler.ImageHandler,ImageHandler"  verb="*" path="ImageHandler.apsx"/>
    </httpHandlers>

這樣前台便能使用了

 
<asp:Content ID="BodyContent" runat="server" ContentPlaceHolderID="MainContent">
    <h2>
        About
    </h2>
    <p>
        Put content here.
        <asp:Image runat="server" ImageUrl="ImageHandler.apsx?ImageName=逆時針的風&Id=02" />
    </p>
</asp:Content>
 

輸出結果

 

 

 本章總結

  本章主要介紹了MemoryStream 的一些概念,異常,結構,包括如何使用,如何解決一些異常等,感謝大家一直支持和鼓勵,文中如出現錯誤還請大家海涵,深夜寫文不容易,

  還請大家多多關注,下篇會介紹BufferedStream,盡請期待!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM