解析 Microsoft.Extensions.DependencyInjection 2.x 版本實現
專案使用了 Microsoft.Extensions.DependencyInjection 2.x 版本,遇到第2次請求時非常高的記憶體佔用情況,於是作了調查,本文對 3.0 版本仍然適用。
先說結論,可以轉到ServiceProvider
章節,為了在效能與開銷中獲取平衡,Microsoft.Extensions.DependencyInjection
在初次請求時使用反射例項化目標服務,再次請求時非同步使用表示式樹替換了目標例項化委託,使得後續請求將得到效能提升。
IServiceProviderEngine
依賴注入的核心是IServiceProviderEngine
,它定義了GetService()
IServiceProvider
間接呼叫。
IServiceProviderEngine
包含若干實現,由ServiceProvider
的建構函式的引數決定具體的實現型別。由於ServiceProviderOptions.Mode
是內部可見列舉,預設值為ServiceProviderMode.Dynamic
,ServiceCollectionContainerBuilderExtensions.BuildServiceProvider()
作為入口沒有控制能力,使得成員_engine
是型別為DynamicServiceProviderEngine
的例項。
最終實現類DynamicServiceProviderEngine
CompiledServiceProviderEngine
繼承,後者再從抽象類ServiceProviderEngine
繼承。
抽象類ServiceProviderEngine
定義了方法GetService(Type serviceType)
,並維護了預設可見性的執行緒安全的字典internal ConcurrentDictionary<Type, Func<ServiceProviderEngineScope, object>> RealizedServices
,目標型別例項化總是先從該字典獲取委託。
方法ServiceProviderEngine.GetService()
ServiceProviderEngine
的私有方法CreateServiceAccessor(Type serviceType)
首先使用CallSiteFactory
分析獲取待解析型別的上下文IServiceCallSite
,接著呼叫子類的RealizeService(IServiceCallSite)
實現。
ServiceProviderEngine
這裡解析兩個重要依賴CallSiteFactory
和CallSiteRuntimeResolver
,以及資料結構IServiceCallSite
,前兩者在ServiceProviderEngine
的建構函式中得到例項化。
CallSiteFactory
ServiceProviderEngine
以注入方式集合作為構建函式的引數,但引數被立即轉交給了CallSiteFactory
,後者在維護注入方式集合與了若干字典物件。
List<ServiceDescriptor> _descriptors
:所有的注入方式集合Dictionary<Type, IServiceCallSite> _callSiteCache
:目標服務型別與其實現的上下文字典Dictionary<Type, ServiceDescriptorCacheItem> _descriptorLookup
:使用目標服務型別分組後注入方式對映
ServiceDescriptorCacheItem
是維護了List<ServiceDescriptor>
的結構體,CallSiteFactory
總是使用最後一個注入方式作為目標型別的例項化依據。
IServiceCallSite
IServiceCallSite
是目標服務型別例項化的上下文,CallSiteFactory
通過方法CreateCallSite()
建立IServiceCallSite
,並通過字典_callSiteCache
進行快取。
- 首先嚐試呼叫針對普通型別的
TryCreateExact()
方法; - 如果前一步為空,接著嘗試呼叫針對泛型型別的
TryCreateOpenGeneric()
方法; - 如果前一步為空,繼續深度呼叫針對可列舉集合的
TryCreateEnumerable()
方法; TryCreateEnumerable()
內部使用了TryCreateExact()
和TryCreateOpenGeneric()
CallSiteFactory
對不同注入方式有選取優先順序,優先選取例項注入方式,其次選取委託注入方式,最後選取型別注入方式,以 TryCreateExact()
為例簡單說明:
- 對於使用單例和常量的注入方式,返回
ConstantCallSite
例項; - 對於使用委託的注入方式,返回
FactoryCallSite
例項; - 對於使用型別注入的,
CallSiteFactory
呼叫方法CreateConstructorCallSite()
;- 如果只有1個建構函式
- 無參建構函式,使用
CreateInstanceCallSite
作為例項化上下文; - 有參建構函式存,首先使用方法
CreateArgumentCallSites()
遍歷所有引數,遞迴建立各個引數的IServiceCallSite
例項,得到陣列。接著使用前一步得到的陣列作為引數, 創建出ConstructorCallSite
例項。
- 無參建構函式,使用
- 如果多於1個建構函式,檢查和選取最佳建構函式再使用前一步邏輯處理;
- 如果只有1個建構函式
- 最後新增生命週期標識
泛型、集合處理多了部分前置工作,在此略過。
如下流程圖簡要地展示了遞迴過程:
CallSiteRuntimeResolver
CallSiteRuntimeResolver
從CallSiteVisitor<ServiceProviderEngineScope, object>
繼承,被抽象類ServiceProviderEngine
依賴,被DynamicServiceProviderEngine
間接引用。
由於目標服務型別例項化上下文已經由CallSiteFactory
獲取完成,該類的工作集中於型別推斷與呼叫合適的方法例項化取目標服務。
ConstantCallSite
:獲取引用的常量;FactoryCallSite
:執行委託;CreateInstanceCallSite
:反射呼叫Activator.CreateInstance()
;ConstructorCallSite
:遞迴例項化各個引數得到陣列,接著作為引數反射呼叫ConstructorInfo.Invoke()
;
前面提到
ServiceProviderEngine
維護了字典,用於該委託的存取,後面繼續會講到。
ServiceProviderEngine.GetService()
內部使用其私有方法CreateServiceAccessor()
,傳遞CallSiteFactory
獲取到IServiceCallSite
例項到子類重寫的方法RealizeService()
,故關注點回到DynamicServiceProviderEngine
。
DynamicServiceProviderEngine
DynamicServiceProviderEngine
重寫父類方法RealizeService()
,返回了一個特殊的委託,委託內包含了對父類CompiledServiceProviderEngine
和抽象類ServiceProviderEngine
的成員變數的呼叫。
- 該委託被儲存到
ServiceProviderEngine
維護的字典; - 該委託被第1次呼叫時,使用
ServiceProviderEngine
內部型別為CallSiteRuntimeResolver
的成員完成目標服務的例項化; - 該委託被第2次呼叫時,除了第1步外,額外另起 Task 呼叫父類
CompiledServiceProviderEngine
內部型別為ExpressionResolverBuilder
成員的方法Resolve()
得到委託,替換前述的ServiceProviderEngine
維護的字典內容。
sequenceDiagram ServiceProvider->>+ServiceProviderEngine: GetService() ServiceProviderEngine->>ServiceProviderEngine: RealizedServices.GetOrAdd() ServiceProviderEngine->>ServiceProviderEngine: CreateServiceAccessor(Type serviceType) %% Func<ServiceProviderEngineScope, object> ServiceProviderEngine->>+CallSiteFactory: CreateCallSite(Type serviceType) CallSiteFactory->>-ServiceProviderEngine: IServiceCallSite note left of CallSiteFactory: context of TService ServiceProviderEngine->>+DynamicServiceProviderEngine: RealizeService(IServiceCallSite) alt Interlocked.Increment(ref callCount) == 2 DynamicServiceProviderEngine-->>CompiledServiceProviderEngine: Task.Run(() => base.RealizeService()) end DynamicServiceProviderEngine->>+CompiledServiceProviderEngine: base.RuntimeResolver.Resolve() CompiledServiceProviderEngine->>-DynamicServiceProviderEngine: Func<ServiceProviderEngineScope, object> DynamicServiceProviderEngine->>-ServiceProviderEngine: delegate ServiceProviderEngine->>ServiceProviderEngine: execute delegate with scope ServiceProviderEngine->>-ServiceProvider: TService委託的前2次執行結果總是由
ServiceProviderEngine.RuntimeResolver
返回的。
CompiledServiceProviderEngine
CompiledServiceProviderEngine
依賴了ExpressionResolverBuilder
,並操作了抽象類ServiceProviderEngine
維護的字典物件RealizedServices
。
ExpressionResolverBuilder
ExpressionResolverBuilder
從CallSiteVisitor<CallSiteExpressionBuilderContext, Expression>
繼承,正如其名是表示式樹的相關實現,其方法Build()
構建和返回型別為Func<ServiceProviderEngineScope, object>
的委託。
ExpressionResolverBuilder
和 CallSiteRuntimeResolver
一樣繼承了抽象類CallSiteVisitor<TArgument, TResult>
,所以解析出表示式樹的過程極其相似,根據 IServiceCallSite
創建出表示式樹。
ConstantCallSite
:使用Expression.Constant()
;FactoryCallSite
:使用Expression.Invocation()
;CreateInstanceCallSite
:使用Expression.New()
;ConstructorCallSite
:遞迴建立各個引數的表示式樹得到陣列,接著作為引數,使用Expression.New()
建立最終的表示式樹;
ServiceProvider
回顧整個流程可知,CallSiteFactory
、CallSiteRuntimeResolver
、ExpressionResolverBuilder
是目標服務例項化的核心實現:
CallSiteFactory
:解析和快取目標服務的例項化上下文;CallSiteRuntimeResolver
:使用反射完成目標服務的例項化;ExpressionResolverBuilder
:使用表示式樹得到目標服務的例項化的前置委託;
ServiceProvider
通過特殊的委託完成了目標服務例項化方式的替換:
- 初次呼叫
GetService()
時- 首先通過
DynamicServiceProviderEngine
返回了委託,該委託被儲存到字典RealizedServices
中; - 接著該委託被第1次執行,通過
CallSiteRuntimeResolver
完成目標服務的例項化;
- 首先通過
- 再將呼叫
GetService()
時,- 直接得到快取的委託並同樣完成目標服務的例項
- 同時通過一個額外的 Task,通過
ExpressionResolverBuilder
使用表示式樹重新生成委託,並操作字典RealizedServices
,替換初次呼叫生成委託;
- 後續呼叫
GetService()
時,字典RealizedServices
查詢到的是已經替換過的使用表示式樹生成的委託。
sequenceDiagram ServiceProviderEngine->>+DynamicServiceProviderEngine: RealizeService(IServiceCallSite) alt Interlocked.Increment(ref callCount) == 2 DynamicServiceProviderEngine->>+CompiledServiceProviderEngine: Task.Run(() => base.RealizeService(IServiceCallSite)) CompiledServiceProviderEngine->>+ExpressionResolverBuilder: Build(IServiceCallSite) note right of ExpressionResolverBuilder: ExpressionTree ExpressionResolverBuilder->>-CompiledServiceProviderEngine: func: Func<ServiceProviderEngineScope, object> CompiledServiceProviderEngine-->>ServiceProviderEngine: base.RealizedServices[callSite.ServiceType] = func note right of ServiceProviderEngine: Rewrite cache end DynamicServiceProviderEngine->>+ServiceProviderEngine: base.RuntimeResolver.Resolve(IServiceCallSite) ServiceProviderEngine->>+CallSiteRuntimeResolver: Resole(IServiceCallSite, scope) note right of CallSiteRuntimeResolver: Reflection CallSiteRuntimeResolver->>-ServiceProviderEngine: Func<ServiceProviderEngineScope, object> ServiceProviderEngine->>-DynamicServiceProviderEngine: delegate DynamicServiceProviderEngine->>-ServiceProviderEngine: delegate ServiceProviderEngine->>ServiceProviderEngine: execute delegate with scope沒有執行緒安全問題,委託一定會被替換,視表示式樹的構建完成時機。
Summary
Microsoft.Extensions.DependencyInjection 2.x 希望在開銷和效能中取得平衡,其實現方式是使用特殊委託完成委託本身的替換。``CallSiteVisitor` 是獲取例項和表示式樹的核心實現。
由於表示式建立的過程中不存在對引數的表示式樹的快取過程,故對於 A 依賴 B 的情況,如果只是獲取 A ,使得 A 的表示式樹構建完成並以委託形式快取,單獨獲取 B 仍然要完成先反射後構造表示式的流程,見 CompiledServiceProviderEngine。
掌握了 Microsoft.Extensions.DependencyInjection 2.x 的實現機制,加上對記憶體 dump 的對比,已經知道表示式樹的構建過程是產生開銷的原因,出於篇幅控制另行展開。
leoninew 原創,轉載請保留出處 www.cnblogs.com/leoni