1. 程式人生 > >終於弄明白了 Singleton,Transient,Scoped 的作用域是如何實現的

終於弄明白了 Singleton,Transient,Scoped 的作用域是如何實現的

## 一:背景 ### 1. 講故事 前幾天有位朋友讓我有時間分析一下 aspnetcore 中為什麼向 ServiceCollection 中注入的 Class 可以做到 Singleton,Transient,Scoped,挺有意思,這篇就來聊一聊這一話題,自從 core 中有了 ServiceCollection, 再加上流行的 DDD 模式,相信很多朋友的專案中很少能看到 new 了,好歹 spring 十幾年前就是這麼幹的。 ## 二:Singleton,Transient,Scoped 基本用法 分析原始碼之前,我覺得有必要先介紹一下它們的玩法,為方便演示,我這裡就新建一個 webapi 專案,定義一個 interface 和 concrete ,程式碼如下: ``` C# public class OrderService : IOrderService { private string guid; public OrderService() { guid = $"時間:{DateTime.Now}, guid={ Guid.NewGuid()}"; } public override string ToString() { return guid; } } public interface IOrderService { } ``` ### 1. AddSingleton 正如名字所示它可以在你的程序中保持著一個例項,也就是說僅有一次例項化,不信的話程式碼演示一下哈。 ``` C# public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddControllers(); services.AddSingleton(); } } [ApiController] [Route("[controller]")] public class WeatherForecastController : ControllerBase { IOrderService orderService1; IOrderService orderService2; public WeatherForecastController(IOrderService orderService1, IOrderService orderService2) { this.orderService1 = orderService1; this.orderService2 = orderService2; } [HttpGet] public string Get() { Debug.WriteLine($"{this.orderService1}\r\n{this.orderService2} \r\n ------"); return "helloworld"; } } ``` 接著執行起來多次重新整理頁面,如下圖: ![](https://img2020.cnblogs.com/other/214741/202009/214741-20200901093822869-1615629489.png) 可以看到,不管你怎麼重新整理頁面,guid都是一樣,說明確實是單例的。 ### 2. AddScoped 正從名字所述:Scope 就是一個作用域,那在 webapi 或者 mvc 中作用域是多大呢? 對的,就是一個請求,當然請求會穿透 Presentation, Application, Repository 等等各層,在穿層的過程中肯定會有同一個類的多次注入,那這些多次注入在這個作用域下維持的就是單例,如下程式碼所示: ``` C# public void ConfigureServices(IServiceCollection services) { services.AddControllers(); services.AddScoped(); } ``` 執行起來多次重新整理頁面,如下圖: ![](https://img2020.cnblogs.com/other/214741/202009/214741-20200901093823229-312981966.png) 很明顯的看到,每次刷 UI 的時候,guid都會變,而在同一個請求 (scope) 中 guid 是一樣的。 ### 3. AddTransient 前面大家也看到了,要麼作用域是整個程序,要麼作用域是一個請求,而這裡的 Transient 就沒有作用域概念了,注入一次 例項化一次,不信的話上程式碼給你看唄。 ``` C# public void ConfigureServices(IServiceCollection services) { services.AddControllers(); services.AddTransient(); } ``` ![](https://img2020.cnblogs.com/other/214741/202009/214741-20200901093823518-335051545.png) 從圖中可以看到,注入一次就 new 一次,非常簡單吧,當然了,各有各的應用場景。 之前不清楚的朋友到現在應該也明白了這三種作用域,接下來繼續思考的一個問題就是,這種作用域是如何做到的呢? 要想回答這個問題,只能研究原始碼了。 ## 三:原始碼分析 aspnetcore 中的 IOC 容器是 ServiceCollection,你可以向 IOC 中注入不同作用域的類,最後生成 provider,如下程式碼所示: ``` C# var services = new ServiceCollection(); services.AddSingleton(); var provider = services.BuildServiceProvider(); ``` ### 1. AddSingleton 的作用域是如何實現的 通常說到單例,大家第一反應就是 static,但是一般 ServiceCollection 中會有成百上千個 AddSingleton 型別,都是靜態變數是不可能的,既然不是 static,那就應該有一個快取字典什麼的,其實還真的有這麼一個。 #### 1)RealizedServices 字典 每一個 provider 內部都會有一個 叫做 RealizedServices 的字典,這個 字典 將會在後面充當快取存在, 如下圖: ![](https://img2020.cnblogs.com/other/214741/202009/214741-20200901093824165-1270364037.png) 從上圖中可以看到,初始化的時候這個字典什麼都沒有,接下來執行 `var orderService = provider.GetService();` 效果如下圖: ![](https://img2020.cnblogs.com/other/214741/202009/214741-20200901093824621-24741092.png) 可以看到 RealizedServices 中已經有了一個 service 記錄了,接著往下執行 `var orderService2 = provider.GetService();`,最終會進入到 `CallSiteRuntimeResolver.VisitCache` 方法判斷例項是否存在,如下圖: ![](https://img2020.cnblogs.com/other/214741/202009/214741-20200901093825486-991969508.png) 仔細看上面程式碼的這句話: `if (!resolvedServices.TryGetValue(callSite.Cache.Key, out obj))` 一旦字典存在就直接返回,否則就要執行 new 鏈路,也就是 ` this.VisitCallSiteMain`。 綜合來看,這就是為什麼可以單例的原因,如果不明白可以拿 dnspy 仔細琢磨琢磨。。。 ### 2. AddTransient 原始碼探究 前面大家也看到了,provider 裡面會有一個 DynamicServiceProviderEngine 引擎類,引擎類中用 字典快取 來解決單例問題,可想而知,AddTransient 內部肯定是沒有字典邏輯的,到底是不是呢? 除錯一下唄。 ![](https://img2020.cnblogs.com/other/214741/202009/214741-20200901093826445-98837623.png) 和單例一樣,最終解析都是由 CallSiteRuntimeResolver 負責的,AddTransient 內部會走到 VisitDisposeCache 方法,而這裡會一直走 `this.VisitCallSiteMain(transientCallSite, context)` 來進行 例項的 new 操作,還記得單例是怎麼做的嗎? 它會在這個 VisitCallSiteMain 上包一層 resolvedServices 判斷,