Microsoft.Extensions.DependencyInjection中(下面簡稱DI)的Transient依賴注入關系,表示每次DI獲取一個全新的注入對象。但是使用Transient依賴注入關系時,最好要配合IServiceScope來一起使用,因為通過Transient依賴注入關系創建的對象,都會被創建它的ServiceProvider對象內部引用,這樣會造成注入對象無法被GC及時回收,造成內存泄漏,只有當調用ServiceProvider對象的Dispose方法后,ServiceProvider才會解除其內部對注入對象的引用,之后這些注入對象才能被GC回收。
我們新建一個.NET Core控制台項目,然后假設我們有接口IPeople和實現類People,他們之間的依賴注入關系是Transient。
現在,如果我們的代碼中有一個for循環,它會循環1000次,每一次都會從DI中獲取一個IPeople對象實例,由於接口IPeople和類People是Transient關系,所以每次DI都會創建一個新的People對象實例。但是我們只需要在每次循環中,調用People類的DoSomething方法做一些事情后,就不需要創建的People對象了,也就是說我們希望每次循環結束后,GC都能盡量回收在循環中創建的People對象實例。
所以我們寫了下面的代碼:
using Microsoft.Extensions.DependencyInjection; using System; namespace NetCoreDITransientInScope { interface IPeople { void DoSomething(); } class People : IPeople { public void DoSomething() { Console.WriteLine("DoSomething is running"); } } class Program { static void Main(string[] args) { IServiceCollection services = new ServiceCollection(); services.AddTransient<IPeople, People>();//注冊接口IPeople和類People的關系為Transient using (ServiceProvider rootServiceProvider = services.BuildServiceProvider()) { //執行1000次循環,每一次循環創建一個People對象實例,在rootServiceProvider調用Dispose方法前,創建的1000個People對象實例都不會被GC回收 for (int i = 0; i < 1000; i++) { IPeople people = rootServiceProvider.GetService<IPeople>(); people.DoSomething(); //在每次循環結束后,創建的People對象實例無法被GC回收,因為在rootServiceProvider的內部對所有創建的Transient對象都保持了引用,除非調用rootServiceProvider的Dispose方法,否則在每次循環中創建的People對象實例都無法被GC回收 } } Console.WriteLine("Press any key to end..."); Console.ReadKey(); } } }
從上面代碼的注釋中,我們可以看到,實際上每一次for循環執行完后,GC並不能立即回收在循環中創建的People對象實例,原因是ServiceProvider對象rootServiceProvider的內部引用了由DI創建的所有People對象實例,除非調用rootServiceProvider的Dispose方法(也就是在上面using代碼塊最后),否則所有的People對象實例都無法被GC回收。設想一下,如果將上面的for循環改為一個死循環(對於有些后台服務程序而言,的確需要死循環),那么DI會創建大量的People對象實例無法被GC及時回收,造成內存泄漏。
所以正確使用Transient依賴注入關系的方法應該是,配合IServiceScope對象來使用,我們將上面的代碼改為如下:
using Microsoft.Extensions.DependencyInjection; using System; namespace NetCoreDITransientInScope { interface IPeople { void DoSomething(); } class People : IPeople { public void DoSomething() { Console.WriteLine("DoSomething is running"); } } class Program { static void Main(string[] args) { IServiceCollection services = new ServiceCollection(); services.AddTransient<IPeople, People>();//注冊接口IPeople和類People的關系為Transient using (ServiceProvider rootServiceProvider = services.BuildServiceProvider()) { //執行1000次循環,每一次循環創建一個People對象實例 for (int i = 0; i < 1000; i++) { //在每一次循環中創建一個IServiceScope對象serviceScope,然后使用serviceScope的ServiceProvider創建People對象實例,這樣在rootServiceProvider中並沒有對People對象實例的引用,只有每次循環中創建的serviceScope中的ServiceProvider保持了People對象實例的引用,這樣在每次循環中當調用serviceScope的Dispose方法后,單次循環中創建的People對象實例就可以被GC回收了,而不是等到rootServiceProvider調用Dispose方法后,才能被GC回收 using (IServiceScope serviceScope = rootServiceProvider.CreateScope()) { IPeople people = serviceScope.ServiceProvider.GetService<IPeople>(); people.DoSomething(); } } } Console.WriteLine("Press any key to end..."); Console.ReadKey(); } } }
從上面代碼中,我們可以看到,由於現在在每次for循環中,是由一個獨立的IServiceScope對象serviceScope的ServiceProvider,來創建People對象實例,所以在for循環外面的ServiceProvider對象rootServiceProvider,其並沒有內部引用由DI創建的People對象實例。而在每次for循環中,我們都調用了serviceScope的Dispose方法(也就是在上面第二個using代碼塊最后),這樣每次循環結束后,就沒有任何代碼引用循環內的People對象實例了,GC就可以及時回收由DI創建的People對象實例。
在使用Microsoft.Extensions.DependencyInjection的Transient依賴注入關系時,一定要注意本文所述的內存泄漏問題,這個問題可能很多才開始接觸Microsoft.Extensions.DependencyInjection的開發人員不會注意到,但是它會嚴重影響你的程序性能和穩定性。可以參考下面兩篇帖子中發帖人提出的問題:
IServiceProvider garbage collection / disposal
When are .NET Core dependency injected instances disposed?
也可以參考在GitHub上,微軟官方對這個問題的討論:
Revisit tracking transient services for disposal