臨時解決方案 - RDLC報表內存泄露問題

項目中使用微軟RDLC生成工作票去打印,但是RDLC存在着嚴重的內存泄露問題。在生產高峯時期,工人將大量的工作票請求發送到服務器,隨着工作票的生成內存就一點點的被吃掉。致使IT部門不得不爲一個小小的工作票服務準備一臺8G內存的服務器,並且定期的查看服務狀態。在生產高峯時期每小時都要重啓。

這個內存泄露問題自從VS2005以來就存在,微軟聲稱在2008 SP1中已經修正,但是項目中使用的是2010的程序集版本且問題依然很嚴重。從微軟官方的回覆看由於RDLC使用VB進行表達式的計算,加載的VB相關的程序集由於某些原因不被Unload。我想微軟在VS2008SP1中修正的應該是這個問題,當然我沒有去考證,但是可以肯定的是在VS2010的RDLC中還是有內存泄露的代碼存在。

網友還做過測試,如果不使用Expression就不會導致內存泄露,但是我並不想修改太多的程序,如果你的項目剛剛開始或並不複雜,這也是一個辦法。參見原文:http://blog.darkthread.net/post-2012-01-12-rdlc-out-of-memory.aspx

於是着手從網上搜索如何查找內存泄露,推薦一個工具給大家。.Net Memory Profile http://memprofiler.com/. 這個工具可以分析出內存中哪些對象已經GC但是沒有被成功的移除或移除的不夠徹底。通過個這工具分析出LocalReport中的方法被事件或代理對象所引用無法GC。如下圖

image

從上圖可以看到LocalReport的身影,這張圖中的所有對象全與RDLC有關Disappointed smile

在網上搜到一牛人用反射解決了ReportViewer的內存泄露問題。參見原貼:http://social.msdn.microsoft.com/Forums/en-US/vsreportcontrols/thread/d21f8b56-3123-4aff-bf84-9cce727bc2ce

於是我參考了這個做法結合.Net Memory Profiler的分析結果開始將LocalReport對象上的事件和代理去掉。雖然這個方法失敗了還是把代碼貼出來吧,如下:

