C#加载程序集

848 阅读15分钟

加载程序集的方式一般使用三种方法:

  • Assembly.Load()
  • Assembly.LoadFrom()
  • Assembly.LoadFile()

我们在使用C# 语言的Assembly.Load 来加载托管程序集并使用反射功能时,一般需要先通过Assembly.Load(), Assembly.LoadFrom() 等方法将目标托管程序集加载到当前应用程序域中,然后生成对应实例,最后再进行调用实例的属性或者方法。

一般情况下,我们调用Assembly.Load 一类方法是不会出问题的,但是对于以下几种情况Assembly.Load 方法无法处理:

  1. 程序集可能是延迟签名的。
  2. 程序集可能被CAS 策略保护。
  3. 宿主程序与目标程序集的处理器架构不同。
  4. 当加载目标程序集时,目标程序集中的方法可能正在运行。 (比如,模块初始化)
  5. 程序集可能应用了绑定策略, 你可能不会得到你想要的那个程序集。

我们现在关注第四种情况,因为这种情况是最常见的。我们思考以下几个问题:

 

1. 为什么目标程序集的方法在运行时不允许再加载一次?

准确地说是为什么在一个应用程序域(AppDomain)中加载后的程序集默认不允许再另外一个应用程序域中加载?这是因为在第一次加载应用程序集时,Assemlby.Load 方法会将此程序集锁住,以防止在自己使用过程中应用程序集被其他应用程序修改(一般指删除)。这其实与Win32 API 中的CreateFile 函数行为类似,我们都知道,在 Windows 中去占用一个文件最直接、最简单的方式就是调用 CreateFile API 函数来打开文件。具体请参照:

blog.csdn.net/xt_xiaotian… 

 

2. Assembly.Load 方法能否实现在加载目标程序集时不锁定它?

我们可以使用如下代码加载我们的程序集:

byte[] buffer = System.IO.File.ReadAllBytes(yourFullfileNamePath);

//Load assembly using byte array

Assembly assembly = Assembly.Load(buffer);

后台的实现是暂时先将目标程序集锁定,然后把程序集内容复制到内存中,读取后将程序集解锁并从内存中加载目标程序集的拷贝。

如果你需要通过上面的方法加载GAC 中的程序集,那么可以通过如下代码实现:


string assemblyName = "AssemblyTest, Version=1.0.0.0, Culture=neutral, PublicKeyToken=fffb45e56dd478e3";

Assembly ass = Assembly.ReflectionOnlyLoad(assemblyName);

byte[] buffer = System.IO.File.ReadAllBytes(ass.Location);

