1. 什么是Assembly?
在C#中,Assembly是.NET框架的一个基本构建模块。它可以被看作是一个包含代码和资源的可部署单元,通常以DLL或EXE文件的形式存在。Assembly承载了以下几个关键特性:
- 代码封装:Assembly将相关的代码和资源进行封装,是代码逻辑和资源的集合。
- 版本控制:每个Assembly都有一个版本号,这对于管理应用程序的不同版本非常重要。
- 安全性:Assembly包含安全身份信息,例如强名称签名,可以确保代码的完整性和来源。
- 类型信息:Assembly包含元数据,描述了其内部类型和成员,可以被其他代码使用。
- 可再分发性:通过将功能模块化,Assembly可以在不同应用程序之间共享和重用。
- 依赖管理:Assembly提供了依赖关系的管理,确保应用程序能够正确加载和使用所需的组件。
Assembly分为两种类型:
- 私有Assembly:仅供单个应用程序使用,通常存放在应用程序的目录中。
- 共享Assembly:可以被多个应用程序使用,通常存放在全局程序集缓存(GAC)中。
GAC是什么?
GAC,全称为全局程序集缓存(Global Assembly Cache),是.NET框架提供的一个用于存储共享Assembly的特殊文件夹。GAC的主要作用是允许多个应用程序共享使用公共的Assembly,实现代码重用和版本管理。以下是GAC的一些重要特点:
- 共享使用:Assembly存放在GAC中后,可以被多个应用程序引用和使用,避免了重复存储和部署。
- 版本控制:GAC支持不同版本的同一个Assembly共存,这使得应用程序可以使用不同版本的组件而不产生冲突。
- 安全性:只有具有强名称签名的Assembly才能存放在GAC中,强名称签名确保了Assembly的唯一性和完整性。
- 管理工具:可以使用命令行工具如
gacutil
来安装或卸载GAC中的Assembly。
2. 使用场景是什么?
Assembly在C#和.NET开发中有多种使用场景,包括:
- 模块化开发:将应用程序分解为多个功能模块,每个模块作为一个独立的Assembly开发和维护。
- 代码重用:将通用功能封装成Assembly,以便在不同项目中共享和重用。
- 插件架构:使用Assembly实现插件系统,允许动态加载和执行外部组件。
- 版本管理:通过Assembly的版本控制机制,支持应用程序的平滑升级和不同版本共存。
- 分布式应用:在分布式系统中,将不同服务或组件打包为Assembly,便于部署和管理。
- 安全性要求:使用强名称签名的Assembly,确保代码的完整性和来源可信。
- 跨语言互操作:通过Assembly提供的元数据支持,允许不同.NET语言之间的互操作。
3. Assembly和AppDomain有什么区别?
在C#和.NET中,Assembly和AppDomain是两个不同的概念,各自承担不同的角色:
Assembly
基本构建单元:Assembly是.NET应用程序的基本构建模块,包含代码和资源,通常以DLL或EXE文件形式存在。
模块化和重用:Assembly用于模块化开发和代码重用,可以被多个应用程序共享。
版本和安全:支持版本管理和强名称签名,确保代码的完整性和来源可信。
类型信息:包含元数据,描述类型和成员信息,支持反射。
AppDomain
应用程序隔离:AppDomain是.NET中用于隔离应用程序的执行环境,提供了一个轻量级的进程内隔离机制。
安全和稳定:在不同AppDomain中运行的代码是相互隔离的,防止错误和崩溃的传播,提高应用程序的稳定性和安全性。
动态加载和卸载:允许在运行时动态加载和卸载Assembly,不需要重启整个应用程序。
跨域通信:AppDomain之间可以通过序列化和远程处理进行通信。
区别
- 作用域:Assembly是代码和资源的物理单位,而AppDomain是逻辑的执行环境。
- 用途:Assembly用于模块化和重用,AppDomain用于隔离和管理执行。
- 隔离性:AppDomain提供代码执行的隔离,而Assembly在加载后共享到AppDomain中。
4. Assembly.Load和AppDomain.Load有什么区别?
System.AppDomain 提供了 Load方法。和Assembly 的静态Load 方法不同,AppDomaim的Load是实例方法,它允许将程序集加载到指定的AppDomain 中。该方法设计由非托管代码调用,允许宿主将程序集“注入”特定 AppDomain 中。托管代码的开发人员一般情况下不应调用它,因为调用 AppDomaim 的Load 方法时需要传递一个标识了程序集的字符串。该方法随后会应用策略,并在一些常规位置搜索程序集。我们知道,AppDomain 关联了一些告诉 CLR如何查找程序集的设置。为了加载这个程序集,CLR 将使用与指定AppDomain 关联的设置,而非与发出调用之AppDomain 关联的设置。但AppDomain 的 Load 方法会返回对程序集的引用。由于System.Assembly类不是从System.MarshalByRefObject派生的,所以程序集对象必须按值封送回发出调用的那个AppDomain。但是,现在CLR就会用发出调用的那个 AppDomain 的设置来定位并加载程序集。如果使用发出调用的那个 AppDomain 的策略和搜索位置找不到指定的程序集,就会抛出一个 FileNotFoundException。这个行为一般不是你所期望的,所以应该避免使用 AppDomain 的 Load 方法。
一台机器可能同时存在具有相同标识的多个程序集。由于重要提示LoadFrom会在内部调用 Load,所以CLR有可能不是加载你指定的文件而是加载一个不同的文件,从而造成非预期的行为。强烈建议每次生成程序集时都更改版本号,确保每个版本都有自己的唯一性标识,确保LoadFrom方法的行为符合预期。除此之外Assembly.Load
和AppDomain.Load
用于加载程序集,但它们的使用场景和行为有所不同:
Assembly.Load
- 作用域:在当前应用程序域(AppDomain)中加载程序集。
- 用法:通常用于在运行时加载程序集,适用于大多数动态加载需求。
- 返回值:返回一个
Assembly
对象,表示已加载的程序集的引用。 - 限制:无法跨应用程序域加载程序集,仅限于当前AppDomain。
AppDomain.Load
- 用法:常用于需要在特定AppDomain中加载程序集的场景。
- 返回值:同样返回一个
Assembly
对象,但是在指定的AppDomain中加载。 - 跨域加载:允许在不同的AppDomain中加载程序集,实现更好的隔离。
区别
- 加载位置:
Assembly.Load
在当前AppDomain加载,而AppDomain.Load
可以指定AppDomain。 - 隔离性:
AppDomain.Load
提供了更好的隔离,可以在不同的应用程序域中加载程序集。 - 使用场景:
Assembly.Load
适用于简单的动态加载,AppDomain.Load
适用于需要隔离和管理的复杂场景。
什么是System.MarshalByRefObject对象?
System.MarshalByRefObject
是 .NET 框架中的一个基类,允许对象通过引用在应用程序域(AppDomain)之间进行通信。它的主要作用是在跨域场景中支持对象的远程访问。
关键点:
- .NET 中的应用程序域类似于轻量级的进程,用于隔离应用程序。
- 每个应用程序域都有自己的内存空间和资源,防止不同域之间的直接访问。
- 默认情况下,对象在不同的应用程序域之间传递时是通过序列化(Marshal-by-Value)进行的。
- 继承自
MarshalByRefObject
的对象,可以通过引用进行传递,这意味着对象本身并不会被复制到目标域,而是通过代理进行访问。
- 适用于需要在不同应用程序域或不同计算机之间进行通信的场景。
- 继承
MarshalByRefObject
的对象通常会有一个有限的生命周期,由远程调用的服务端来管理。 - 可以通过覆盖
InitializeLifetimeService
方法来控制对象的生存时间。
使用场景:
5. CLR为什么不提供卸载?
CLR不提供卸载单独程序集的能力。如果 CLR 允许这样做,那么一旦线程从某个方法返回至已卸载的一个程序集中的代码,应用程序就会崩溃。健壮性和安全性是CLR最优先考虑的目标,如果允许应用程序以这样的一种方式崩溃,就和它的设计初衷背道而驰了。卸载程序集必须卸载包含它的整个AppDomain。使用 ReflectionOnlyLoadFrom或ReflectionOnlyLoad 方法加载的程序集表面上是可以卸载的。毕竟,这些程序集中的代码是不允许执行的。但CLR 一样不允许卸载用这两个方法加载的程序集。因为用这两个方法加载了程序集之后,仍然可以利用反射来创建对象,以便引用这些程序集中定义的元数据。如果卸载程序集,就必须通过某种方式使这些对象失效。无论是实现的复杂性,还是执行速度,跟踪这些对象的状态都是得不偿失的。
总结:
不提供直接卸载单个程序集的功能,主要有以下几个原因:
- 内存管理复杂性:卸载单个程序集会增加内存管理的复杂性,可能导致内存泄漏或其他资源管理问题。
- 依赖关系:程序集之间可能存在复杂的依赖关系,卸载一个程序集可能会影响其他程序集的正常运行。
- 应用程序稳定性:为了确保应用程序的稳定性和一致性,CLR选择不支持单个程序集的卸载。
- 替代方案:CLR提供了应用程序域(AppDomain)作为隔离和管理程序集的机制。可以卸载整个AppDomain,从而释放相关的程序集和资源。
6. 反射的性能
太多文章讲解反射的好处和使用这里就不说了直接来看缺点是什么,原因有哪些。
缺点:
- 反射造成编译时无法保证类型安全性。由于反射严重依赖字符串,所以会丧失编译时类型安全性。例如,执行 Type.GetType(“int”);要求通过反射在程序集中查找名为“int”的类型,代码会通过编译,但在运行时会返回null,因为CLR只知道System.Int32不知int。
- 反射速度慢。使用反射时,类型及其成员的名称在编译时未知;你要用字符电名称标识每个类型及其成员,然后在运行时发现它们。也就是说,使用System.Reflection命名空间中的类型扫描程序集的元数据时,反射机制会不停地执行字符串搜索。通常,字符串搜索执行的是不区分大小写的比较,这会进一步影响速度。
使用反射调用成员也会影响性能。用反射调用方法时,首先必须将实参打包(pack)成数组:在内部,反射必须将这些实参解包(unpack)到线程栈上。此外,在调用方法前,CLR 必须实参具有正确的数据类型。最后,CLR必须确保调用者有正确的安全权限来访问被调用的成员。好上述所有原因,最好避免利用反射来访问字段或调用方法/属性。应该利用以下两种技权一开发应用程序来动态发现和构造类型实例。
- 让类型从编译时已知的基类型派生。在运行时构造派生类型的实例,将对它的引用放到基类型的变量中(利用转型),再调用基类型定义的虚方法。
- 让类型实现编译时已知的接口。在运行时构造类型的实例,将对它的引用放到接口类型的变量中(利用转型),再调用接口定义的方法。
示例代码:
using System.Diagnostics;
using System.Reflection;
namespace AssemblyDemo;
class Program
{
static void Main(string[] args)
{
// 创建测试对象
var testObject = new TestClass();
// 测试直接调用
Stopwatch directStopwatch = Stopwatch.StartNew();
for (int i = 0; i < 1000000; i++)
{
testObject.SimpleMethod();
}
directStopwatch.Stop();
Console.WriteLine($"直接调用时间: {directStopwatch.ElapsedMilliseconds} ms");
// 获取方法信息
MethodInfo methodInfo = typeof(TestClass).GetMethod("SimpleMethod");
// 测试反射调用
Stopwatch reflectionStopwatch = Stopwatch.StartNew();
for (int i = 0; i < 1000000; i++)
{
methodInfo.Invoke(testObject, null);
}
reflectionStopwatch.Stop();
Console.WriteLine($"反射调用时间: {reflectionStopwatch.ElapsedMilliseconds} ms");
}
}
class TestClass
{
private int _counter = 0;
public void SimpleMethod()
{
// 增加计数器
_counter++;
}
}
运行结果:
直接调用时间: 1 ms
反射调用时间: 10 ms
该文章在 2024/9/18 12:01:20 编辑过