临时解决方案-RDLC报表内存泄露问题

项目中使用微软RDLC生成工作票去打印,但是RDLC存在着严重的内存泄露问题。在生产高峰时期,工人将大量的工作票请求发送到服务器,随着工作票的生成内存就一点点的被吃掉。致使IT部门不得不为一个小小的工作票服务准备一台8G内存的服务器,并且定期的查看服务状态。在生产高峰时期每小时都要重启。

创新互联是专业的三亚网站建设公司,三亚接单;提供成都做网站、成都网站制作,网页设计,网站设计,建网站,PHP网站建设等专业做网站服务;采用PHP框架,可快速的进行三亚网站开发网页制作和功能扩展;专业做搜索引擎喜爱的网站,专业的做网站团队,希望更多企业前来合作!

这个内存泄露问题自从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。如下图

临时解决方案 - RDLC报表内存泄露问题

从上图可以看到LocalReport的身影,这张图中的所有对象全与RDLC有关临时解决方案 - RDLC报表内存泄露问题

在网上搜到一牛人用反射解决了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.Reflection;using System.Linq;using System.Windows.Forms;  using Microsoft.Reporting.WinForms;  using Microsoft.Win32;using System.Collections;namespace TOG.ProductionOutput.Services{publicclass LocalReportDisposer : IDisposable    {// Fields  privatebool _CollectGarbageOnDispose = false;private LocalReport localReport;privatebool disposedValue = false;privateconststring LOCALREPORT_DATASOURCES = "m_dataSources";privateconststring LOCALREPORT_PROCESSINGHOST = "m_processingHost";privateconststring PROCESSINGHOST_DATARETRIEVAL = "m_dataRetrieval";privateconststring DATARETRIEVAL_SUBREPORTDATACALLBACK = "m_subreportDataCallback";privateconststring SUBREPORTDATACALLBACK_TARGET = "_target";privateconststring PROCESSINGHOST_EXECUTIONSESSION = "m_executionSession";privateconststring EXECUTIONSESSION_COMPILEDREPORT = "__compiledReport";privateconststring EXECUTIONSESSION_REPORTSNAPSHOT = "__ReportSnapshot";privateconststring DATASOURCES_ONCHANGE = "OnChange";// Methods  public LocalReportDisposer(LocalReport localReport)        {if (localReport == null)            {thrownew ArgumentNullException("ReportViewer cannot be null.");            }this.localReport = localReport;        }publicvoid Dispose()        {this.Dispose(true);            GC.SuppressFinalize(this);        }protectedvirtualvoid Dispose(bool disposing)        {if (!this.disposedValue && disposing)            {//this.TearDownLocalReport();this.localReport.Dispose();if (this._CollectGarbageOnDispose)                {                    GC.Collect();                    GC.WaitForPendingFinalizers();                    GC.Collect();                }            }this.disposedValue = true;        }privatevoid TearDownLocalReport()        {            Type t = this.localReport.GetType();//localReport.m_dataSourcesFieldInfo fi = t.GetField(LOCALREPORT_DATASOURCES, BindingFlags.NonPublic | BindingFlags.Instance);object dataSources = fi.GetValue(this.localReport);//remove event from localReport.m_dataSources.ChangeReflectUtil.RemoveEventHandlersFrom(delegate(Delegate subject) { return subject.Method.Name == DATASOURCES_ONCHANGE; },                dataSources);//localReport.m_processingHostfi = t.GetField(LOCALREPORT_PROCESSINGHOST, BindingFlags.NonPublic | BindingFlags.Instance);object processingHost = fi.GetValue(this.localReport);//localReport.m_processingHost.dataretrievalt = processingHost.GetType().BaseType;            fi = t.GetField(PROCESSINGHOST_DATARETRIEVAL, BindingFlags.NonPublic | BindingFlags.Instance);object dataRetrieval = fi.GetValue(processingHost);//localReport.m_processingHost.m_dataRetrieval.m_subreportDataCallbackt = dataRetrieval.GetType();            fi = t.GetField(DATARETRIEVAL_SUBREPORTDATACALLBACK, BindingFlags.NonPublic | BindingFlags.Instance);object subReportDataCallBack = fi.GetValue(dataRetrieval);//localReport.m_processingHost.m_dataRetrieval.m_subreportDataCallback._targett = 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  publicbool CollectGarbageOnDispose        {get{returnthis._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 delegates = new List();
            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);
            }
        }
    }
}
using System;using System.Collections.Generic;using System.Globalization;using System.Reflection;namespace TOG.ProductionOutput.Services{publicsealedclass ReflectUtil    {privatestatic BindingFlags PrivatePublicStaticInstance          = BindingFlags.NonPublic | BindingFlags.Public |            BindingFlags.Instance | BindingFlags.Static;publicdelegatebool MatchesOnDelegate(Delegate subject);publicstaticvoid RemoveEventHandlersFrom(          MatchesOnDelegate matchesOnDelegate, paramsobject[] 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 delegateprivatestaticvoid RemoveSubscriberEvenIfItsPrivate(          EventInfo eventInfo, object owningObject, Delegate subscriber)        {            MethodInfo privateRemoveMethod = eventInfo.GetRemoveMethod(true);            privateRemoveMethod.Invoke(owningObject,                                       PrivatePublicStaticInstance, null,                                       newobject[] { subscriber }, CultureInfo.CurrentCulture);        }privatestatic DelegateInfo[] GetDelegates(object owningObject)        {            List delegates = new List();            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();        }privateclass DelegateInfo        {privatereadonly Delegate delegateInformation;public Delegate DelegateInformation            {get { return delegateInformation; }            } privatereadonly FieldInfo fieldInfo;privatereadonlyobject 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
{
    /// 
    /// Manager of LocalReport for create and dispose
    /// 
    public class LocalReportManager : IDisposable
    {
        // Fields
        private bool collectGarbageOnDispose = false;
        private LocalReport localReport;
        private LocalReportFactory factory;
        private bool disposedValue = false;
        /// 
        /// Init LocalReport Disposer
        /// 
        /// LocalReport Object
        public LocalReportManager()
        {
            factory = new LocalReportFactory();
            this.localReport = factory.CreateLocalReportParser();
        }
        /// 
        /// get local report
        /// 
        public LocalReport LocalReport
        {
            get { return localReport; }
        }
        /// 
        /// IDispose.Dispose
        /// 
        public void Dispose()
        {
            this.Dispose(true);
            GC.SuppressFinalize(this);
        }
        /// 
        /// Dispose LocalReport Object
        /// 
        /// GC?
        protected virtual void Dispose(bool disposing)
        {
            if (!this.disposedValue && disposing)
            {
                factory.Unload();
                if (this.collectGarbageOnDispose)
                {
                    GC.Collect();
                    GC.WaitForPendingFinalizers();
                    GC.Collect();
                }
            }
            this.disposedValue = true;
        }
        /// 
        /// whether GC immediatelly when dispose, might affect the performance
        /// 
        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.Text;namespace TOG.ProductionOutput.Services{/// /// Manager of LocalReport for create and dispose/// publicclass LocalReportManager : IDisposable    {// Fields  privatebool collectGarbageOnDispose = false;private LocalReport localReport;private LocalReportFactory factory;privatebool disposedValue = false;/// /// Init LocalReport Disposer/// /// LocalReport Objectpublic LocalReportManager()        {            factory = new LocalReportFactory();this.localReport = factory.CreateLocalReportParser();        }/// /// get local report/// public LocalReport LocalReport        {get { return localReport; }        }/// /// IDispose.Dispose/// publicvoid Dispose()        {this.Dispose(true);            GC.SuppressFinalize(this);        }/// /// Dispose LocalReport Object/// /// GC?protectedvirtualvoid Dispose(bool disposing)        {if (!this.disposedValue && disposing)            {                factory.Unload();if (this.collectGarbageOnDispose)                {                    GC.Collect();                    GC.WaitForPendingFinalizers();                    GC.Collect();                }            }this.disposedValue = true;        }/// /// whether GC immediatelly when dispose, might affect the performance/// publicbool CollectGarbageOnDispose        {get{returnthis.collectGarbageOnDispose;            }set{this.collectGarbageOnDispose = value;            }        }    }}
临时解决方案 - RDLC报表内存泄露问题
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
{
    /// 
    /// Factory of LocalReport
    /// 
    public class LocalReportFactory : MarshalByRefObject
    {
        public AppDomain LocalAppDomain = null;
        public string ErrorMessage = string.Empty;
        /// 
        /// Creates a new instance of the LocalReportParser in a new AppDomain
        /// 
        /// 
        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;
        }
        /// 
        /// Create a new app domain.
        /// 
        /// domain name
        /// 
        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;
        }
        /// 
        /// Unload app domain
        /// 
        public void Unload()
        {
            if (this.LocalAppDomain != null)
            {
                AppDomain.Unload(this.LocalAppDomain);
                this.LocalAppDomain = null;
            }
        }
    }
}
使用方法如下:
临时解决方案 - RDLC报表内存泄露问题
using (LocalReportManager reportManager = new LocalReportManager())
{
       LocalReport Ticket = reportManager.LocalReport;
       // to do report
}

config中加入:

临时解决方案 - RDLC报表内存泄露问题

    

如果不加入这个配置还是不行,我想LocalReport虽然放到了当前的Domain里,但是LocalReport在Render时不一定会运行在当前的Domain里。微软的官方说法是默认是当前Domain,但是我的测试结果是不加入这个配置就不会运行在当前Domain里。

分析结果中就看不到LocalReport及其同党的身影了临时解决方案 - RDLC报表内存泄露问题

临时解决方案 - RDLC报表内存泄露问题

这仅是一个临时的解决方案,记录一下仅供参考,希望对你有所帮助,如果有更好的办法请告诉我,谢谢临时解决方案 - RDLC报表内存泄露问题


文章题目:临时解决方案-RDLC报表内存泄露问题
网站路径:http://bzwzjz.com/article/psgocd.html

其他资讯

Copyright © 2007-2020 广东宝晨空调科技有限公司 All Rights Reserved 粤ICP备2022107769号
友情链接: 重庆网站设计 重庆网站制作 成都网站设计 网站建设方案 成都网站建设公司 成都网站建设公司 攀枝花网站设计 企业网站建设 手机网站制作 成都营销网站制作 成都网站建设 重庆外贸网站建设 网站建设方案 响应式网站设计方案 营销型网站建设 成都网站建设公司 定制网站设计 成都网站建设 温江网站设计 企业网站建设公司 定制级高端网站建设 成都网站设计