解析 Microsoft.Extensions.DependencyInjection 2.x 版本實現
- 2019 年 11 月 1 日
- 筆記
項目使用了 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
維護的字典內容。
委託的前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
查找到的是已經替換過的使用表達式樹生成的委託。
沒有執行緒安全問題,委託一定會被替換,視表達式樹的構建完成時機。
Summary
Microsoft.Extensions.DependencyInjection 2.x 希望在開銷和性能中取得平衡,其實現方式是使用特殊委託完成委託本身的替換。“CallSiteVisitor` 是獲取實例和表示式樹的核心實現。
由於表達式創建的過程中不存在對參數的表達式樹的快取過程,故對於 A 依賴 B 的情況,如果只是獲取 A ,使得 A 的表達式樹構建完成並以委託形式快取,單獨獲取 B 仍然要完成先反射後構造表達式的流程,見 CompiledServiceProviderEngine。
掌握了 Microsoft.Extensions.DependencyInjection 2.x 的實現機制,加上對記憶體 dump 的對比,已經知道表達式樹的構建過程是產生開銷的原因,出於篇幅控制另行展開。
leoninew 原創,轉載請保留出處 www.cnblogs.com/leoninew