using System;
using System.Reflection;
using System.Linq;
using System.Windows.Forms;
using Microsoft.Reporting.WinForms;
using Microsoft.Win32;
using System.Collections;
namespace TOG.ProductionOutput.Services
{
    public class LocalReportDisposer : IDisposable
    {
        // Fields
        private bool _CollectGarbageOnDispose = false;
        private LocalReport localReport;
        private bool disposedValue = false;
        private const string LOCALREPORT_DATASOURCES = "m_dataSources";
        private const string LOCALREPORT_PROCESSINGHOST = "m_processingHost";
        private const string PROCESSINGHOST_DATARETRIEVAL = "m_dataRetrieval";
        private const string DATARETRIEVAL_SUBREPORTDATACALLBACK = "m_subreportDataCallback";
        private const string SUBREPORTDATACALLBACK_TARGET = "_target";
        private const string PROCESSINGHOST_EXECUTIONSESSION = "m_executionSession";
        private const string EXECUTIONSESSION_COMPILEDREPORT = "__compiledReport";
        private const string EXECUTIONSESSION_REPORTSNAPSHOT = "__ReportSnapshot";
        private const string DATASOURCES_ONCHANGE = "OnChange";
        // Methods
        public LocalReportDisposer(LocalReport localReport)
        {
            if (localReport == null)
            {
                throw new ArgumentNullException("ReportViewer cannot be null.");
            }
            this.localReport = localReport;
        }
        public void Dispose()
        {
            this.Dispose(true);
            GC.SuppressFinalize(this);
        }
        protected virtual void Dispose(bool disposing)
        {
            if (!this.disposedValue && disposing)
            {
                //this.TearDownLocalReport();
                this.localReport.Dispose();
                if (this._CollectGarbageOnDispose)
                {
                    GC.Collect();
                    GC.WaitForPendingFinalizers();
                    GC.Collect();
                }
            }
            this.disposedValue = true;
        }
        private void TearDownLocalReport()
        {
            Type t = this.localReport.GetType();
            //localReport.m_dataSources
            FieldInfo fi = t.GetField(LOCALREPORT_DATASOURCES, BindingFlags.NonPublic | BindingFlags.Instance);
            object dataSources = fi.GetValue(this.localReport);
            //remove event from localReport.m_dataSources.Change
            ReflectUtil.RemoveEventHandlersFrom(
                delegate(Delegate subject) { return subject.Method.Name == DATASOURCES_ONCHANGE; },
                dataSources);
            //localReport.m_processingHost
            fi = t.GetField(LOCALREPORT_PROCESSINGHOST, BindingFlags.NonPublic | BindingFlags.Instance);
            object processingHost = fi.GetValue(this.localReport);
            //localReport.m_processingHost.dataretrieval
            t = processingHost.GetType().BaseType;
            fi = t.GetField(PROCESSINGHOST_DATARETRIEVAL, BindingFlags.NonPublic | BindingFlags.Instance);
            object dataRetrieval = fi.GetValue(processingHost);
            //localReport.m_processingHost.m_dataRetrieval.m_subreportDataCallback
            t = dataRetrieval.GetType();
            fi = t.GetField(DATARETRIEVAL_SUBREPORTDATACALLBACK, BindingFlags.NonPublic | BindingFlags.Instance);
            object subReportDataCallBack = fi.GetValue(dataRetrieval);
            //localReport.m_processingHost.m_dataRetrieval.m_subreportDataCallback._target
            t = subReportDataCallBack.GetType().BaseType.BaseType;
            fi = t.GetField(SUBREPORTDATACALLBACK_TARGET, BindingFlags.NonPublic | BindingFlags.Instance);
            fi.SetValue(subReportDataCallBack, null);
            t = processingHost.GetType().BaseType;
            fi = t.GetField(PROCESSINGHOST_EXECUTIONSESSION, BindingFlags.NonPublic | BindingFlags.Instance);
            object executionSession = fi.GetValue(processingHost);
            t = executionSession.GetType();
            fi = t.GetField(EXECUTIONSESSION_COMPILEDREPORT, BindingFlags.NonPublic | BindingFlags.Instance);
            IDisposable report = fi.GetValue(executionSession) as IDisposable;
            if (report != null) report.Dispose();
            fi = t.GetField(EXECUTIONSESSION_REPORTSNAPSHOT, BindingFlags.NonPublic | BindingFlags.Instance);
            report = fi.GetValue(executionSession) as IDisposable;
            if (report != null) report.Dispose();
        }
        // Properties
        public bool CollectGarbageOnDispose
        {
            get
            {
                return this._CollectGarbageOnDispose;
            }
            set
            {
                this._CollectGarbageOnDispose = value;
            }
        }
    }
}

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Reflection;
namespace TOG.ProductionOutput.Services
{
    public sealed class ReflectUtil
    {
        private static BindingFlags PrivatePublicStaticInstance
          = BindingFlags.NonPublic | BindingFlags.Public |
            BindingFlags.Instance | BindingFlags.Static;
        public delegate bool MatchesOnDelegate(Delegate subject);
        public static void RemoveEventHandlersFrom(
          MatchesOnDelegate matchesOnDelegate, params object[] objectsWithEvents)
        {
            foreach (object owningObject in objectsWithEvents)
            {
                foreach (DelegateInfo eventFromOwningObject in GetDelegates(owningObject))
                {
                    foreach (Delegate subscriber in eventFromOwningObject.GetInvocationList())
                    {
                        if (matchesOnDelegate(subscriber))
                        {
                            EventInfo theEvent = eventFromOwningObject.GetEventInfo();
                            if(theEvent != null)
                                RemoveSubscriberEvenIfItsPrivate(theEvent, owningObject, subscriber);
                        }
                    }
                }
            }
        }
        // You can use eventInfo.RemoveEventHandler(owningObject, subscriber)
        // unless it's a private delegate
        private static void RemoveSubscriberEvenIfItsPrivate(
          EventInfo eventInfo, object owningObject, Delegate subscriber)
        {
            MethodInfo privateRemoveMethod = eventInfo.GetRemoveMethod(true);
            privateRemoveMethod.Invoke(owningObject,
                                       PrivatePublicStaticInstance, null,
                                       new object[] { subscriber }, CultureInfo.CurrentCulture);
        }
        private static DelegateInfo[] GetDelegates(object owningObject)
        {
            List<DelegateInfo> delegates = new List<DelegateInfo>();
            FieldInfo[] allPotentialEvents = owningObject.GetType()
              .GetFields(PrivatePublicStaticInstance);
            foreach (FieldInfo privateFieldInfo in allPotentialEvents)
            {
                Delegate eventFromOwningObject = privateFieldInfo.GetValue(owningObject)
                  as Delegate;
                if (eventFromOwningObject != null)
                {
                    delegates.Add(new DelegateInfo(eventFromOwningObject, privateFieldInfo,
                      owningObject));
                }
            }
            return delegates.ToArray();
        }
        private class DelegateInfo
        {
            private readonly Delegate delegateInformation;
            public Delegate DelegateInformation
            {
                get { return delegateInformation; }
            }
            private readonly FieldInfo fieldInfo;
            private readonly object owningObject;
            public DelegateInfo(Delegate delegateInformation, FieldInfo fieldInfo,
              object owningObject)
            {
                this.delegateInformation = delegateInformation;
                this.fieldInfo = fieldInfo;
                this.owningObject = owningObject;
            }
            public Delegate[] GetInvocationList()
            {
                return delegateInformation.GetInvocationList();
            }
            public EventInfo GetEventInfo()
            {
                return owningObject.GetType().GetEvent(fieldInfo.Name,
                  PrivatePublicStaticInstance);
            }
        }
    }
}

