前言
項目使用了 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
