程序集加载和反射

时间:2021-09-03 16:08:23

前言


  本篇讨论程序集的加载及反射。主要涉及到System.Reflection.Assembly和System.Type两个类,前者可以用于访问指定程序集的相关信息,或把程序集加载到程序当中,后者可以访问任何数据类型的信息。以下,是本篇文章涉及的主要内容。

程序集加载和反射


程序集加载


  本节首先介绍Assembly类,该类位于System.Reflection命名空间下,它允许访问指定程序集的元数据,也包含加载和执行程序集的中的方法。下面将介绍几种常用的动态加载程序集的方式:

方法名称 说明
Load 加载程序集
LoadFrom 加载指定路径的程序集
LoadFile 仅加载指定路径的程序集(不包括依赖项)
ReflectionOnlyLoad 加载程序集(不执行任何带代码)
ReflectionOnlyLoadFrom 加载指定路径的程序集(不执行任何代码)

Assembly.Load

  Assembly的Load方法有几个重载版本,两种最常用的重载:Load(AssemblyName)和Load(String),传入的参数是需要加载的程序集的名称。

创建控制台应用程序AssemblyAndReflection,向解决方案添加新项目AssemblyLoad,添加类ClassA、ClassB、ClassC,编译后为AssemblyLoad.dll分配强名称并注册到GAC中。

    public class ClassA
{
public void SayHello()
{
Console.WriteLine("Hello.This is ClassA");
}
}

public class ClassB
{
public void SayHello()
{
Console.WriteLine("Hello.This is ClassB");
}
}

public class ClassC
{
public void SayHello()
{
Console.WriteLine("Hello.This is ClassC");
}
}

向Program.cs中添加如下代码:

    static void Main(string[] args)
{
string fullName = "AssemblyLoad,Version=1.0.0.0,Culture=neutral,PublicKeyToken=098608575f7409cd, processor architecture=MSIL";
//string fullName = "AssemblyLoad";
//Assembly assembly = Assembly.Load(new AssemblyName(fullName));
Assembly assembly = Assembly.Load(fullName);

if (assembly != null)
{
foreach (var c in assembly.GetTypes())
{
Console.WriteLine(c.FullName);
}
}
Console.ReadLine();
//****************************************************OutPut****************************************************
//AssemblyLoad.ClassA
//AssemblyLoad.ClassB
//AssemblyLoad.ClassC
//**************************************************************************************************************
}

注意,Load方法的参数可以是强命名程序集或弱命名程序集(上述代码中注释掉的fullName变量)。传入不同参数时,查找程序集的方式略有不同。

程序集加载和反射

Assembly.LoadFrom

  Assembly的LoadFrom方法加载指定了路径名的程序集。将AssemblyLoad.dll文件放至D:\DLL\下,修改上述代码:

    static void Main(string[] args)
{
Assembly assembly = Assembly.LoadFrom(@"D:\DLL\AssemblyLoad.dll");

if (assembly != null)
{
foreach (var c in assembly.GetTypes())
{
Console.WriteLine(c.FullName);
}

}

Console.ReadLine();

//****************************************************OutPut****************************************************
//AssemblyLoad.ClassA
//AssemblyLoad.ClassB
//AssemblyLoad.ClassC
//**************************************************************************************************************
}

LoadFrom的执行原理:

  1. 调用System.Reflection.AssemblyName类的静态方法GetAssemblyName方法,打开指定路径下的文件,返回AssemblyName对象。下面是GetAssemblyName方法的源码:

    [System.Security.SecuritySafeCritical]  // auto-generated
    [ResourceExposure(ResourceScope.None)]
    [ResourceConsumption(ResourceScope.Machine, ResourceScope.Machine)]
    static public AssemblyName GetAssemblyName(String assemblyFile)
    {
    if(assemblyFile == null)
    throw new ArgumentNullException("assemblyFile");
    Contract.EndContractBlock();

    // Assembly.GetNameInternal() will not demand path discovery
    // permission, so do that first.
    String fullPath = Path.GetFullPathInternal(assemblyFile);
    new FileIOPermission( FileIOPermissionAccess.PathDiscovery, fullPath ).Demand();
    return nGetFileInformation(fullPath);
    }
  2. 调用Assembly.Load方法,将步骤1中返回的AssemblyName对象作为参数传入。