RefactUtil是我從網上找到的,因爲使用RemoveEventHandler方法會報 “Cannot remove the event handler since no public remove method exists for the event.”,所以搜索到了這篇文章。

參見原文:http://www.thekua.com/atwork/2007/09/events-reflection-and-how-they-dont-work-in-c/comment-page-1/

由於複雜的引用關係並且在反射時並不是所有的分析器分析到的對象都能拿到,這個方法最終還是放棄了。

繼續在網上查找,找到了如下這個貼子

http://stackoverflow.com/questions/6220915/very-high-memory-usage-in-net-4-0

大致的意思就是起一個線程並讓Report運行在這個線程的內存堆上,這樣當線程銷燬時LocalReport也隨着線程一起銷燬了,試驗效果還是不錯的。

這時又在網上找到了另一段代碼http://www.pcreview.co.uk/forums/reportviewer-localreport-own-appdomain-t3997991.html

大致的意思就是創建出一個新App Domain讓LocalReport運行在這個Domain上,當要銷燬LocalReport時銷燬這個App Domain即可。

最終代碼如下:

using Microsoft.Reporting.WinForms;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace TOG.ProductionOutput.Services
{
    /// <summary>
    /// Manager of LocalReport for create and dispose
    /// </summary>
    public class LocalReportManager : IDisposable
    {
        // Fields
        private bool collectGarbageOnDispose = false;
        private LocalReport localReport;
        private LocalReportFactory factory;
        private bool disposedValue = false;
        /// <summary>
        /// Init LocalReport Disposer
        /// </summary>
        /// <param name="localReport">LocalReport Object</param>
        public LocalReportManager()
        {
            factory = new LocalReportFactory();
            this.localReport = factory.CreateLocalReportParser();
        }
        /// <summary>
        /// get local report
        /// </summary>
        public LocalReport LocalReport
        {
            get { return localReport; }
        }
        /// <summary>
        /// IDispose.Dispose
        /// </summary>
        public void Dispose()
        {
            this.Dispose(true);
            GC.SuppressFinalize(this);
        }
        /// <summary>
        /// Dispose LocalReport Object
        /// </summary>
        /// <param name="disposing">GC?</param>
        protected virtual void Dispose(bool disposing)
        {
            if (!this.disposedValue && disposing)
            {
                factory.Unload();
                if (this.collectGarbageOnDispose)
                {
                    GC.Collect();
                    GC.WaitForPendingFinalizers();
                    GC.Collect();
                }
            }
            this.disposedValue = true;
        }
        /// <summary>
        /// whether GC immediatelly when dispose, might affect the performance
        /// </summary>
        public bool CollectGarbageOnDispose
        {
            get
            {
                return this.collectGarbageOnDispose;
            }
            set
            {
                this.collectGarbageOnDispose = value;
            }
        }
    }
}

