主頁 > .NET開發 > 深入理解.NET/WPF記憶體泄漏

深入理解.NET/WPF記憶體泄漏

2020-12-12 06:00:33 .NET開發

眾所周知,記憶體管理和如何避免記憶體泄漏(memory leak)一直是軟體開發的難題,不要說C、C++等非托管(unmanaged)語言,即使是Java、.NET等托管(managed)語言,盡管有著完善的垃圾回收器(GC),記憶體泄漏也經常發生,不過,這并非GC的bug或設計缺陷,而是因為在開發時有太多能夠導致記憶體泄漏的方式了,尤其是對于系結(Binding)、事件(Event)、行為(Behavior)滿天飛的WPF/UWP應用,

對于托管類應用,記憶體泄漏主要可以分為兩大類:托管類記憶體泄漏(managed memory leak)和非托管類記憶體泄漏(unmanaged memory leak),

 

1.托管類記憶體泄漏(managed memory leak)


這種泄漏發生的根本原因是由于無用的、本該被回收的托管類物件(managed objects)由于被“無意的”(unintended)的參考而導致無法被回收,

這與GC的作業原理有關:在進行垃圾回收時,應用將掛起所有執行緒,這樣GC就可以遍歷所有的GC Root物件,并將它們標記為”不可回收“,接著GC進一步將它們所參考的所有物件也都標記為”不可回收“,這個程序將一直遞回進行下去,直到無法繼續,所有未被標記為”不可回收“的物件都被GC視為垃圾物件,最終都將被回收,簡而言之,GC對“無用”物件的識別機制很簡單:判斷物件是否被“GC Root”物件所參考,

可以被視為GC Root的物件主要包括三大類:

a.正在執行的執行緒的“活躍”堆疊(Live Stack of the running threads),包括正在執行的方法的引數、區域變數、暫存器變數等;
b.靜態變數(Static variables);
c.通過interop傳遞給COM物件(其記憶體回收采用“參考計數”機制)的托管物件

如果一個物件被生存期更長的物件(例如全域物件或靜態類)所參考,那么在進行GC時,即使它已經不會再被用到,也會被標記為”不可回收“,這就是記憶體泄漏 ,當然,被沒有被標記為”不可回收“的物件(“垃圾”物件)所參考是不會阻止被參考物件被回收的,這種情況就不算是記憶體泄漏,

通常,導致無用的物件被GC Root無意參考的常見場景有以下幾種:(注意這里只討論”無意“的參考,開發者通過靜態變數或生存期超長的物件建立的”有意“的參考不在討論之列)

 

1)Event訂閱


普通的事件訂閱(event subscribing),例如代碼source.SomeEvent += new SomeEventHandler(someObject.MyEventHandler),將創建一個從source到someObject的強參考(strong reference),如果source物件的生存期比someObject的長,那么將產生記憶體泄漏,

如一個在WPF/UWP中非常常見的場景:

    public partial class UserControl1 : UserControl
    {
        public UserControl1()
        {
            InitializeComponent();

            Application.Current.MainWindow.SizeChanged += this.MainWindow_SizeChanged;

        }

        private void MainWindow_SizeChanged(object sender, SizeChangedEventArgs e)
        {
            Debug.WriteLine($"主表單size改變:{e.NewSize}");
        }
    }

UserControl1訂閱了MainWindow的SizeChanged事件,那么MainWindow將保持一個對UserControl1的參考,如果MainWindow的生存期比UserControl1長,那么將產生記憶體泄漏,

  解決這類記憶體泄漏的方法有:

a)手動取消訂閱(unsubscribe)

可以將上述代碼修改如下:

    public partial class UserControl1 : UserControl
    {
        public UserControl1()
        {
            InitializeComponent();
            this.Loaded += this.UserControl1_Loaded;
            this.Unloaded += this.UserControl1_Unloaded;
        }

        private void UserControl1_Unloaded(object sender, RoutedEventArgs e)
        {
            Application.Current.MainWindow.SizeChanged -= this.MainWindow_SizeChanged;
        }

        private void UserControl1_Loaded(object sender, RoutedEventArgs e)
        {
            Application.Current.MainWindow.SizeChanged += this.MainWindow_SizeChanged;
        }

        private void MainWindow_SizeChanged(object sender, SizeChangedEventArgs e)
        {
              Debug.WriteLine($"主表單size改變:{e.NewSize}");
        }
    }

 