Assembly assembly = Assembly.Load(buffer

3. Assembly.Load 方法的缓存

在我们使用Assembly.Load 系列方法加载目标程序集时,可能有各种情况导致加载失败,最常见的是目标程序集不存在而导致加载失败问题。失败后我们可能想要再加载一次或者加载多次直到成功为止,但是在.NET Framework 2.0 以后默认是无法实现的,原因在于.NET Framework 2.0 以后 Assembly.Load 方法有缓存,第一次加载目标程序集的失败或者成功的状态都会被缓存,这样在你下一次加载目标程序集时不会真的加载,会直接从缓存里取目标程序集的内容和状态。

对这种情况我们可以在调用Assembly.Load 方法的宿主程序的app.config 中加入如下配置信息来禁用缓存:


<?xml version="1.0"?>
<configuration>
  <runtime>
    <disableCachingBindingFailures enabled="1" />
  </runtime>
  <startup>
    <supportedRuntime version="v2.0.50727"/>
  </startup>
</configuration>

4. 还有其他方案吗?

CLR 框架略图:

image.png

因为目前Assembly 的加载与卸载是完全由应用程序域控制的,一个程序集只可以通过应用程序域加载,只能通过应用程序域的卸载而卸载,其他任何方式都不可以!!!

那么我们可以在需要时将目标程序集加载进应用程序域,不需要时将应用程序域卸载,但是当前应用程序域的卸载只能通过关闭程序来实现。

到目前为止,看似无解了,但是我们忽略了一个事实,那就是我们可以在当前应用程序域中创建子应用程序域。可以通过在子应用程序域中进行程序集的加载,或者说只要涉及程序集加载的全部放在子应用程序域中,主应用程序域中不做任何与程序集加载有关的事情。

这部分内容我就不详细介绍了,大家可以参考:

www.codeproject.com/Articles/42…

 

5. 我们确实需要使用Assembly.Load吗?

因为Assembly.Load 是将整个程序集以及其相关的依赖程序集全部加载进来,只要有一个出错就会导致加载失败。如果我们只是为了使用当前程序集的类型,而不是使用其方法或者属性的话就完全可以抛弃Assembly.Load 方法。

微软在.Net Framework 2.0 时介绍了几个新的程序集加载APIs:

Assembly.ReflectionOnlyLoadFrom(String assemblyFile)

Assembly.ReflectionOnlyLoad(byte[] rawAssembly)

Assembly.ReflectionOnlyLoad(String assemblyName)

基于开篇提到的Assembly.Load 方法的5种问题, Reflection Only程序集加载APIs 可以:

  1. 跳过程序集强命名认证。
  2. 跳过程序集CAS策略认证。
  3. 跳过处理器架构检查规则。
  4. 不在目标程序中执行任何方法,包括构造函数。
  5. 不应用任何绑定策略。

使用时有如下几个注意事项:

  1. CLR 不会搜索目标程序集所依赖的程序集,我们必须通过ReflectionOnlyAssemblyResolve(Assembly.Load 中的对应事件是AssemblyResolve)事件手动处理。

  2. 不可以在通过ReflectionOnly 方法加载进来的程序集执行任何方法,包括构造函数,只可以获取程序集的信息和类型。

  3. 建议使用Assembly.ReflectionOnlyLoadFrom 方法,但是如果目标程序集在GAC中那么可以使用Assembly.ReflectionOnlyLoad方法。

具体示例代码如下:


using System;
using System.IO;
using System.Reflection;

public class ReflectionOnlyLoadTest
{
    private String m_rootAssembly;
    public ReflectionOnlyLoadTest(String rootAssembly)
    {
        m_rootAssembly = rootAssembly;
    }

    public static void Main(String[] args)
    {
        if (args.Length != 1)
        {
            Console.WriteLine("Usage: Test assemblyPath");
            return;
        }

        try
        {
            ReflectionOnlyLoadTest rolt = new ReflectionOnlyLoadTest(args[0]);
            rolt.Run();
        }
        catch (Exception e)
        {
            Console.WriteLine("Exception: {0}!!!", e.Message);
        }
    }

    internal void Run()
    {
        AppDomain curDomain = AppDomain.CurrentDomain;
        curDomain.ReflectionOnlyAssemblyResolve += 
            new ResolveEventHandler(MyReflectionOnlyResolveEventHandler);
        Assembly asm = Assembly.ReflectionOnlyLoadFrom(m_rootAssembly);
        // force loading all the dependencies
        Type[] types = asm.GetTypes();
        // show reflection only assemblies in current appdomain
        Console.WriteLine("------------- Inspection Context --------------");
        foreach (Assembly a in curDomain.ReflectionOnlyGetAssemblies())
        {
            Console.WriteLine("Assembly Location: {0}", a.Location);
            Console.WriteLine("Assembly Name: {0}", a.FullName);
            Console.WriteLine();
        }
    }

    private Assembly MyReflectionOnlyResolveEventHandler(object sender, ResolveEventArgs args)
    {
        AssemblyName name = new AssemblyName(args.Name);
        String asmToCheck = Path.GetDirectoryName(m_rootAssembly) + "\" + name.Name + ".dll";
        if (File.Exists(asmToCheck))
        {
            return Assembly.ReflectionOnlyLoadFrom(asmToCheck);
        }

        return Assembly.ReflectionOnlyLoad(args.Name);
    }
}

6. 为什么没有Assembly.UnLoad 方法?

以下是CLR 产品单元经理(Unit Manager) Jason Zander 文章中的内容的整理:

  1) 为了保证 CLR 中代码所引用的代码地址都是有效的,必须跟踪诸如 GC 对象和 COM CCW 之类的特殊应用。否则会出现 Unload 一个 Assembly 后,还有 CLR 对象或 COM 组件使用到这个 Assembly 的代码或数据地址,进而导致访问异常。为了避免这种错误进行的跟踪,目前是在 AppDomain 一级进行的,如果要加入 Assembly.Unload 支持,则跟踪的粒度必须降到 Assembly 一级,这虽然在技术上不是不能实现,但代价太大了。   2) 如果支持 Assembly.Unload 则必须跟踪每个 Assembly 的代码使用到的句柄和对现有托管代码的引用。例如现在 JITer 在编译方法时,生成代码都在一个统一的区域,如果要支持卸载 Assembly 则必须对每个 Assembly 都进行独立编译。此外还有一些类似的资源使用问题,如果要分离跟踪技术上虽然可行,但代价较大,特别是在诸如 WinCE 这类资源有限的系统上问题比较明显。   3) CLR 中支持跨 AppDomain 的 Assembly 载入优化,也就是 domain neutral 的优化,使得多个 AppDomain 可以共享一份代码,加快载入速度。而目前 v1.0 和 v1.1 无法处理卸载 domain neutral 类型代码。这也导致实现 Assembly.Unload 完整语义的困难性。

