主頁 > .NET開發 > 【翻譯】- ASP.NET Core 中的記憶體管理和模式

【翻譯】- ASP.NET Core 中的記憶體管理和模式

2021-01-28 06:01:41 .NET開發

記憶體管理很復雜?, 即使在像 .NET 這樣的托管框架中. 分析和理解記憶體問題也很具挑戰性.

最近 一個用戶在 ASP.NET Core 主存盤庫中 提交了一個問題 指出垃圾回收器(GC) "未運行垃圾回收", 那它就失去了存在的意義. 癥狀如提交者描述那樣, 記憶體在請求后不斷增長, 這讓他們認為問題出在 GC.

?我們試圖獲得有關此問題的更多資訊?, 了解問題出在 GC 還是應用程式本身, 但我們得到的是貢獻者提交的一系列類似行為報告: ?記憶體不斷增長?. 有了一定的線索后,我們決定把它分成多個問題,并獨立跟進. 最后,大多數問題都可以解釋為對.NET中?記憶體消耗的作業原理?存在誤解, ?但也存在如何測量的問題?.

為了幫助 .NET 開發人員更好地了解他們的應用程式,我們需要了解記憶體管理在 ASP.NET Core 中的作業方式、如何檢測記憶體相關問題以及如何防止常見錯誤.

垃圾回收在 ASP.NET Core 中如何作業

GC按段分配,其中每個段是連續的記憶體范圍. 放在其中的物件分為三代 0, 1, 2. 代決定了GC 嘗試在應用程式不再參考的托管物件上釋放記憶體的頻率 - 數字越小頻率越高.

物件根據其生存期從一代移動到另一代. 隨著物件存在周期的延長,它們會被移動到更高的代中, 并減少回收檢查次數. 生存期較短的物件 (如Web請求生命周期期間參考的物件)將始終保留在第 0 代中. 而應用程式級別的單例物件很可能移動到第1代,并最終移動到第2代.

當 ASP.NET Core 應用啟動時, GC將為初始堆段保留一些記憶體, 并在加載運行時提交其中的一小部分. 這樣做是出于性能原因,因此堆段可以位于連續記憶體中.

重要: ASP.NET Core 行程在啟動時將會預先分配大量記憶體.

顯式呼叫GC

?手動呼叫GC執行? GC.Collect(). 將觸發第2代和所有較低代回收. ?這通常僅在調查記憶體泄漏時使用?, 確保在測量前GC移除記憶體中所有懸空物件.

注意: 應用程式不應直接呼叫 GC.Collect().

?分析應用程式的記憶體使用情況?

?專用工具可幫助分析記憶體使用情況?:

  • 物件參考數量
  • ?測量 GC 對 CPU 的影響?
  • ?測量每一代使用的空間?

然而為了簡單起見,本文不會使用這些,而是呈現一些應用內實時圖表.

要深入分析,請閱讀這些文章 其中演示如何使用 Visual Studio .NET:

不使用除錯器情況下的記憶體使用情況

在 Visual Studio 中衡量記憶體使用情況

?檢測記憶體問題?

大多數時候,任務管理 中顯示的記憶體??度??量值用于了解ASP.NET應用程式記憶體量. 此值表示計算機行程使用的記憶體量, ASP.NET應用程式的生存物件和其他記憶體使用者,如本機記憶體使用情況.
此值表示ASP.NET的行程的記憶體使用量, 其中包括應用程式的活動物件和其他記憶體使用者(如本機記憶體)

看到此值無限增加是代碼中某處存在記憶體泄漏的線索,但它無法解釋它是什么. 下一節將向您介紹特定的記憶體使用模式并對其進行解釋.

?運行應用程式?

完整的源代碼在 GitHub 上提供 https://github.com/sebastienros/memoryleak

一旦應用程式啟動,應用程式顯示一些記憶體和GC統計資訊,頁面每隔一秒鐘重繪一次. 特定的API介面執行特定的記憶體分配模式.

?測驗此應用程式?, ?只需啟動它?. ?您可以看到分配的記憶體不斷增加?, 因為顯示這些統計資訊就是在分配自定義物件. ?GC 最終運行并收集它們?.

此頁顯示一個包含分配記憶體和GC集合的圖. ?圖例還顯示 CPU 使用率和吞吐量(以請求數/秒表示)?.