Assembly.LoadFile

  加载指定路径上的程序集文件的内容。LoadFile方法不会加载目标程序集引用和依赖的其他程序集。

Assembly.ReflectionOnlyLoad和Assembly.ReflectionOnlyLoadFrom

  如果只希望通过反射来分析程序集的元数据,并确保程序集中的任何代码都不会被执行,这种情况下可以使用Assembly类的ReflectionOnlyLoadFrom方法或ReflectionOnlyLoad方法。


获取类型的信息


  本节介绍System.Type类,通过这个类可以访问任何数据类型的信息,System.Type类型是执行类型和对象操作的起点。获取Type对象的几种方式:

Object.GetType()

    int x = 100;
Type t = x.GetType();
Console.WriteLine(t.FullName);

System.Type类提供的静态方法ReflectionOnlyGetType()

    string typeName = Type.ReflectionOnlyGetType("AssemblyLoad.ClassA, AssemblyLoad, Version=1.0.0.0, Culture=neutral, PublicKeyToken=098608575f7409cd, processor architecture=MSIL", false, true).FullName;
Console.WriteLine(typeName);

System.Reflection.Assembly类提供的实例成员GetTypes、DefinedTypes和ExportedTypes

    string fullName = "AssemblyLoad,Version=1.0.0.0,Culture=neutral,PublicKeyToken=098608575f7409cd, processor architecture=MSIL";

Assembly assembly = Assembly.Load(fullName);
Console.WriteLine("assembly.GetTypes():");
foreach (var t in assembly.GetTypes())
{
Console.WriteLine(t.FullName);
}
Console.WriteLine();
Console.WriteLine("assembly.ExportedTypes:");
foreach (var t in assembly.ExportedTypes)
{
Console.WriteLine(t.FullName);
}
Console.WriteLine();
Console.WriteLine("assembly.DefinedTypes:");
foreach (var t in assembly.DefinedTypes)
{
Console.WriteLine(t.FullName);
}
Console.WriteLine();

typeof关键字(应尽量使用这个操作符来获取Type引用,因为操作符生成的代码通常更快)

    Console.WriteLine(typeof(int).FullName);


构造类型的实例


  获取对Type派生对象的引用之后,就可以构造该类型的实例了。

System.Activator.CreateInstance

string fullName = "AssemblyLoad,Version=1.0.0.0,Culture=neutral,PublicKeyToken=098608575f7409cd, processor architecture=MSIL";
Type t = Assembly.Load(fullName).GetType("AssemblyLoad.ClassA");
var o = Activator.CreateInstance(t);
Console.WriteLine(o.GetType());


设计支持加载项的应用程序


反射的性能

  反射是相当强大的机制,允许在运行时发现并使用编译时还不太了解的类型及成员。但是,反射也存在如下缺点:

  • 反射造成编译时无法保证类型安全
  • 反射速度慢。

基于上述原因,应尽量避免使用反射来访问字段或调用方法及属性。在设计支持加载项的应用程序时,让类型实现编译时已知的接口,在运行时构造类型的实例,将对它的引用放到接口类型的变量中,再调用接口定义的方法。

创建支持加载项的应用程序

  1. 添加CoreLib项目,创建ISayHello接口并为接口定义SayHello方法

    namespace ISayHello
    {
    public interface ISayHello
    {
    void SayHello();
    }
    }
  2. 修改AssemblyLoad,添加CoreLib引用,并使其中的ClassA、ClassB、ClassC分别实现接口ISayHello,重新编译,将AssemblyLoad.dll文件拷贝至D:\DLL\AssemblyLoad.dll

    using CoreLib;

    namespace AssemblyLoad
    {
    public class ClassA : ISayHello
    {
    public void SayHello()
    {
    Console.WriteLine("Hello.This is ClassA");
    }
    }

    public class ClassB : ISayHello
    {
    public void SayHello()
    {
    Console.WriteLine("Hello.This is ClassB");
    }
    }

    public class ClassC : ISayHello
    {
    public void SayHello()
    {
    Console.WriteLine("Hello.This is ClassC");
    }
    }
    }
  3. 回到控制台应用程序AssemblyAndReflection,添加CoreLib引用,向App.config中添加配置节:

    <appSettings>
      <add key="Test" value="ClassA"/>
    </appSettings>

    修改Program中的Main方法:

    class Program
    {
    static void Main(string[] args)
    {
    string type = ConfigurationManager.AppSettings["Test"];
    Assembly assembly = Assembly.LoadFrom(@"D:\DLL\AssemblyLoad.dll");

    var q = from r in assembly.ExportedTypes
    where r.IsClass && typeof(ISayHello).GetTypeInfo().IsAssignableFrom(r.GetTypeInfo()) && r.Name == type
    select r;

    foreach (var t in q)
    {
    ISayHello s = (ISayHello)Activator.CreateInstance(t);
    s.SayHello();
    }

    Console.ReadLine();
    }
    }

    启动程序,控制台输出Hello.This is ClassA,以上示例完成了根据配置文件中的参数调用指定类型下的方法,当然,也可以把参数放在数据库中。