详细请参考: www.cnblogs.com/ccBoy/archi…

blogs.msdn.com/b/jasonz/ar…

 

7. 需要牢记的经验

  1. 只加载自己需要直接调用的程序集,不加载目标程序集内部引用的程序集和其他无关程序集。

  2. 能使用RefelectionLoad 方法加载的程序集绝不要使用Assembly.Load 方法加载。

  3. 一旦出现加载错误,不要显而易见认为是程序集不存在!要检查程序集加载缓存、是否出现同一程序集被不同应用程序域加载情况等。

 

至此,我们已经阐述了Assembly.Load 方法的一些特性,你已经了解它了吗?

参考原文:www.cnblogs.com/weifeng123/…

Assembly.Load()

Load()方法接收一个String或AssemblyName类型作为参数,这个参数实际上是需要加载的程序集的强名称(名称,版本,语言,公钥标记)。例如.NET 2.0中的FileIOPermission类,它的强名称是:System.Security.Permissions.FileIOPermission, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089,对于弱命名的程序集,则只会有程序集名称,而不会有版本,语言和公钥标记。如 TestClassLibrary.

 Assembly.LoadFrom()

LoadFrom()方法可以从指定文件中加载程序集,通过查找程序集的AssemblyRef元数据表,得知所有引用和需要的程序集,然后在内部调用Load()方法进行加载。例如:Assembly.LoadFrom(@"C:\ABC\Test.dll");

LoadFrom()首先会打开程序集文件,通过GetAssemblyName方法得到程序集名称,然后关闭文件,最后将得到的AssemblyName对象传入Load()方法中
随后,Load()方法会再次打开这个文件进行加载。所以,LoadFrom()加载一个程序集时,会多次打开文件,造成了效率低下的现象(与Load相比)
由于内部调用了Load(),所以LoadFrom()方法还是会应用版本绑定重定向策略,也会在GAC和各个指定位置中进行查找
LoadFrom()会直接返回Load()的结果——一个Assembly对象的引用
如果目标程序集已经加载过,LoadFrom()不会重新进行加载
LoadFrom支持从一个URL加载程序集(如"www.abc.com/test.dll"),…
从URL加载程序集时,如果计算机未联网,LoadFrom会抛出一个异常。如果IE被设置为“脱机工作”,则不会抛出异常,转而从缓存中寻找已下载的文件

Assembly.LoadFile()

LoadFile()从一个指定文件中加载程序集,它和LoadFrom()的不同之处在于LoadFile()不会加载目标程序集所引用和依赖的其他程序集。您需要自己控制并显示加载所有依赖的程序集

LoadFile()不会解析任何依赖
LoadFile()可以多次加载同一程序集
显式加载依赖程序集的方法是,注册AppDomain的AssemblyResolve事件

一些关于C#反射的知识,估计也就最多达到使用API的程度,至于要深入了解,以现在的水平估计很难做到,所以下面此篇文章,以作为一个阶段的总结。

对于反射的总结,我想从以下几个方面展开,首先是反射程序集,模块,类的成员以及成员的一些信息;接下来就是动态调用类的成员方法;第三个方面就动态产生程序集,模块和类以及类的成员。好了,现在就让我们从反射各种信息开始吧

在C#中,我们要使用反射,首先要搞清楚以下命名空间中几个类的关系:

System.Reflection命名空间

(1)   AppDomain:应用程序域,可以将其理解为一组程序集的逻辑容器

(2)   Assembly:程序集类

(3)   Module:模块类

(4)   Type:使用反射得到类型信息的最核心的类

他们之间是一种从属关系,也就是说,一个AppDomain可以包含N个Assembly,一个Assembly可以包含N个Module,而一个Module可以包含N个Type.

AppDomain这个类我们等下再来讲解。我们先关注Assembly个类

在程序中,如果我们要动态加载一个程序集怎么办呢?有几种方式可以使用,分别是Load、LoadFrom和LoadWithPartialName三个Assembly的静态方法.

先来讲解Assembly.Load方法,该方法会有多个重载版本,其中一个就是提供程序集的详细信息,即程序集的标识,包括程序集的名称,版本,区域信息,公有密钥标记,全部都是以一个字符串的形式提供,例如:"MyAssembly,Version=1.0.0.0,culture=zh-CN,PublicKeyToken=47887f89771bc57f”.

那么,使用Assembly.Load加载程序集的顺序是怎样的呢?首先它会去全局程序集缓存查找,然后到应用程序的根目录查找,最后会到应用程序的私有路径查找。

当然,如果你使用的是弱命名程序集,也即只给出程序集的名称,那么这个时候,CLR将不会在程序集上应用任何安全或者部署策略,而且Load也不会到全局缓存程序集中查找程序集。