由于Framework的Loaded和Unloaded事件都是成對出現,因此,可以保證當UserControl1被從Visual Tree卸載時,對MainWindow的SizeChanged被及時訂閱,從而解除MainWindow對UserControl1的參考,避免記憶體泄漏,

  b)弱事件模式(Weak Event Pattern):使用WeakReference或 WeakEventManager,或第三方的庫(如Prism的EventAggregator),這里只舉一個WeakReference的例子,關于后者可以參考MSDN檔案:https://docs.microsoft.com/en-us/dotnet/desktop/wpf/advanced/weak-event-patterns

還是上面的例子,用WeakReference改寫后代碼如下:

   public class WeakEventHandler<TEventArgs> where TEventArgs : SizeChangedEventArgs
    {
        public WeakReference Reference { get; }

        public MethodInfo Method { get; }

        public WeakEventHandler(SizeChangedEventHandler eventHandler)
        {
            this.Handler = eventHandler;
            this.Reference = new WeakReference(eventHandler.Target);
            Method = eventHandler.Method;
        }

        public SizeChangedEventHandler Handler { get; }

        public void Invoke(object sender, TEventArgs e)
        {
            object target = Reference.Target;
            if (null != target)
            {
                Method.Invoke(target, new object[] { sender, e });
            }
        }

        public static implicit operator SizeChangedEventHandler(WeakEventHandler<TEventArgs> weakHandler)
        {

            return weakHandler.Handler;
        }

    }


    /// <summary>
    /// Interaction logic for UserControl1.xaml
    /// </summary>
    public partial class UserControl1 : UserControl
    {
        public UserControl1()
        {
            InitializeComponent();
 
            Application.Current.MainWindow.SizeChanged += new WeakEventHandler<SizeChangedEventArgs>(this.MainWindow_SizeChanged);
 
        }

        private void MainWindow_SizeChanged(object sender, SizeChangedEventArgs e)
        {
            Debug.WriteLine($"主表單size改變:{e.NewSize}");
        }
    }

  

  c)盡可能利用匿名函式(anonymous method)并避免“捕獲”物件的任何成員(member),如上面的例子可以改為:

    public partial class UserControl1 : UserControl
    {
        public UserControl1()
        {
            InitializeComponent();
 
            Application.Current.MainWindow.SizeChanged += (s, e) =>
            {
                Debug.WriteLine($"主表單size改變:{e.NewSize}");
            };
 
        }
    }

 

2)在匿名函式中捕獲物件的成員(member)


上面提到將event hander換成匿名函式并避免捕獲物件的成員可以避免記憶體泄漏,換句話說,如果匿名函式捕獲了物件的成員,就可能導致記憶體泄漏,如上面匿名函式的例子換成下面的:

    public partial class UserControl1 : UserControl
    {
        public UserControl1()
        {
            InitializeComponent();
 
            Application.Current.MainWindow.SizeChanged += (s, e) =>
            {
           Debug.WriteLine($"{e.NewSize.Width - this.Width}");
 
            };
 
        }
    }

這里,由于UserControl1的成員Width被匿名函式捕獲,結果導致整個UserControl1的實體也被MainWindow所參考,從而產生記憶體泄漏,

這類泄漏的解決辦法可能很簡單——使用區域變數代替物件的成員:

    public partial class UserControl1 : UserControl
    {
        public UserControl1()
        {
            InitializeComponent();

            var w = this.Width;
            Application.Current.MainWindow.SizeChanged += (s, e) =>
            {
                Debug.WriteLine($"{e.NewSize.Width - w}");
            };
 
        }
    }

 