?圖表顯示記憶體使用情況的兩個值?:

  • Allocated(分配): 托管物件占用的記憶體量?
  • Working Set(?作業集?): 行程使用的總物理記憶體(RAM) (如任務管理器中顯示的)

瞬態物件?

以下 API 創建一個 10KB String 實體并回傳到客戶端?. 每個請求在記憶體中分配一個新物件,并在回應上寫入.

注意: .NET中字串以UTF-16編碼存盤,因此每個字符在記憶體中需要兩個位元組?.

[HttpGet("bigstring")]
public ActionResult<string> GetBigString()
{
    return new String('x', 10 * 1024);
}

下圖以相對較小的5K RPS負載生成,以便了解記憶體分配如何受到GC的影響.

?在此示例中?, ?當分配達到略高于300MB 的閾值時,GC大約每兩秒鐘收集一次0代實體?. ?作業集穩定在 500 MB 左右?, CPU使用率低.

?此圖顯示的是,在相對較低的請求吞吐量時,記憶體消耗非常穩定,達到 GC 選擇的量?.

?一旦負載增加到機器可以處理的最大吞吐量,將繪制以下圖表?.

?有一些值得注意的點?:

  • ?回收發生的頻率要大得多?, 每秒多次
  • 現在有第一代回收, 這是因為我們在同一時間內分配了更多的資源
  • ?作業集仍然穩定?

?我們看到的是,只要CPU沒有被過度利用?, ?垃圾回收可以處理大量的分配?.

Workstation GC vs. Server GC

.NET 垃圾收集器可以在兩種不同的模式下作業?, 分別為 Workstation GCServer GC. 正如名字所述, 它們針對不同的作業負載進行了優化. ASP.NET 應用默認使用Server GC 模式, 而桌面應用使用 Workstation GC 模式.

區分兩種模式的影響, 我們可以通過修改專案檔案(.csproj)中ServerGarbageCollection引數,強制Web應用使用 Workstation GC. ?這需要重新生成應用程式?.

    <ServerGarbageCollection>false</ServerGarbageCollection>

?也可以通過在已發布的應用程式的檔案 runtimeconfig.json 設定? System.GC.Server 屬性來完成.

以下是5K RPS使用Workstation GC下的記憶體使用情況.

差異是巨大的:

  • ?作業集從 500MB 到 70MB?
  • GC每秒執行多次0代回收,而不是每兩秒執行一次
  • ?GC 閾值從 300MB 到 10MB?

?在典型的 Web 服務器環境中,CPU資源比記憶體更重要?, 因此使用Server GC更合適. 然而, 某些服務器可能更適合使用Workstation GC, 例如當一個服務器托管了多個Web應用程式時,記憶體資源更加寶貴.

注意: 在單核心機器上,GC的模式總是 Workstation.

持久的參考

?即使垃圾回收器在防止記憶體增長方面做得很好?, ?如果物件由用戶代碼持續持有,? GC就沒法釋放它. ?如果此類物件使用的記憶體量不斷增加?, 這叫做托管記憶體泄漏.

以下 API 創建一個 10KB String 實體并回傳到客戶端. 不同于第一個例子的是,此實體由靜態成員參考, 這意味著它不會被回收.

private static ConcurrentBag<string> _staticStrings = new ConcurrentBag<string>();

[HttpGet("staticstring")]
public ActionResult<string> GetStaticString()
{
    var bigString = new String('x', 10 * 1024);
    _staticStrings.Add(bigString);
    return bigString;
}

這是一個典型的用戶代碼記憶體泄漏,記憶體將持續增加直到引發OutOfMemory例外導致行程崩潰.

通過此圖表上可以看到,一旦開始在這個終結點上發起請求作業集不再穩定,且不斷增加. 在此期間,隨著記憶體增加GC會嘗試呼叫第2代垃圾回收釋放記憶體, ?這成功并釋放了一些?, ?但這并沒有阻止作業集增長.

?某些方案需要無限期地保留物件參考?, 在這種情況下,緩解此問題的一種方法是使用WeakReference類,以便在記憶體壓力下仍可以回收物件上保留參考. 這是在ASP.NET Core中 IMemoryCache 的默認實作.

本機記憶體

記憶體泄漏不一定是由對托管物件的持久參考造成的. 有些.NET物件依賴本機記憶體來運行. GC無法收集此記憶體,.NET物件需要使用本機代碼釋放它.

