CLR查找和加载程序集的方式(一)
C#开发者在开发WinForm程序、Asp.Net Web(MVC)程序等,不可避免的在项目中引用许多第三方的DLL程序集,
编译后引用的dll都放在根目录下。以我个人作品 AutoProject Studio 自动化项目生成器 为例,由于需要支持SQL Server、
Oracle、MySQL、PostgreSQL、DB2、Sybase、Infomix、SQLite、Access等多种数据库,所以引用了DmProvider.dll、
IBM.Data.DB2.dll、IBM.Data.Informix.dll、MySql.Data.dll、Npgsql.dll、SQLite.Interop.dll、System.Data.dll、
System.Data.OracleClient.dll、Sybase.AdoNet4.AseClient.dll 等DLL,参考下图:
随着项目的日益增大,根目录下充满了各种各样的dll,非常的不美观。如果能够把dll按照想要的目录来存放,那么系统就美观多了。
此问题就涉及到 CLR查找和加载程序集的方式。
CLR解析一个程序集会在一个根目录内进行搜索,整个探索过程又称Probing,这个根目录很显然就是当前包含当前程序集的目录。
AppDomainSetup这个类存储着探索目录的信息,其成员包括: ApplicationBase 、 PrivateBinPath 。
程序搜索DLL的顺序如下(区分强名称签名、没有强名称签名的程序集)
- 程序的根目录
- 根目录下面,与被引用程序集同名的子目录
- 根目录下面被明确定义为私有目录的子目录
- 在目录中查找的时候,如果dll查找不到,则会尝试查找同名的exe
- 如果程序集带有区域性,而不是语言中立的,则还会尝试查找以语言区域命名的子目录
- 全局程序集缓存
- 如果有定义codebase,则以codebase定义为准,如果 codebase指定的路径找不到,则直接报告错误
- 程序的根目录
- 根目录下面,与被引用程序集同名的子目录
- 根目录下面被明确定义为私有目录的子目录
- 在目录中查找的时候,如果dll查找不到,则会尝试查找同名的exe
- 如果程序集带有区域性,而不是语言中立的,则还会尝试查找以语言区域命名的子目录
如何让程序识别不同目录下的dll?
我们看到,上面的顺序无论是否有强名称签名看,都提到了一个名词 “私有目录”。
针对该问题,微软提供了<probing> 元素, 在配置文件中自定义存储目录。
1 <?xml version="1.0" encoding="utf-8" ?>
2 <configuration>
3 <runtime>
4 <!--双核以上计算机请开启下面的选项为 true-->
5 <gcServer enabled="false" />
6 <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
7 <probing privatePath="bin;bin2\subbin;bin3" />
8 </assemblyBinding>
9 </runtime>
10 </configuration>
- configuration:每个配置文件中的根元素,常用语言 runtime 和 .NET Framework 应用程序会使用这些文件。
- runtime:包含程序集绑定和垃圾回收的相关信息。
- assemblyBinding:包含有关程序集版本重定向和程序集位置的信息。
- privatePath:必选特性。指定可能包含程序集的应用程序基目录的子目录。 用分号分隔每个子目录。
上述 privatePath中的"bin;bin2\subbin;bin3",其中bin是默认编译输出目录,bin2、bin3 是自定义目录,subbin是bin2下的子目录。
把DLL分别放入上述目录中,程序运行正常。这是最简单的方法,当然也有一定的局限性,就是没法对DLL做控制。另外,无法解决第三方 DllImport
中引入的程序集不在根目录下的问题。但是该方法基本解决了分目录存储的问题。
应用程序集域中支持在程序集解析时的处理: AppDomain.CurrentDomain.AssemblyResolve += CurrentDomain_AssemblyResolve; 通过这个事件,我们可以在程序集解析时,根据不同的程序集做不用的处理,比如加载x86的程序集还是64位的程序集,当然也就可以指定程序集目录了,这也正是 Assembly.Load 和 Assembly.LoadFrom 等方法的用武之地。
1 using System; 2 using System.IO; 3 using System.Reflection; 4 using System.Windows.Forms; 5 6 namespace WindowsFormsApplication1 7 { 8 static class Program 9 { 10 /// <summary> 11 /// 应用程序的主入口点。 12 /// </summary> 13 [STAThread] 14 static void Main() 15 { 16 Application.EnableVisualStyles(); 17 Application.SetCompatibleTextRenderingDefault(false); 18 19 AppDomain.CurrentDomain.AssemblyResolve += CurrentDomain_AssemblyResolve; 20 21 Application.Run(new Form1()); 22 } 23 24 /// <summary> 25 /// 解析当前应用程序域内指定目录下的DLL 26 /// </summary> 27 /// <param name="sender"></param> 28 /// <param name="args"></param> 29 /// <returns></returns> 30 private static Assembly CurrentDomain_AssemblyResolve(object sender,ResolveEventArgs args) 31 { 32 string path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory,@"xx目录\"); 33 path = Path.Combine(path,args.Name.Split(',')[0]); 34 path = string.Format(@"{0}.dll",path); 35 return Assembly.LoadFrom(path); 36 } 37 } 38 }
1 using System; 2 using System.IO; 3 using System.Reflection; 4 using System.Windows.Forms; 5 6 namespace WindowsFormsApplication1 7 { 8 static class Program 9 { 10 /// <summary> 11 /// 应用程序的主入口点。 12 /// </summary> 13 [STAThread] 14 static void Main() 15 { 16 Application.EnableVisualStyles(); 17 Application.SetCompatibleTextRenderingDefault(false); 18 19 SetPrivateBinPath();//设置程序集所在的指定目录并解析DLL 20 21 Application.Run(new Form1()); 22 } 23 24 25 /// <summary> 26 /// 设置程序集所在的指定目录并解析DLL 27 /// </summary> 28 private static void SetPrivateBinPath() 29 { 30 if(AppDomain.CurrentDomain.IsDefaultAppDomain()) 31 { 32 string appName = AppDomain.CurrentDomain.FriendlyName; 33 var currentAssembly = Assembly.GetExecutingAssembly(); 34 35 AppDomainSetup setup = new AppDomainSetup(); 36 setup.ApplicationBase = Environment.CurrentDirectory; 37 setup.PrivateBinPath = "xx目录"; 38 setup.ConfigurationFile = setup.ApplicationBase + string.Format("\\Config\\{0}.config",appName); 39 40 AppDomain newDomain = AppDomain.CreateDomain("NewAppDomain",null,setup); 41 int ret = newDomain.ExecuteAssemblyByName(currentAssembly.FullName); 42 43 AppDomain.Unload(newDomain); 44 Environment.ExitCode = ret; 45 Environment.Exit(0); 46 return; 47 } 48 } 49 } 50 }
通过 Environment.CurrentDirectory=customPath ,切换目录后,在调用dll方法时运行正常。
处理 [DllImport] 中的程序集的加载,此处提供用一种方式来处理:增加环境变量。
C#代码如下:
static void AddEnvironmentPaths(IEnumerable<string> paths) { var path = new[] { Environment.GetEnvironmentVariable("PATH") ?? string.Empty }; string newPath = string.Join(Path.PathSeparator.ToString(), path.Concat(paths)); Environment.SetEnvironmentVariable("PATH", newPath); }
成在管理,败在经验;嬴在选择,输在不学! 贵在坚持!
个人作品
BIMFace.SDK.NET
开源地址:https://gitee.com/NAlps/BIMFace.SDK
系列博客:https://www.cnblogs.com/SavionZhang/p/11424431.html
系列视频:https://www.cnblogs.com/SavionZhang/p/14258393.html
技术栈
1、Visual Studio、.NET Core/.NET、MVC、Web API、RESTful API、gRPC、SignalR、Java、Python
2、jQuery、Vue.js、Bootstrap、ElementUI
3、数据库:分库分表、读写分离、SQLServer、MySQL、PostgreSQL、Redis、MongoDB、ElasticSearch、达梦DM
4、架构:DDD、ABP、SpringBoot、jFinal
5、环境:跨平台、Windows、Linux、Nginx
6、移动App:Android、IOS、HarmonyOS、微信小程序、钉钉、uni-app、MAUI
分布式、高并发、云原生、微服务、Docker、CI/CD、DevOps、K8S;Dapr、RabbitMQ、Kafka、RPC、Elasticsearch。
欢迎关注作者头条号 张传宁IT讲堂,获取更多IT文章、视频等优质内容。
出处:www.cnblogs.com/SavionZhang
作者:张传宁 技术顾问、培训讲师、微软MCP、系统架构设计师、系统集成项目管理工程师、科技部创新工程师。
专注于企业级通用开发平台、工作流引擎、自动化项目(代码)生成器、SOA 、DDD、 云原生(Docker、微服务、DevOps、CI/CD);PDF、CAD、BIM 审图等研究与应用。
多次参与电子政务、图书教育、生产制造等企业级大型项目研发与管理工作。
熟悉中小企业软件开发过程:可行调研、需求分析、架构设计、编码测试、实施部署、项目管理。通过技术与管理帮助中小企业实现互联网转型升级全流程解决方案。
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
如有问题,可以通过邮件905442693@qq.com联系。共同交流、互相学习。
如果您觉得文章对您有帮助,请点击文章右下角【推荐】。您的鼓励是作者持续创作的最大动力!