测试加载弱命名程序集的例子如下:

(1)   新建一个控制台应用程序的工程,同时勾选创建解决方案

(2)   在解决方案中新建一个类库的项目,随便写一个类和一个方法

(3)   在控制台项目中,首先不添加引用,直接在Main方法中添加如下代码:

Assembly assembly = Assembly.Load("MyAssembly");

if (assembly != null)

{ Console.WriteLine("加载成功"); }

执行程序,会抛出异常,说找不到该程序集。什么原因呢?因为我们使用的是弱命名程序集,Load方法不会去全局程序集缓存中查找,而该应用程序目录下又没有该程序集,所以程序找不到。这个时候,我们把程序稍微改一下,不用添加代码,只需添加对MyAssembly的引用,重新运行程序,加载成功了。

 Assembly.Load()方法,Assembly.LoadFrom()方法,Assembly.LoadFile()方法的区别!

1,Assembly.Load()

这个方法通过程序集的长名称(包括程序集名,版本信息,语言文化,公钥标记)来加载程序集的,会加载此程序集引用的其他程序集,一般情况下都应该优先使用 这个方法,他的执行效率比LoadFrom要高很多,而且不会造成重复加载的问题(原因在第2点上说明)

使用这个方法的时候, CLR会应用一定的策略来查找程序集,实际上CLR按如下的顺序来定位程序集:

⑴如果程序集有强名称,在首先在全局程序集缓(GAC)中查找程序集。         

⑵如果程序集的强名称没有正确指定或GAC中找不到,那么通过配置文件中的元素指定的URL来查找

⑶如果没有指定强名称或是在GAC中找不到,CLR会探测特定的文件夹:

假设你的应用程序目录是C:\AppDir,元素中的privatePath指定了一个路径Path1,你要定位的程序集是AssemblyName.dll则CLR将按照如下顺序定位程序集

          C:\AppDir\AssemblyName.dll

          C:\AppDir\AssemblyName\AssemblyName.dll

          C:\AppDir\Path1\AssemblyName.dll

          C:\AppDir\Path1\AssemblyName\AssemblyName.dll

如果以上方法不能找到程序集,会发生编译错误,如果是动态加载程序集,会在运行时抛出异常!

2,Assembly.LoadFrom()

这个方法从指定的路径来加载程序集,实际上这个方法被调用的时候,CLR会打开这个文件,获取其中的程序集版本,语言文化,公钥标记等信息,把他们传递给 Load方法,接着,Load方法采用上面的策略来查找程序集。如果找到了程序集,会和LoadFrom方法中指定的路径做比较,如果路径相同,该程序集会被认为是应用程序的一部分,如果路径不同或Load方法没有找到程序集,那该程序集只是被作为一个“数据文件”来加载,不会被认为是应用程序的一部分。 这就是在第1点中提到的Load方法比LoadFrom方法的执行效率高的原因。另外,由于可能把程序集作为“数据文件”来加载,所以使用 LoadFrom从不同路径加载相同程序集的时候会导致重复加载。当然这个方法会加载此程序集引用的其他程序集。

3,Assembly.LoadFile()

这个方法是从指定的文件来加载程序集,和上面方法的不同之处是这个方法不会加载此程序集引用的其他程序集!

结论:一般大家应该优先选择Load方法来加载程序集,如果遇到需要使用LoadFrom方法的时候,最好改变设计而用Load方法来代替!

另:Assembly.LoadFile 与 Assembly.LoadFrom的区别

1、Assembly.LoadFile只载入相应的dll文件,比如Assembly.LoadFile("abc.dll"),则载入abc.dll,假如abc.dll中引用了def.dll的话,def.dll并不会被载入。

Assembly.LoadFrom则不一样,它会载入dll文件及其引用的其他dll,比如上面的例子,def.dll也会被载入。

2、用Assembly.LoadFrom载入一个Assembly时,会先检查前面是否已经载入过相同名字的Assembly,比如abc.dll有两个版本(版本1在目录1下,版本2放在目录2下),程序一开始时载入了版本1,当使用Assembly.LoadFrom("2\abc.dll")载入版本2时,不能载入,而是返回版本1。Assembly.LoadFile的话则不会做这样的检查,比如上面的例子换成Assembly.LoadFile的话,则能正确载入版本2。

LoadFile:加载指定路径上的程序集文件的内容。LoadFrom: 根据程序集的文件名加载程序集文件的内容。

区别:

LoadFile 方法用来来加载和检查具有相同标识但位于不同路径中的程序集.但不会加载程序的依赖项。

LoadFrom 不能用于加载标识相同但路径不同的程序集。

原文链接:blog.csdn.net/xuchen_wang…