项目结构示意图:

程序集加载和反射

使用反射获取类型的成员


获取类型的成员

  抽象基类System.Reflection.MemberInfo封装了所有类型成员都通用的一组属性。MemberInfo有许多派生类,每个都封装了与特性类型成员相关的更多属性,以下是这些类型的层次结构图:

程序集加载和反射

调用类型的成员:

成员类型 调用(Invoke)成员需要调用的方法
FieldInfo 调用GetValue获取字段的值
调用SetValue设置字段的值
ConstructorInfo 调用Invoke构造类型的实例并调用构造器
MethodInfo 调用Invoke来调用类型的方法
PropertyInfo 调用GetValue获取属性的get访问器方法
调用SetValue获取属性的set访问器方法
EventInfo 调用AddEventHandler来调用事件的add访问器方法
调用RemoveEventHandler来调用时间的remove访问器方法

使用绑定句柄减少进程的内存消耗

  Type和MemberInfo类型的对象需要大量的内存,如果将这些对象保存在集合当中,可能对程序的性能产生负面的影响。如果需要保存/缓存大量的Type和MemberInfo对象,可以使用运行时句柄代替对象以减少占用的内存。System命名空间下有三个运行时句柄类型:

  • RuntimeTypeHandle
  • RuntimeFieldHandle
  • RuntimeMethodHandle

以下是《CLR Via C#》第4版中的示例(博主已经想不到更贴切的示例了):

class Program
{
private const BindingFlags c_bf = BindingFlags.FlattenHierarchy |
BindingFlags.Instance | BindingFlags.Static |
BindingFlags.Public | BindingFlags.NonPublic;
static void Main(string[] args)
{

//显示在任何反射操作之前堆的大小
Show("Before doing anything");

//为MScorlib.dll中的所有方法构建MethodInfo对象缓存
List<MethodBase> methodInfos = new List<MethodBase>();
foreach (Type t in typeof(Object).Assembly.GetExportedTypes())
{
//跳过所有泛型类型
if (t.IsGenericTypeDefinition)
{
continue;
}

MethodBase[] mb = t.GetMethods(c_bf);
methodInfos.AddRange(mb);
}

//显示当绑定所有方法之后,方法的个数和堆的大小
Console.WriteLine("# of methods={0:N0}", methodInfos.Count);
Show("After building cache of MethodInfo obejcts");

//为所有MethodInfo对象构建RuntimeMethodHandle缓存
List<RuntimeMethodHandle> methodHandles = methodInfos.ConvertAll<RuntimeMethodHandle>(m => m.MethodHandle);

Show("Holding MethodInfo and RuntimeMethodHandle cache");
GC.KeepAlive(methodInfos); //组织缓存被过早垃圾回收
methodInfos = null; //现在允许缓存垃圾回收
Show("After freeing MethodInfo objects");

methodInfos = methodHandles.ConvertAll<MethodBase>(rmh => MethodBase.GetMethodFromHandle(rmh));

Show("Size of heap after re-creating MethodInfo objects");
GC.KeepAlive(methodHandles);
GC.KeepAlive(methodInfos);

methodHandles = null;
methodInfos = null;

Show("After freeing MethodInfos and RuntimeMethodHandles");

Console.ReadLine();
}

private static void Show(string s)
{
Console.WriteLine("Heap size={0,12:N0} - {1}", GC.GetTotalMemory(true), s);
}
}