3)不正確的Binding(WPF)


  如果系結的不是DependencyProperty而且沒有實作INotifyPropertyChanged,那么將產生記憶體泄漏,這與WPF的Binding的實作機制有關:如果系結的是DependencyProperty或一個實作了INotifyPropertyChanged的物件的屬性,那么WPF將利用Weak events模式,不會產生記憶體泄漏,否則,WPF將不得不訴諸于訂閱System.ComponentModel.PropertyDescriptor類的ValueChanged事件來監聽系結source的屬性值的改變,問題在于,這將導致CLR創建一個從PropertyDescriptor到系結source物件的一個強參考,多數情況下,CLR將用一個全域串列保存這個參考,這無疑將導致記憶體泄漏,

  不過這種記憶體泄漏只有當BindingMode為OneWay或TwoWay時才會發生,當BindingMode為OneTime或OneWayToSource時,CLR不會創建強參考,即使Binding的不是DependencyProperty而且沒有實作INotifyPropertyChanged,

與此類似的是系結沒有實作INotifyCollectionChanged介面的Collection,這是WPF將創建一個到這個Collection的強參考,產生記憶體泄漏,


4)WPF中x:Name導致的記憶體泄漏


  如果Xaml的元素用x:Name進行了命名,那么WPF將創建一個到該元素的全域的強參考,例如:

     <local:UserControl1 x:Name="MyUserControl1"/>

那么用code behind動態地將MyUserControl1從其父容器移除并不能真正導致該元素可以被回收,雖然看似MyUserControl1已經被移除了,

        private void ButtonBase_OnClick(object sender, RoutedEventArgs e)
        {
             rootPanel.Children.Remove(this.MyUserControl1);  
             this.MyUserControl1 = null;
  
        }

解決辦法也很簡單:

        private void ButtonBase_OnClick(object sender, RoutedEventArgs e)
        {
            this.UnregisterName("MyUserControl1");
            rootPanel.Children.Remove(this.MyUserControl1);
            this.MyUserControl1 = null;
        }

到目前為止,我們談的都是托管記憶體(managed memory),這類記憶體是由GC管理的,非托管記憶體(unmanaged memory)則完全是另一回事事,下面簡單討論以下非托管記憶體泄漏,

 

 

 

2.非托管類記憶體泄漏(unmanaged memory leak)

下面通過一個簡單的列子來說明這個問題:

public class SomeClass
{
    private IntPtr _buffer;
 
    public SomeClass()
    {
        _buffer = Marshal.AllocHGlobal(1000);
    }
 
}

上面的物件在創建時通過Marshal.AllocHGlobal()分配了一塊非托管記憶體,在底層,AllocHGlobal()呼叫了Win32的Kernel32.dll的LocalAlloc()函式,如果沒有顯式呼叫Marshal.FreeHGlobal()來釋放這塊記憶體,那么這塊非托管類記憶體將被視為已占用,將長期停駐在堆記憶體,這正是典型的非托管類記憶體泄漏,

要解決這個問題,除了主動呼叫Marshal.FreeHGlobal(),還有一種簡單直接的方法是在析構器里呼叫該方法,如:

    public class SomeClass
    {
        private IntPtr _buffer;

        public SomeClass()
        {
            _buffer = Marshal.AllocHGlobal(1000);
            // do stuff without freeing the buffer memory
        }
        ~SomeClass()
        {
            if (this._buffer != IntPtr.Zero)
            {
                Marshal.FreeHGlobal(_buffer);
                _buffer = IntPtr.Zero;
            }
            
        }
    }

在SomeClass被回收時,Destructor必然被呼叫(除非對這個物件呼叫了GC.SuppressFinalize),進而呼叫Marshal.FreeHGlobal(),這塊非托管記憶體被回收,避免了記憶體泄漏,這意味著,只要沒有托管類記憶體泄漏導致SomeClass無法被回收,這塊非托管記憶體都能被回收,

在Constructor里分配非托管記憶體,在Destructor里釋放, 這個解決方案似乎完美無缺,但是該方案的問題有二:首先,如果SomeClass因為托管類記憶體泄漏無法被回收,那么其非托管資源將無法被釋放;其次,一個無用的托管物件何時被回收也就是說其Destructor什么時候被呼叫是不確定的,取決于GC,對于后一種情形的嚴重性,不妨考慮一個極端的例子:程式創建了大量小托管物件,而且這些物件都分配大量非托管記憶體,盡管沒有托管類記憶體泄漏,這些小托管物件都可以回收,但是由于GC只能看到托管記憶體,看不到非托管記憶體,于是它認為不需要進行垃圾回收,情況嚴重時,將導致應用占用大量記憶體,其影響不亞于記憶體泄漏,