幸運的是 .NET 提供了 IDisposable 介面讓開發人員主動釋放本機記憶體?. ?即使? Dispose() ?未及時呼叫?, 類通常在終結器運行時自動執行... ?除非類未正確實作?.

?讓我們看一下這個代碼?:

[HttpGet("fileprovider")]
public void GetFileProvider()
{
    var fp = new PhysicalFileProvider(TempPath);
    fp.Watch("*.*");
}

PhysicaFileProvider ?是托管類?, 因此所有實體將會在請求結束后回收.

?下面是連續呼叫此 API 時生成的記憶體分析.

這個圖表顯示了這個類實作的一個明顯問題, 它不斷增加記憶體使用量. ?這是一個已知問題,正在這里跟蹤? https://github.com/aspnet/Home/issues/3110

?同樣的問題很容易在用戶代碼中發生?, ?不正確地釋放類或忘記呼叫?需要釋放物件的 Dispose() 方法.

?大型物件堆?

?隨著記憶體的連續分配和釋放?, ?記憶體中可能發生碎片?. ?這是因為物件必須分配在連續的記憶體塊中所導致. 為了?緩解此問題?, ?每當垃圾回收器釋放一些記憶體?, 將嘗試進行碎片整理. 這個程序叫做 壓縮.

壓縮面臨的問題是,物件越大?, ?移動速度越慢?. 當到達一定大小后,?移動它所花費的時間使移動它不再那么有效?. 因此,GC 為這些大型物件創建一個特殊的??記憶體??區域?, 成為 大型物件堆 (LOH). 大于 85,000 bytes (非 85 KB)的物件?被放置在那里?, 不壓縮, 而且僅在2代回收時釋放. 但是當LOH滿的時候, 將會自動觸發2代垃圾回收, 這本質上是較慢的, 因為它觸發了所有其他代的回收.

?下面是一個 API,它說明了此行為?:

[HttpGet("loh/{size=85000}")]
public int GetLOH1(int size)
{
    return new byte[size].Length;
}

下圖顯示了?在最大負載下?,呼叫使用84,975位元組陣列終結點的記憶體分析

當呼叫同一個終結點,但只多了一個位元組時, i.e. 84,976 bytes (byte[]結構在實際位元組序列化的基礎上有一些開銷).

?在這兩種情況下,作業集大致相同?, 穩定 450 MB. 但需要我們注意的是,并非回收了第0代, 我們回收了第2代, 這需要更多的CPU時間,直接影響吞吐量 從 35K 到 18K RPS, ?幾乎減半?.

?這表明應避免非常大的物件?. 例如ASP.NET Core Response Caching 中間件,將快取項拆分為小于85,000位元組的塊以處理此情況.

下面是處理此行為的特定實作的一些鏈接?

  • https://github.com/aspnet/ResponseCaching/blob/c1cb7576a0b86e32aec990c22df29c780af29ca5/src/Microsoft.AspNetCore.ResponseCaching/Streams/StreamUtilities.cs#L16
  • https://github.com/aspnet/ResponseCaching/blob/c1cb7576a0b86e32aec990c22df29c780af29ca5/src/Microsoft.AspNetCore.ResponseCaching/Internal/MemoryResponseCache.cs#L55

HttpClient

?不是具體到記憶體泄漏問題,更多的是資源泄漏問題?, 但這在用戶代碼中已經出現了很多次,值得在這里提及.

有經驗的 .NET 開發者實作 IDisposable介面釋放物件或其他本機資源,如資料庫連接和檔案處理程式, ?不這樣做可能會導致記憶體泄漏? (參見前面的示例).

HttpClient例外, ?即使它實作? IDisposable, 應該重用它,而不是在每次使用后釋放.

這是一個API終結點,它在每次請求中都創建新的實體而后釋放.

[HttpGet("httpclient1")]
public async Task<int> GetHttpClient1(string url)
{
    using (var httpClient = new HttpClient())
    {
        var result = await httpClient.GetAsync(url);
        return (int)result.StatusCode;
    }
}

當給終結點施加負載后, 一些例外就會被記錄下來:

fail: Microsoft.AspNetCore.Server.Kestrel[13]
      Connection id "0HLG70PBE1CR1", Request id "0HLG70PBE1CR1:00000031": An unhandled exception was thrown by the application.