using Microsoft.Reporting.WinForms;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;
namespace TOG.ProductionOutput.Services
{
    /// <summary>
    /// Factory of LocalReport
    /// </summary>
    public class LocalReportFactory : MarshalByRefObject
    {
        public AppDomain LocalAppDomain = null;
        public string ErrorMessage = string.Empty;
        /// <summary>
        /// Creates a new instance of the LocalReportParser in a new AppDomain
        /// </summary>
        /// <returns></returns>
        public LocalReport CreateLocalReportParser()
        {
            this.CreateAppDomain(null);
            LocalReport parser = null;
            try
            {
                Type MyLR = typeof(LocalReport);
                parser = (LocalReport)this.LocalAppDomain.CreateInstanceAndUnwrap(MyLR.Assembly.FullName, MyLR.FullName);
            }
            catch (Exception ex)
            {
                this.ErrorMessage = ex.Message;
            }
            return parser;
        }
        /// <summary>
        /// Create a new app domain.
        /// </summary>
        /// <param name="appDomain">domain name</param>
        /// <returns></returns>
        private void CreateAppDomain(string appDomain)
        {
            if (string.IsNullOrEmpty(appDomain))
                appDomain = "LocalReportParser" + Guid.NewGuid().ToString().GetHashCode().ToString("x");
            AppDomainSetup domainSetup = new AppDomainSetup();
            // *** Point at current directory
            domainSetup.DisallowBindingRedirects = false;
            domainSetup.DisallowCodeDownload = true;
            this.LocalAppDomain = AppDomain.CreateDomain(appDomain, null, domainSetup);
            // *** Need a custom resolver so we can load assembly from non current path
            this.LocalAppDomain.AssemblyResolve += new ResolveEventHandler(CurrentDomain_AssemblyResolve);
        }
        Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args)
        {
            foreach (Assembly LR in AppDomain.CurrentDomain.GetAssemblies())
            {
                if (string.Compare(LR.GetName().Name, args.Name, true) == 0 ||
                string.Compare(LR.FullName, args.Name, true) == 0)
                    return LR;
            }
            return null;
        }
        /// <summary>
        /// Unload app domain
        /// </summary>
        public void Unload()
        {
            if (this.LocalAppDomain != null)
            {
                AppDomain.Unload(this.LocalAppDomain);
                this.LocalAppDomain = null;
            }
        }
    }
}
使用方法如下:
using (LocalReportManager reportManager = new LocalReportManager())
{
       LocalReport Ticket = reportManager.LocalReport;
       // to do report
}

config中加入:

<runtime>
    <NetFx40_LegacySecurityPolicy enabled="true"/>
</runtime>

如果不加入這個配置還是不行,我想LocalReport雖然放到了當前的Domain裏,但是LocalReport在Render時不一定會運行在當前的Domain裏。微軟的官方說法是默認是當前Domain,但是我的測試結果是不加入這個配置就不會運行在當前Domain裏。

分析結果中就看不到LocalReport及其同黨的身影了Smile

Capture

這僅是一個臨時的解決方案,記錄一下僅供參考,希望對你有所幫助,如果有更好的辦法請告訴我,謝謝Smile

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章