1. 程式人生 > 實用技巧 >跟我一起學.NetCore之依賴注入作用域和物件釋放

跟我一起學.NetCore之依賴注入作用域和物件釋放

前言

上一小節簡單闡述了依賴注入及Asp.NetCore中自帶依賴注入元件的常規用法,其中提到容器管控了自己建立物件的生命週期,包含了三種生命週期:Singleton、Scoped、Transient,對於Singleton、Transient相對於Scoped來說比較好理解,其實這裡面有一個作用域的概念,也可以理解為根容器和子容器的範圍;上一小節中有一個例子中說到,當注入的生命週期為Scoped的時,在同一個請求內,注入的物件都是同一個,這裡Asp.NetCore將每個請求作為了一個作用域,在此作用域內,生命週期為Scoped的物件就是同一個;下面簡單說說作用域和物件釋放的常規知識點;

正文

作用域這裡可以理解為服務範圍,由IServiceScope承載,如其原始碼所示,每一個服務範圍內都一個根容器,如下所示:

// 摘要:
    //     /// The System.IDisposable.Dispose method ends the scope lifetime. Once Dispose
    //     /// is called, any scoped services that have been resolved from /// Microsoft.Extensions.DependencyInjection.IServiceScope.ServiceProvider
    
// will be /// disposed. /// public interface IServiceScope : Object, IDisposable { // IServiceProvider 即代表容器 // // 摘要: // /// The System.IServiceProvider used to resolve dependencies from the scope. // /// IServiceProvider ServiceProvider {
get; } }

每一個服務範圍內可以通過自身IServiceProvider建立對應的子作用域,而任何一個子作用域的IServiceProvider都有對根容器的引用,如下圖結構:

每一個IServiceProvider中都會將其建立的物件存入列表中,針對於繼承與IDisposable介面的型別物件會單獨存放在一個列表中,當作用域IServiceScope物件的Dispose方法被呼叫時,最終對應IServicePorvider對應的物件列表也會清空,針對於繼承與IDisposable型別的物件會呼叫其Dispose方法;最後導致對應作用域下容器建立的物件成為垃圾物件,被GC給回收;

在Asp.NetCore中,框架預設將其分有根作用域(與引用程式同生命週期)和請求作用域(每一個請求一個作用域),通常也會稱其為根容器和請求容器,名稱分別為ApplicationServices和RequestServices,在程式中獲取方式如下:

  • 通過IApplicationBuilder物件可以獲取ApplicationServices

  • 通過HttpContext.RequestServices獲取RequestServices

作用域簡單舉例,在請求作用域下再建立子作用域,分別檢視對應不同生命週期物件是否一致,新建一個WebApi專案,針對三個生命週期添加了對應檔案,結構如下:

對應紅框內的檔案內容如下:

細心的小夥伴可能會看到,每個實現類裡面都繼承了IDisposable介面,這主要是後面顯示釋放用的,這裡先不管;編輯好以上內容之後就將其進行註冊,如下:

接下來就是使用了,這裡通過這Action中使用,在當前的請求作用域下建立子作用域,對比每一個生命週期物件,如程式碼所示:

[HttpGet]
public string Get([FromServices]ITestSingleton testSingleton, [FromServices]ITestSingleton testSingleton1,
                  [FromServices]ITestScoped testScoped, [FromServices]ITestScoped testScoped1,
                  [FromServices]ITestTransient testTransient, [FromServices]ITestTransient testTransient1)
{
    //獲取請求作用域(請求容器)
    var requestServices = HttpContext.RequestServices;
    //在請求作用域下建立子作用域
    using(IServiceScope scope = requestServices.CreateScope())
    {
        //在子作用域中通過其容器獲取注入的不同生命週期物件
        ITestSingleton testSingleton11 = scope.ServiceProvider.GetService<ITestSingleton>();
        ITestScoped testScoped11 = scope.ServiceProvider.GetService<ITestScoped>();
        ITestTransient testTransient11 = scope.ServiceProvider.GetService<ITestTransient>();
​
        ITestSingleton testSingleton12 = scope.ServiceProvider.GetService<ITestSingleton>();
        ITestScoped testScoped12 = scope.ServiceProvider.GetService<ITestScoped>();
        ITestTransient testTransient12 = scope.ServiceProvider.GetService<ITestTransient>();
        Console.WriteLine("================Singleton=============");
        Console.WriteLine($"請求作用域的ITestSingleton物件:{testSingleton.GetHashCode()}");
        Console.WriteLine($"請求作用域的ITestSingleton1物件:{testSingleton1.GetHashCode()}");
        Console.WriteLine($"請求作用域下子作用域的ITestSingleton11物件:{testSingleton11.GetHashCode()}");
        Console.WriteLine($"請求作用域下子作用域的ITestSingleton12物件:{testSingleton12.GetHashCode()}");
        Console.WriteLine("================Scoped=============");
        Console.WriteLine($"請求作用域的ITestScoped物件:{testScoped.GetHashCode()}");
        Console.WriteLine($"請求作用域的ITestScoped1物件:{testScoped1.GetHashCode()}");
        Console.WriteLine($"請求作用域下子作用域的ITestScoped11物件:{testScoped11.GetHashCode()}");
        Console.WriteLine($"請求作用域下子作用域的ITestScoped12物件:{testScoped12.GetHashCode()}");
        Console.WriteLine("================Transient=============");
        Console.WriteLine($"請求作用域的ITestTransient物件:{testTransient.GetHashCode()}");
        Console.WriteLine($"請求作用域的ITestTransient1物件:{testTransient1.GetHashCode()}");
        Console.WriteLine($"請求作用域下子作用域的ITestTransient11物件:{testTransient11.GetHashCode()}");
        Console.WriteLine($"請求作用域下子作用域的ITestTransient12物件:{testTransient12.GetHashCode()}");
    }
​
    return "TestServiceScope";
​
}

執行,進行請求,看列印結果:

  • 對於Singleton來說始終不變,因為其是跟隨根容器生命週期,引用程式退出才釋放;

  • 對於Scoped來說只要在自己的作用域內就是單例的;

  • 對於Transient來說始終建立;

以上一直在說釋放,下面利用繼承IDisposable介面釋放時會呼叫對應Dispoable方法的原理簡單演示各個生命週期的釋放時機;在Controller中增加幾個Action方法,如下:

這裡使用IHostApplicationLifetime中的StopApplication模擬關閉程式釋放單例下的物件;執行看效果:

使用坑:不要從根容器中獲取Transient生命週期的物件,因為通過根容器建立的物件不會回收,除非等到應用程式退出,這樣會導致記憶體洩露;如下演示:

新增Action方法:

執行,傳送請求看結果:

總結

作用域及物件釋放就簡單說這麼多,容器只管理自己創建出來的物件生命週期;下一節說說使用第三方元件擴充套件依賴注入功能;