第二個問題的解決方案是實作Dispose模式,并在物件不再使用時盡早呼叫Dispose方法,

  public class SomeClass : IDisposable
    {

        private IntPtr _buffer;

        // To detect redundant calls
        private bool _disposed = false;


        public SomeClass()
        {
            _buffer = Marshal.AllocHGlobal(1000);
        }

        ~SomeClass() => Dispose(false);

        // Public implementation of Dispose pattern callable by consumers.
        public void Dispose()
        {
            Dispose(true);

       //加上這句后,則如果已經被disposed,則在回收時不需要呼叫析構器(呼叫析構器對性能有一定影響) GC.SuppressFinalize(
this); } // Protected implementation of Dispose pattern. protected virtual void Dispose(bool disposing) { if (_disposed) { return; } if (disposing) { // TODO: dispose managed state (managed objects). } // TODO: free unmanaged resources (unmanaged objects) and override a finalizer below. // TODO: set large fields to null. if (this._buffer != IntPtr.Zero) { Marshal.FreeHGlobal(_buffer); _buffer = IntPtr.Zero; } _disposed = true; } }


(上面的Dispose模式是MSDN和Resharper都推薦的模式,其中黑體字為新增代碼)這意味著,當實作IDisposable介面的物件不再有用時,應該盡早呼叫其Dispose()方法,關于Dispose模式,這里有必要補充一點:在有些存在托管記憶體泄漏的情況下,我們不能被動依靠GC在銷毀一個垃圾物件時呼叫它的析構器呼叫Dispose,因為由于托管記憶體泄漏,這個物件可能無法被GC回收,如下面的例子:

    public class SomeClass : IDisposable
    {

        // To detect redundant calls
        private bool _disposed = false;


        public SomeClass()
        {
            Application.Current.Deactivated += this.Current_Deactivated;
        }

        private void Current_Deactivated(object sender, EventArgs e)
        {
            //do something
        }

        ~SomeClass() => Dispose(false);

        // Public implementation of Dispose pattern callable by consumers.
        public void Dispose()
        {
            Dispose(true);

            //加上這句后,則如果已經被disposed,則在回收時不需要呼叫析構器(呼叫析構器對性能有一定影響)
            GC.SuppressFinalize(this);
        }

        // Protected implementation of Dispose pattern.
        protected virtual void Dispose(bool disposing)
        {
            if (_disposed)
            {
                return;
            }

            if (disposing)
            {
                // TODO: dispose managed state (managed objects).
                Application.Current.Deactivated -= this.Current_Deactivated;
            }

            // TODO: free unmanaged resources (unmanaged objects) and override a finalizer below.
            // TODO: set large fields to null.
 

            _disposed = true;
        }
    }

由于被全域物件Application參考,在應用結束前這個物件都無法被回收的,因此其析構器也不會被呼叫,這時就必須手動呼叫其Dispose()方法,否則將產生托管記憶體泄漏,

值得一提的是,上面的例子中,由于我們必須手動呼叫Dispose(),所以也就不再需要析構器里那些代碼,也不需要判斷_disposed是否為true,因此Dispose模式在這個例子中可以大大簡化:

    public class SomeClass : IDisposable
    {
        public SomeClass()
        {
            Application.Current.Deactivated += this.Current_Deactivated;
        }

        private void Current_Deactivated(object sender, EventArgs e)
        {
            //do something
        }         

        // Public implementation of Dispose pattern callable by consumers.
        public void Dispose()
        {
            Application.Current.Deactivated += this.Current_Deactivated;
        }
 
    }

 

 

 

  總結:本文簡單討論了.NET/WPF中記憶體泄漏的型別,產生的原因,GC的作業原理,常見的記憶體泄漏場景以及相應的解決方案,正確的Dispose模式等,由于實際開發中記憶體泄漏問題的復雜性,本文未涉及的記憶體泄漏場景還有很多,如TextBox的Undo快取泄漏、Attached Behaviors等,(原創文章,感謝閱讀,歡迎批評指正,專注請注明出處)

 

