ASP.NET Core 源码阅读笔记(2) ---Microsoft.Extensions.DependencyInjection生命周期管理
在上一篇文章中我们主要分析了ASP.NET Core默认依赖注入容器的存储和解析,这一篇文章主要补充一下上一篇文章忽略的一些细节:有关服务回收的问题,即服务的生命周期问题。有关源码可以去GitHub上找到。
这次的主角就是ServiceProvider一人,所有有关生命周期的源码几乎都集中在ServiceProvider.cs这个文件中。
我们知道服务的生命周期由三种,分别是:
- Transient
- Scoped
- Singleton
首先给出我的结论:这三种生命周期类别本质上没有区别,服务的生命周期都是由提供服务的容器,即ServiceProvider的生命周期决定的,一个ServiceProvider被回收之后,所有由它产生的Service也随之被回收。由此看来,一个ServiceProvider起了一个ServiceScoped的作用,其实就是这样,ServiceScope本质上就是一个ServiceProvider。
internal class ServiceScope : IServiceScope
{
//仅有一个只读的ServiceProvider字段
private readonly ServiceProvider _scopedProvider; public ServiceScope(ServiceProvider scopedProvider)
{
_scopedProvider = scopedProvider;
} public IServiceProvider ServiceProvider
{
get { return _scopedProvider; }
} public void Dispose()
{
_scopedProvider.Dispose();
}
}
所以其实也没ServiceScope什么事情,每一个范围都是由ServiceProvider控制的。这么一来,Singleton服务和Scoped服务就是一样的,因为每一个程序都有一个最初的ServiceProvider,我们可以叫它root,或者叫它爸爸,其他的所有ServiceProvider都是由root(爸爸)创建的,自然爸爸的范围最大,所以被爸爸创建的Scoped服务就是所谓的Singleton,因为没有比root(爸爸)范围更大的ServiceProvider了。假如root都被回收了,那么整个程序就该结束了。
public ServiceProvider(IEnumerable<ServiceDescriptor> serviceDescriptors)
{
_root = this;
_table = new ServiceTable(serviceDescriptors); _table.Add(typeof(IServiceProvider), new ServiceProviderService());
_table.Add(typeof(IServiceScopeFactory), new ServiceScopeService());
_table.Add(typeof(IEnumerable<>), new OpenIEnumerableService(_table));
} // This constructor is called exclusively to create a child scope from the parent
internal ServiceProvider(ServiceProvider parent)
{
//注意下面这句代码
_root = parent._root;
_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的源码。
internal class ServiceProvider : IServiceProvider, IDisposable
{
private readonly ServiceProvider _root;
private readonly ServiceTable _table;
private bool _disposeCalled; //Scoped模式的服务的映射,用于释放服务实例
private readonly Dictionary<IService, object> _resolvedServices = new Dictionary<IService, object>();
//一次性服务的Dispose列表
private List<IDisposable> _transientDisposables; internal IServiceCallSite GetResolveCallSite(IService service, ISet<Type> callSiteChain)
{
IServiceCallSite serviceCallSite = service.CreateCallSite(this, callSiteChain);
if (service.Lifetime == ServiceLifetime.Transient)
{
return new TransientCallSite(serviceCallSite);
}
else if (service.Lifetime == ServiceLifetime.Scoped)
{
return new ScopedCallSite(service, serviceCallSite);
}
else
{
return new SingletonCallSite(service, serviceCallSite);
}
} private class TransientCallSite : IServiceCallSite
{
private readonly IServiceCallSite _service;
//Involve方法是关键
public object Invoke(ServiceProvider provider)
{
//触发并放入ServiceProvider的一次性服务释放列表
return provider.CaptureDisposable(_service.Invoke(provider));
}
//省略Build方法
}
private class ScopedCallSite : IServiceCallSite
{
private readonly IService _key;
private readonly IServiceCallSite _serviceCallSite;
//Invoke方法是关键,省略了其他无关的方法
public virtual object Invoke(ServiceProvider provider)
{
object resolved;
//放入ServiceProvider的Scoped服务解析列表
lock (provider._resolvedServices)
{
//如果ResolvedService列表中已经缓存了,就不用再创建
if (!provider._resolvedServices.TryGetValue(_key, out resolved))
{
resolved = _serviceCallSite.Invoke(provider);
provider._resolvedServices.Add(_key, resolved);
}
}
return resolved;
}
}
从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)是不会去解析实现了IDisposable接口的Transient服务的(会被root引用)。它可以创建ServiceScope,由它们的ServiceProvider去解析服务。
为了避免大量的无用的服务留在内存中,我们要释放无用的服务,比如在RenderView的时候,有关Route的服务肯定已经没用了,为此可以创建不同的ServiceScope。使用using语句可以正确的释放无用的服务。
void DoSthAboutRoute()
{
using (IServiceScope serviceScope = serviceProvider.GetService<IServiceScopeFactory>().CreateScope())
{
IService routeService = serviceScope.ServiceProvider.GetService<IRouteService>();
//....
}
}
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回收。
ASP.NET Core 源码阅读笔记(2) ---Microsoft.Extensions.DependencyInjection生命周期管理的更多相关文章
- ASP.NET Core 源码阅读笔记(1) ---Microsoft.Extensions.DependencyInjection
这篇随笔主要记录一下ASP.NET Core团队实现默认的依赖注入容器的过程,我的理解可能并不是正确的. DependencyInjection这个项目不大,但却是整个ASP.NET Core的基础, ...
- ASP.NET Core 源码阅读笔记(5) ---Microsoft.AspNetCore.Routing路由
这篇随笔讲讲路由功能,主要内容在项目Microsoft.AspNetCore.Routing中,可以在GitHub上找到,Routing项目地址. 路由功能是大家都很熟悉的功能,使用起来也十分简单,从 ...
- ASP.NET Core 源码阅读笔记(3) ---Microsoft.AspNetCore.Hosting
有关Hosting的基础知识 Hosting是一个非常重要,但又很难翻译成中文的概念.翻译成:寄宿,大概能勉强地传达它的意思.我们知道,有一些病毒离开了活体之后就会死亡,我们把那些活体称为病毒的宿主. ...
- springMVC源码阅读-通过画图理解一个请求生命周期(十二)
- CI框架源码阅读笔记5 基准测试 BenchMark.php
上一篇博客(CI框架源码阅读笔记4 引导文件CodeIgniter.php)中,我们已经看到:CI中核心流程的核心功能都是由不同的组件来完成的.这些组件类似于一个一个单独的模块,不同的模块完成不同的功 ...
- CI框架源码阅读笔记4 引导文件CodeIgniter.php
到了这里,终于进入CI框架的核心了.既然是“引导”文件,那么就是对用户的请求.参数等做相应的导向,让用户请求和数据流按照正确的线路各就各位.例如,用户的请求url: http://you.host.c ...
- CI框架源码阅读笔记3 全局函数Common.php
从本篇开始,将深入CI框架的内部,一步步去探索这个框架的实现.结构和设计. Common.php文件定义了一系列的全局函数(一般来说,全局函数具有最高的加载优先权,因此大多数的框架中BootStrap ...
- CI框架源码阅读笔记2 一切的入口 index.php
上一节(CI框架源码阅读笔记1 - 环境准备.基本术语和框架流程)中,我们提到了CI框架的基本流程,这里再次贴出流程图,以备参考: 作为CI框架的入口文件,源码阅读,自然由此开始.在源码阅读的过程中, ...
- Three.js源码阅读笔记-5
Core::Ray 该类用来表示空间中的“射线”,主要用来进行碰撞检测. THREE.Ray = function ( origin, direction ) { this.origin = ( or ...
随机推荐
- JAVA的包装类 【转】
Java语言是一个面向对象的语言,但是Java中的基本数据类型却是不面向对象的,这在实际使用时存在很多的不便,为了解决这个不足,在设计类时为每个基本数据类型设计了一个对应的类进行代表,这样八个和基本数 ...
- 第五百八十二天 how can I 坚持
好吧,是我错了,昨天,做好自己就行了,别人怎么样是别人的事,永远保持一颗单纯向上的心. 时间过得真快,明天又周六了.. 睡觉.
- oracle中SQL根据生日日期查询年龄的方法
方法:SELECT Trunc(MONTHS_BETWEEN(SYSDATE,BIRTH_DATE)/12) FROM 某表 Trunc函数在这里对带有小数位数的数字取整数部分: SYSDATE为or ...
- C# 接口应用及意义
写在前面:新手入行,读者勉强看看吧,写的不对的欢迎讨论,板砖轻拍! 一.定义 接口描述的是可属于任何类或结构的一组相关功能,所以实现接口的类或结构必须实现接口定义中指定的接口成员. 通常用Interf ...
- {Reship}Precision, Accuracy & Recall
============================================================== This aritcle came from here ========= ...
- CSS布局--浮动与清除
浮动和清除 浮动和清除是页面布局的重要属性.浮动的意思是指将元素从常规的文档流中取出来. 当你浮动一个元素的时候,浮动的元素会被浏览器尽量的往上放,能放多高就放多高,一直到某个元素的边界为止. 浮动元 ...
- jquery 根据年 月设置报表表头
function setTblHeadr(thisTime){ $("#datatable_ajax1 thead").empty(); //获取星期 var weekday=ne ...
- Mosquitto-Ubuntu 14.04快速安装问题解决
Mosquitto是一个轻量级的MQTT Broker,支持很多种系统. 下载与安装:http://mosquitto.org/download/ 注意:由于客户端paho工程进展较快,目前需要使用最 ...
- iOS 设置button文字过长而显示省略号的解决办法
UIButton * button =[UIButton buttonWithType:UIButtonTypeCustom];button.titleLabel.adjustsFontSizeToF ...
- 《CSS3秘籍》(第三版)-读书笔记(2)
第6章 文本格式化 1. 使用字体 字体font-family: 通用的字体样式: serif字体最适用于冗长的文字信息.这种字体使字母主笔画的结尾处会有一些细小的“足”. sans-serif字体 ...