在上一篇文章中我們主要分析了ASP.NET Core預設依賴註入容器的存儲和解析,這一篇文章主要補充一下上一篇文章忽略的一些細節:有關服務回收的問題,即服務的生命周期問題。有關源碼可以去GitHub上找到。 這次的主角就是ServiceProvider一人,所有有關生命周期的源碼幾乎都集中在Serv ...
在上一篇文章中我們主要分析了ASP.NET Core預設依賴註入容器的存儲和解析,這一篇文章主要補充一下上一篇文章忽略的一些細節:有關服務回收的問題,即服務的生命周期問題。有關源碼可以去GitHub上找到。
這次的主角就是ServiceProvider一人,所有有關生命周期的源碼幾乎都集中在ServiceProvider.cs這個文件中。
我們知道服務的生命周期由三種,分別是:
- Transient
- Scoped
- Singleton
首先給出我的結論:這三種生命周期類別本質上沒有區別,服務的生命周期都是由提供服務的容器,即ServiceProvider的生命周期決定的,一個ServiceProvider被回收之後,所有由它產生的Service也隨之被回收。由此看來,一個ServiceProvider起了一個ServiceScoped的作用,其實就是這樣,ServiceScope本質上就是一個ServiceProvider。
1 internal class ServiceScope : IServiceScope 2 { 3 //僅有一個只讀的ServiceProvider欄位 4 private readonly ServiceProvider _scopedProvider; 5 6 public ServiceScope(ServiceProvider scopedProvider) 7 { 8 _scopedProvider = scopedProvider; 9 } 10 11 public IServiceProvider ServiceProvider 12 { 13 get { return _scopedProvider; } 14 } 15 16 public void Dispose() 17 { 18 _scopedProvider.Dispose(); 19 } 20 }
所以其實也沒ServiceScope什麼事情,每一個範圍都是由ServiceProvider控制的。這麼一來,Singleton服務和Scoped服務就是一樣的,因為每一個程式都有一個最初的ServiceProvider,我們可以叫它root,或者叫它爸爸,其他的所有ServiceProvider都是由root(爸爸)創建的,自然爸爸的範圍最大,所以被爸爸創建的Scoped服務就是所謂的Singleton,因為沒有比root(爸爸)範圍更大的ServiceProvider了。假如root都被回收了,那麼整個程式就該結束了。
1 public ServiceProvider(IEnumerable<ServiceDescriptor> serviceDescriptors) 2 { 3 _root = this; 4 _table = new ServiceTable(serviceDescriptors); 5 6 _table.Add(typeof(IServiceProvider), new ServiceProviderService()); 7 _table.Add(typeof(IServiceScopeFactory), new ServiceScopeService()); 8 _table.Add(typeof(IEnumerable<>), new OpenIEnumerableService(_table)); 9 } 10 11 // This constructor is called exclusively to create a child scope from the parent 12 internal ServiceProvider(ServiceProvider parent) 13 { 14 //註意下麵這句代碼 15 _root = parent._root; 16 _table = parent._table; 17 }
上面貼出來的是ServiceProvider的兩個構造函數,註意第二個構造函數:_root欄位引用的是爸爸的根,而不是爸爸。假如ServiceProviderA(SPA)創建了SPB,而SPB創建了SPC,那麼SPC的_root欄位引用的也是SPA。也就是說,所有ServiceProvider之間不是層狀結構,不是我們熟悉的樹結構,而是一種星型結構,應用程式的第一個ServiceProvider在最中間,其他所有的ServiceProvider的_root欄位都是引用了第一個ServiceProvider,除了第一個ServiceProvider,其他的ServiceProvider都是平等的。假如SPC要創建一個Singleton類型的服務,那麼直接讓_root(也就是SPA)創建即可。
既然Singleton就是Scoped,那我們就把重點放在Scoped和Transient上。下麵是ServiceProvider中有關Scoped和Transient的源碼。
1 internal class ServiceProvider : IServiceProvider, IDisposable 2 { 3 private readonly ServiceProvider _root; 4 private readonly ServiceTable _table; 5 private bool _disposeCalled; 6 7 //Scoped模式的服務的映射,用於釋放服務實例 8 private readonly Dictionary<IService, object> _resolvedServices = new Dictionary<IService, object>(); 9 //一次性服務的Dispose列表 10 private List<IDisposable> _transientDisposables; 11 12 internal IServiceCallSite GetResolveCallSite(IService service, ISet<Type> callSiteChain) 13 { 14 IServiceCallSite serviceCallSite = service.CreateCallSite(this, callSiteChain); 15 if (service.Lifetime == ServiceLifetime.Transient) 16 { 17 return new TransientCallSite(serviceCallSite); 18 } 19 else if (service.Lifetime == ServiceLifetime.Scoped) 20 { 21 return new ScopedCallSite(service, serviceCallSite); 22 } 23 else 24 { 25 return new SingletonCallSite(service, serviceCallSite); 26 } 27 } 28 29 private class TransientCallSite : IServiceCallSite 30 { 31 private readonly IServiceCallSite _service; 32 //Involve方法是關鍵 33 public object Invoke(ServiceProvider provider) 34 { 35 //觸發並放入ServiceProvider的一次性服務釋放列表 36 return provider.CaptureDisposable(_service.Invoke(provider)); 37 } 38 //省略Build方法 39 } 40 private class ScopedCallSite : IServiceCallSite 41 { 42 private readonly IService _key; 43 private readonly IServiceCallSite _serviceCallSite; 44 //Invoke方法是關鍵,省略了其他無關的方法 45 public virtual object Invoke(ServiceProvider provider) 46 { 47 object resolved; 48 //放入ServiceProvider的Scoped服務解析列表 49 lock (provider._resolvedServices) 50 { 51 //如果ResolvedService列表中已經緩存了,就不用再創建 52 if (!provider._resolvedServices.TryGetValue(_key, out resolved)) 53 { 54 resolved = _serviceCallSite.Invoke(provider); 55 provider._resolvedServices.Add(_key, resolved); 56 } 57 } 58 return resolved; 59 } 60 }
從ServiceProvider的GetResolvedCallSite方法可以看出,當我們要解析一項服務時,先根據服務的生存周期生成不同的CallSite,不同CallSite的Invoke方法決定了ServiceProvider怎麼管理這些服務。
首先看TransientCallSite.Invoke()。裡面調用了ServiceProvider的私有方法:CaptureDisposable(),這個方法是捕捉實現了IDisposable介面的服務,如果實現了介面,就將其放入ServiceProvider的_transientDisposables欄位中。這個欄位顧名思義,是為了釋放釋放Transient類型的服務而存在的。那如果某個服務沒有實現IDisposable介面,那麼當服務結束之後ServiceProvider不會保持對它的引用,由於沒有變數對它有引用,自然會被GC回收。
再看ScopedCallSite.Invoke()。首先是在ServiceProvider的_resolvedServices欄位中查找相應的服務,如果能找到,說明之前創建過,就無須再創建了。如果還沒創建,就將其放入_resolvedServices欄位緩存,以備不時之需。貌似Scoped類型服務沒有像Transient服務那樣有專門的欄位管理Dispose,因為這不需要,_resolvedServices欄位既可以作為緩存使用,又可以供Dispose使用。
看一下ServiceProvider的Dispose方法:
public void Dispose() { lock (SyncObject) { if (_disposeCalled) { return; } _disposeCalled = true; if (_transientDisposables != null) { foreach (var disposable in _transientDisposables) { disposable.Dispose(); } _transientDisposables.Clear(); } foreach (var entry in _resolvedServices) { (entry.Value as IDisposable)?.Dispose(); } _resolvedServices.Clear(); } }
從上面的方法中可以看出,ServiceProvider對待_resolvedServices和_transientDisposables是一樣的,並不會特意將Transient的服務頻繁釋放幾次。Transient服務和Scoped服務唯一的區別就在於Transient服務在實例化之前不會去緩存欄位中查找是否已經有緩存了,如果有需要,ServiceProvider就會幫你實例化一個。所有ServiceProvider創建的服務(無論是Transient還是Scoped)都只會在ServiceProvider釋放的時候才會釋放。這會帶來一個問題:如果一個ServiceProvider長時間不Dispose,那麼如果它要解析Transient類型的服務,會占用大量的記憶體甚至造成記憶體泄漏,實例越多,對GC也有影響。由此可以想象,ASP.NET Core的第一個ServiceProvider(也就是root)是不會去解析Transient服務的。它可以創建ServiceScope,由它們的ServiceProvider去解析服務。
為了避免大量的無用的服務留在記憶體中,我們要釋放無用的服務,比如在RenderView的時候,有關Route的服務肯定已經沒用了,為此可以創建不同的ServiceScope。使用using語句可以正確的釋放無用的服務。
1 void DoSthAboutRoute() 2 { 3 using (IServiceScope serviceScope = serviceProvider.GetService<IServiceScopeFactory>().CreateScope()) 4 { 5 IService routeService = serviceScope.ServiceProvider.GetService<IRouteService>(); 6 //.... 7 } 8 }
using 語句在結束時會自動調用serviceScope.ServiceProvider.Dispose()方法,所以所有由該ServiceProvider創建的服務都會被及時的釋放掉,此時變數serviceScope已經超出了它的作用域,它會被GC標記為垃圾對象。
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
總結一下:
- ServiceProvider負責回收服務,其中_transientDisposables欄位負責transient服務,_resolvedServices欄位記錄Scoped服務,用於緩存和釋放。
- Singleton服務就是Scoped服務,只不過是應用程式的第一個ServiceProvider(root)的Scoped服務。每一個在應用程式中出現的ServiceProvider都會保持對root的引用,要解析Singleton服務時,直接交給root處理。
- ServiceProvider的Dispose方法對transientDisposables和_resolvedServices一視同仁,所以除了transient服務不會在實例化之前查詢是否有緩存之外,其他的都和Scoped服務沒區別。
- 服務的回收主要和服務的創建者ServiceProvider的回收相關(依賴於它的Dispose方法),為了避免記憶體泄漏,可以使用using 語法及時釋放服務。
- 這一點在正文中沒有提到。Dispose()不是Finalize(),就算你顯式地調用某個服務的Dispose方法,它的記憶體也不會釋放掉,因為ServiceProvider還保持這對它的引用。CLR runtime規定只有當某個對象不可達時,才會標記為垃圾對象,才會被GC回收。