參考文章:
http://dotnet.agilekiwi.com/blog/2010/04/memory-leaks-in-managed-code.html
https://michaelscodingspot.com/ways-to-cause-memory-leaks-in-dotnet
https://michaelscodingspot.com/find-fix-and-avoid-memory-leaks-in-c-net-8-best-practices/
https://stackoverflow.com/questions/18542940/can-bindings-create-memory-leaks-in-wpf/18543350#18543350
https://blog.jetbrains.com/dotnet/2014/09/04/fighting-common-wpf-memory-leaks-with-dotmemory/
https://docs.microsoft.com/en-us/dotnet/standard/garbage-collection/implementing-dispose

 

轉載請註明出處,本文鏈接:https://www.uj5u.com/net/233306.html

標籤:C#

上一篇:字串截取問題

下一篇:字串轉換注意編碼

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • WebAPI簡介

    Web體系結構: 有三個核心:資源(resource),URL(統一資源識別符號)和表示 他們的關系是這樣的:一個資源由一個URL進行標識,HTTP客戶端使用URL定位資源,表示是從資源回傳資料,媒體型別是資源回傳的資料格式。 接下來我們說下HTTP. HTTP協議的系統是一種無狀態的方式,使用請求/ ......

    uj5u.com 2020-09-09 22:07:47 more
  • asp.net core 3.1 入口:Program.cs中的Main函式

    本文分析Program.cs 中Main()函式中代碼的運行順序分析asp.net core程式的啟動,重點不是剖析原始碼,而是理清程式開始時執行的順序。到呼叫了哪些實體,哪些法方。asp.net core 3.1 的程式入口在專案Program.cs檔案里,如下。ususing System; us ......

    uj5u.com 2020-09-09 22:07:49 more
  • asp.net網站作為websocket服務端的應用該如何寫

    最近被websocket的一個問題困擾了很久,有一個需求是在web網站中搭建websocket服務。客戶端通過網頁與服務器建立連接,然后服務器根據ip給客戶端網頁發送資訊。 其實,這個需求并不難,只是剛開始對websocket的內容不太了解。上網搜索了一下,有通過asp.net core 實作的、有 ......

    uj5u.com 2020-09-09 22:08:02 more
  • ASP.NET 開源匯入匯出庫Magicodes.IE Docker中使用

    Magicodes.IE在Docker中使用 更新歷史 2019.02.13 【Nuget】版本更新到2.0.2 【匯入】修復單列匯入的Bug,單元測驗“OneColumnImporter_Test”。問題見(https://github.com/dotnetcore/Magicodes.IE/is ......

    uj5u.com 2020-09-09 22:08:05 more
  • 在webform中使用ajax

    如果你用過Asp.net webform, 說明你也算是.NET 開發的老兵了。WEBform應該是2011 2013左右,當時還用visual studio 2005、 visual studio 2008。后來基本都用的是MVC。 如果是新開發的專案,估計沒人會用webform技術。但是有些舊版 ......

    uj5u.com 2020-09-09 22:08:50 more
  • iis添加asp.net網站,訪問提示:由于擴展配置問題而無法提供您請求的

    今天在iis服務器配置asp.net網站,遇到一個問題,記錄一下: 問題:由于擴展配置問題而無法提供您請求的頁面。如果該頁面是腳本,請添加處理程式。如果應下載檔案,請添加 MIME 映射。 WindowServer2012服務器,添加角色安裝完.netframework和iis之后,運行aspx頁面 ......

    uj5u.com 2020-09-09 22:10:00 more
  • WebAPI-處理架構

    帶著問題去思考,大家好! 問題1:HTTP請求和回傳相應的HTTP回應資訊之間發生了什么? 1:首先是最底層,托管層,位于WebAPI和底層HTTP堆疊之間 2:其次是 訊息處理程式管道層,這里比如日志和快取。OWIN的參考是將訊息處理程式管道的一些功能下移到堆疊下端的OWIN中間件了。 3:控制器處理 ......

    uj5u.com 2020-09-09 22:11:13 more
  • 微信門戶開發框架-使用指導說明書

    微信門戶應用管理系統,采用基于 MVC + Bootstrap + Ajax + Enterprise Library的技術路線,界面層采用Boostrap + Metronic組合的前端框架,資料訪問層支持Oracle、SQLServer、MySQL、PostgreSQL等資料庫。框架以MVC5,... ......

    uj5u.com 2020-09-09 22:15:18 more
  • WebAPI-HTTP編程模型

    帶著問題去思考,大家好!它是什么?它包含什么?它能干什么? 訊息 HTTP編程模型的核心就是訊息抽象,表示為:HttPRequestMessage,HttpResponseMessage.用于客戶端和服務端之間交換請求和回應訊息。 HttpMethod類包含了一組靜態屬性: private stat ......

    uj5u.com 2020-09-09 22:15:23 more
  • 部署WebApi隨筆

    一、跨域 NuGet參考Microsoft.AspNet.WebApi.Cors WebApiConfig.cs中配置: // Web API 配置和服務 config.EnableCors(new EnableCorsAttribute("*", "*", "*")); 二、清除默認回傳XML格式 ......

    uj5u.com 2020-09-09 22:15:48 more
最新发布
  • C#多執行緒學習(二) 如何操縱一個執行緒

    <a href="https://www.cnblogs.com/x-zhi/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/2943582/20220801082530.png" alt="" /></...

    uj5u.com 2023-04-19 09:17:20 more
  • C#多執行緒學習(二) 如何操縱一個執行緒

    C#多執行緒學習(二) 如何操縱一個執行緒 執行緒學習第一篇:C#多執行緒學習(一) 多執行緒的相關概念 下面我們就動手來創建一個執行緒,使用Thread類創建執行緒時,只需提供執行緒入口即可。(執行緒入口使程式知道該讓這個執行緒干什么事) 在C#中,執行緒入口是通過ThreadStart代理(delegate)來提供的 ......

    uj5u.com 2023-04-19 09:16:49 more
  • 記一次 .NET某醫療器械清洗系統 卡死分析

    <a href="https://www.cnblogs.com/huangxincheng/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/214741/20200614104537.png" alt="" /&g...

    uj5u.com 2023-04-18 08:39:04 more
  • 記一次 .NET某醫療器械清洗系統 卡死分析

    一:背景 1. 講故事 前段時間協助訓練營里的一位朋友分析了一個程式卡死的問題,回過頭來看這個案例比較經典,這篇稍微整理一下供后來者少踩坑吧。 二:WinDbg 分析 1. 為什么會卡死 因為是表單程式,理所當然就是看主執行緒此時正在做什么? 可以用 ~0s ; k 看一下便知。 0:000> k # ......

    uj5u.com 2023-04-18 08:33:10 more
  • SignalR, No Connection with that ID,IIS

    <a href="https://www.cnblogs.com/smartstar/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/u36196.jpg" alt="" /></a>...

    uj5u.com 2023-03-30 17:21:52 more
  • 一次對pool的誤用導致的.net頻繁gc的診斷分析

    <a href="https://www.cnblogs.com/dotnet-diagnostic/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/3115652/20230225090434.png" alt=""...

    uj5u.com 2023-03-28 10:15:33 more
  • 一次對pool的誤用導致的.net頻繁gc的診斷分析

    <a href="https://www.cnblogs.com/dotnet-diagnostic/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/3115652/20230225090434.png" alt=""...

    uj5u.com 2023-03-28 10:13:31 more
  • C#遍歷指定檔案夾中所有檔案的3種方法

    <a href="https://www.cnblogs.com/xbhp/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/957602/20230310105611.png" alt="" /></a&...

    uj5u.com 2023-03-27 14:46:55 more
  • C#/VB.NET:如何將PDF轉為PDF/A

    <a href="https://www.cnblogs.com/Carina-baby/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/2859233/20220427162558.png" alt="" />...

    uj5u.com 2023-03-27 14:46:35 more
  • 武裝你的WEBAPI-OData聚合查詢

    <a href="https://www.cnblogs.com/podolski/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/616093/20140323000327.png" alt="" /><...

    uj5u.com 2023-03-27 14:46:16 more