System.Net.Http.HttpRequestException: Only one usage of each socket address (protocol/network address/port) is normally permitted ---> System.Net.Sockets.SocketException: Only one usage of each socket address (protocol/network address/port) is normally permitted
   at System.Net.Http.ConnectHelper.ConnectAsync(String host, Int32 port, CancellationToken cancellationToken)

HttpClient 實體被釋放, ?實際網路連接需要一些時間才能由作業系統釋放?. 每個客戶端連接都需要自己的客戶端埠,通過不斷創建新連接,??可用埠最終被耗盡.

解決方式是像這樣重用同一個 HttpClient 實體:

private static readonly HttpClient _httpClient = new HttpClient();

[HttpGet("httpclient2")]
public async Task<int> GetHttpClient2(string url)
{
    var result = await _httpClient.GetAsync(url);
    return (int)result.StatusCode;
}

?當應用程式停止時,此實體最終將被釋放?.

這表明,可釋放的資源也不意味著需要立即釋放

?注意?: 從ASP.NET Core 2.1開始有個更好的方式處理 HttpClient實體的生命周期 https://blogs.msdn.microsoft.com/webdev/2018/02/28/asp-net-core-2-1-preview1-introducing-httpclient-factory/

物件池

在上一個例子中我們看到 我們看到了如何使HttpClient實體靜態使用,并由所有請求重用,以防止資源耗盡

類似的模式是使用物件池. 這個想法是,如果一個物件的創建是昂貴的, 我們應該重用它的實體來防止資源分配. 物件池是可跨執行緒保留和釋放的預初始化物件的集合. 物件池可以定義硬限制之類的分配規則, 預定義大小, ?或增長率?.

Nuget 包 Microsoft.Extensions.ObjectPool ?包含有助于管理此類池的類?.

?展示它是多么有效, 讓我們使用一個API終結點來實體化一個byte緩沖區, 該緩沖區在每個請求中填充亂數:

        [HttpGet("array/{size}")]
        public byte[] GetArray(int size)
        {
            var random = new Random();
            var array = new byte[size];
            random.NextBytes(array);

            return array;
        }

在一些負載下,我們看到第0代回收每秒都在進行.

優化這些代碼我們,可以使用ArrayPool<>,將位元組陣列放入物件池中. ?靜態實體在請求之間重復使用?.

此方案的特殊部分是,我們從 API 回傳一個池物件, 這意味著只要我們從方法回傳,就失去了對它的控制, 且無法釋放它. 為了解決這個問題,我們需要將陣列池封裝在可釋放物件中, 然后將此物件注冊到 HttpContext.Response.RegisterForDispose(). ?此方法將負責對目標物件呼叫? Dispose(), 所以它只有在HTTP請求完成時才被釋放.

private static ArrayPool<byte> _arrayPool = ArrayPool<byte>.Create();

private class PooledArray : IDisposable
{
    public byte[] Array { get; private set; }

    public PooledArray(int size)
    {
        Array = _arrayPool.Rent(size);
    }

    public void Dispose()
    {
        _arrayPool.Return(Array);
    }
}

[HttpGet("pooledarray/{size}")]
public byte[] GetPooledArray(int size)
{
    var pooledArray = new PooledArray(size);

    var random = new Random();
    random.NextBytes(pooledArray.Array);

    HttpContext.Response.RegisterForDispose(pooledArray);

    return pooledArray.Array;
}

以下是使用與非應用池版本相同負載的請求圖表:

?您可以看到主要差異是分配的位元組?, 并且第0代的回收也更少.

?結論?

理解垃圾回收如何與ASP.NET Core協同作業,?有助于調查記憶體壓力問題?,最終影回應用程式的性能.

應用本文中解釋的實踐應該可以防止應用程式出現記憶體泄漏的跡象.

?參考文章?

?進一步了解記憶體管理在 .NET 中的作業原理?, ?這里有一些推薦的文章?.

垃圾回收

使用并發可視化工具了解不同的GC模式

GitHub地址 memoryleak

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

標籤:.NET Core

上一篇:求助大佬:C#程式采集到的資料存到listbox中了,怎樣將ListBox中的資料匯出到Excel中

下一篇:一文說通Dotnet的委托

標籤雲
其他(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