返回

程序集加载和反射

程序集加载

我们知道,JIT编译器将方法的IL代码编译成本机代码时,会查看IL代码中引用了哪些类型。在运行时,JIT编译器利用程序集的TypeRefAssemblyRef元数据表来确定哪一个程序集定义了所引用的类型。

Load

在内部, CLR使用System。Reflection。Assembly类的静态Load方法尝试加载这个程序集。

Assembly的Load方法是将程序集加载到AppDomain的首选方式。但它要求事先掌握构成程序集标识的各个部分。

public class Assmbly
{   
   public static Assembly Load(AssemblyName assemblyRef);
   public static Assembly Load(String assemblyString);
   // 未列出不常用Load重载
}

在内部,Lad导致CLR向程序集应用一个版本绑定重定向策略,并在GAC(全局程序集缓存)中查找程序集。如果没找到,就接着去应用程序的基目录、私有路径子目录和codebase位置查找。

LoadFrom

调用Assembly的LoadFrom方法加载指定了路径名的程序集:

public class Assembly
{
    public static Assembly LoadFrom(string path);
}

在内部:

  1. LoadFrom首先调用System。Reflection。AssemblyName类的静态GetAssemblyName方法。
  2. 该方法打开指定的文件,找到AssemblyRef元数据表的记录项,提取程序集标识信息,
  3. 然后以一个System。Reflection。AssemblyName对象的形式返回这些信息。
  4. 随后,LoadFrom方法在内部调用AssemblyLoad方法,将AssemblyName对象传给它。
  5. 然后,CLR应用版本绑定重定向策略,并在各个位置查找匹配的程序集。Load找到匹配程序集会加载它,并返回待办已加载程序集的Assembly对象;LoadFrom方法将返回到这个值。
    • 如果Load没有找到匹配的程序集,LoadFrom会加载通过LoadFrom的实参传递的路径中的程序集。
    • 当然,如果已加载具有相同标识的程序集,LoadFrom方法就会直接返回代表已加载程序集的Assembly对象。

ReflectionOnlyLoad

如果你构建的一个工具只想通过反射来分析程序集的元数据,并希望确保程序集中的任何代码都不会执行,那么加载程序集的最佳方式就是使用AssemblyReflectionOnlyLoadFrom方法或者使用AssemblyReflectionOnlyLoad方法(后者比较少见)。

public class Assembly
{
    public static Assembly ReflectionOnlyLoadFrom(string assemblyFile);
    public static Assembly ReflectionOnlyLoad(string assemblyString);
}

ReflectionOnlyLoadFrom方法加载由路径指定的文件;文件的强名称标识不会获取,也不会在GAC和其他位置搜索文件。

ReflectionOnlyLoad方法会在GAC、应用程序基目录、私有路径和codebase指定的位置搜索指定的程序集。但和Load方法不同的是,ReflectionOnlyLoad方法不会应用版本控制策略

利用反射来分析由这两个方法之一加载的程序集时,代码经常需要向AppDomain的ReflectionOnlyAssemblyResovle事件注册一个回调方法,以便手动加载任何引用的程序集;CLR不会自动帮你做这个事情。回调方法被调用时,它必须调用Assembly的ReflectionOnlyLoadFrom或ReflectionOnlyLoad方法来显式加载引用程序集,并返回对程序集的引用。

关于程序集卸载

CLR不提供卸载单独程序集的能力。 如果CLR允许这样做, 那么一旦线程从某个方法返回至已卸载的一个程序集中的代码, 应用程序就会崩溃, 健壮性和安全性是CLR最优先考虑的目标。

将依赖DLL嵌入程序集

许多应用程序都是由一个要依赖于众多dll文件的exe文件构成。部署应用程序时,所有文件都必须部署。但有一个技术允许只部署一个exe文件。首先标识出exe文件要依赖的、不是作为。NET Framework一部分发布的所有dll文件。然后将这些dll添加到vs项目中。对于添加的每个dll,都显示它的属性,将它的“生成操作”更改为“嵌入的资源”。这会导致C#编译器将dll文件嵌入exe文件中,以后就只需要部署这个exe。

在运行时,CLR会找不到依赖的dll程序集。为了解决这个问题,当应用程序初始化时,向AppDomain的ResolveAssembly事件登记一个回调方法,代码大致如下:

private static Assembly ResolveEventHandler(object senderResolveEventArgs args)
{
    string dllName=new AssemblyName(argsName)Name+".dll";
    var assem = Assembly.GetExecutingAssembly();
    string resourceName = assem.GetManifestResourceNames().FirstOrDefault(c => c.EndsWith(dllName));
    if (resourceName==null)
    {
        return null;//not found,maybe another handler will find it
    }

    using (var stream=assem.GetManifestResourceStream(resourceName))
    {
        byte[] assemblyData=new byte[stream.Length];
        stream.Read(assemblyData, 0, assemblyData.Length);
        return Assembly.Load(assemblyData);
    }
}

现在,线程首次调用一个方法时,如果发现该方法引用了依赖DLL文件中的类型,就会引发一个AssemblyResolve事件,而上述回调代码会找到所需的签入DLL资源,并调用Assembly的Load方法获取一个byte[]实参的重载版本来加载所需的资源。虽然我喜欢将依赖DLL嵌入程序集的技术,但要注意这会增大应用程序在运行时的内存消耗。

使用反射构建动态可扩展程序

众所周知,元数据是用一系列的表存储的。生成程序集或模块时,编译器会创建一个类型定义表、一个字段定义表、一个方法定义表以及其他表。利用System。Reflection命名空间中包含的类型,可以写代码来反射这些元数据表。实际上,这个命名空间中的类型为程序集或模块中包含的元数据提供了一个对象模型。

利用对象模型中的类型,可以轻松枚举类型定义元数据表中的所有类型,而针对每个类型都可获取它的基类型、它实现的接口以及与类型关联的标志。利用System。Reflection命名空间中的其他类型,还可解析对应的元数据表来查询类型的字段、方法、属性和事件。还可发现应用于任何元数据实体的定制特性。

反射的性能

反射缺陷:

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

    由于反射严重依赖字符串,所以会丧失编译时的类型安全性。

  • 反射速度慢。

    使用System。Reflection命名空间中的类型扫描程序集的元数据时,反射机制会不停执行字符串搜索。通常,字符串搜索执行的是不区分大小写的比较,这会进一步影响速度。

基于上述所有原因,最好避免利用反射来访问字段或调用方法/属性。应对方案:

  • 让类型从编译时已知的基类型派生。
  • 让类型实现编译时已知的接口。

发现程序集中定义的类型

反射经常用于判断程序集定义了哪些类型。FCL提供了许多api来获取这方面的信息。目前常用的是Assembly的ExportedTypes属性。 显示其中定义的所有公开导出的类型(也就是public类型)。

static void Main(string[] args)
{
    string dataAssembly = "System.Data,version=4.0.0.0," + "culture=neutral,PublicKeyToken=b77a5c561934e089";
    LoadAssemAndShowPublicTypes(dataAssembly);
}
private static void LoadAssemAndShowPublicTypes(string assemblyName)
{
    //显式地将程序集加载到这个appDomain中
    Assembly a = Assembly.Load(assemblyName);
    //在一个循环中显示已加载程序集中每个公开导出type全名
    foreach (Type t in a.ExportedTypes)
    {
        Console.WriteLine(t.FullName);
    }
}

类型对象的准确含义

System。Type对象代表一个类型引用(而不是类型定义)。

众所周知,System.Object定义了公共非虚实例方法GetType。调用这个方法时,CLR会判定指定对象的类型,并返回对该类型的Type对象的引用。由于在一个AppDomain中,每个类型只有一个Type对象,所以可以使用相等和不相等操作符来判断两个对象是不是相同的类型。

o1.GetType() == o2.GetType();

除了调用Object的GetType方法,FCL还提供了获得Type对象的其他几种方式。

  1. System.Type类型提供了静态GetType方法的几个重载版本。所有版本都接受一个String参数。字符串必须指定类型的全名,而不是编辑器支持的基元类型(int,string,bool等), 这些名称对CLR没有任何意义。
  2. System.Type类型提供了静态的ReflectionOnlyGetType方法。 与上一条行为上相似, 只是类型会以”仅反射”的方式加载, 不能执行。
  3. System.TypeInfo类型提供了实例成员DeclaredNestedTypesGetDeclaredNestedType
  4. System.Reflection。Assembly类型提供了实例成员GetTypeDefinedTypesExportedTypes

许多编程语言都允许使用一个操作符(typeof)并根据编译时已知的类型名来获得Type对象。尽量用这个操作符获取Type引用,而不要使用上述列表中的任何方法,因为操作符生成的代码通畅更快。C#的这个操作符称为typeof,通常用它将晚期绑定的类型信息与早期绑定(编译时已知)的类型信息进行比较。

    //getType在运行时返回对象的类型(晚期绑定)
    //typeof返回指定类的类型(早期绑定)
    if (o.GetType()==typeof(FileInfo)){}

上述代码中, 使用typeof是精确匹配, 而不是兼容匹配

  • 精确匹配: 不检查是否从FileInfo类型派生的对象, 只检查是否引用了FileInfo类型对象。
  • 兼容匹配: 使用转型或者C#is/as操作符时, 测试的就是兼容匹配。

如前所述,Type对象是轻量级的对象引用。要更多地了解类型本身,必须获取一个TypeInfo对象,后者才代表类型定义。可调用System。Reflection.IntrospectionExtensionsGetTypeInfo扩展方法将Type对象转换成TypeInfo对象。还可调用TypeInfoAsType方法TypeInfo对象转换为Type对象

获取TypeInfo对象会强迫CLR确保已加载类型的定义程序集,从而对类型进行解析。这个操作可能代价高昂。

构造类型的实例

获取对Type派生对象的引用之后,就可以构造该类型的实例了。FCL提供了一下几个机制。

  • System.ActivatorCreateInstance方法

    Activator类提供了静态CreateInstance方法的几个重载版本。 可以传递Type对象引用, 也可以传递标识了类型的String, 直接获取类型对象的几个版本较为简单。

  • System.ActivatorCreateInstanceForm方法

    Activator类还提供了一组静态CreateInstanceForm方法,他们与CreateInstance的行为相似,只是必须通过字符串参数来指定类型及其程序集。

  • System.Appdomain的方法

    Appdomain类型提供了4个用于构造类型实例的实例方法,包括CreateInstanceCreateInstanceFromCreateInstanceFromAndUnwrap。这些方法和行为和Activator类的方法相似。区别在于他们都是实例方法,允许指定在哪个Appdomain中构造对象。另外,带Unwrap后缀的方法还能简化操作,不必执行额外的方法调用。

  • System.Reflection.ConstructorInfoInvoke实例方法

    使用一个Type对象引用,可以绑定到一个特定的构造器,并获取对构造器的ConstructorInfo对象的引用。然后,可利用ConstructorInfo对象引用来调用它的Invoke方法。类型总是在调用Appdomain中创建,返回的是对新对象的引用。

创建数组 需要调用Array的静态CreateInstance方法。所有版本的CreateInstance方法获取的第一个参数都是对数组元素Type的引用CreateInstance的其他参数允许指定数组位数维数上下限的各种组合。

创建委托 则要调用MethodInfo的静态CreateDelegate方法。所有版本的CreateDelegate方法获取的第一个参数都是对委托Type的引用。CreateDelegate方法的其他参数允许指定在调用实例方法时应将哪个对象作为this参数传递。

构造泛型类型的实例首先要获取对开放类型的引用,然后调用TypeMakeGenericType方法并向其传递一个数组(其中包含要作为类型实参使用的类型)。然后,获取返回的Type对象并把它传给上面列出的某个方法。

internal sealed class Dictionary<TKeyTValue>{}

public static class Program
{
    static void Main(string[] args)
    {
        // 获取对泛型类型的类型对象的引用 , 没限定泛型的类型就是开放类型
        Type openType = typeof(Dictionary<,>);

        // 使用Tkey=string、Tvalue=int封闭泛型类型
        // 限定了泛型类型, 就是封闭类型
        Type closedType = openType.MakeGenericType(typeof(string), typeof(int));

        // 构造封闭类型的实例
        Object o = Activator.CreateInstance(closedType);
        // 证实能正常工作
        ConsoleWriteLine(o.GetType());
        // 输出:ConsoleApp2.Dictionary`2[System。String,System。Int32]
    }
}

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

构建可扩展应用程序时, 接口是中心。可用基类代替接口,但接口通常是首选的。因为它允许加载项开发人员选择他们自己的基类。 例如要写一个应用程序来无缝加载和使用别人的类型。 下面描述了如何设计:

  • 创建宿主SDK程序集, 它定义一个接口。

    接口的方法作为宿主应用程序加载项之间的通信机制使用。 接口方法定义参数和返回类型时, 请尝试使用MSCorLib。dll中定义的其他接口类型。 要传递并返回自己的数据类型, 也在宿主SDK程序集中定义。 一定搞定接口定义, 就可为这个程序集赋予强名称。 然后打包并部署到用户那里。 发布以后要避免对该程序集中的类型做出任何重大的改变。 例如: 不要以任何方式更改接口。 如果定义了任何数据类型, 在类型中添加新成员时完全允许的。

  • 加载项开发人员会在加载项程序集中定义自己的类型。

    这些程序集引用你的宿主程序集中的类型。 加载项开发人员可按自己的步调推出程序集的新版本, 而宿主应用程序能正常使用加载项中的类型。

  • 创建单独的”宿主应用程序“ 程序集, 在其中包含你的应用程序的类型。

    这个程序集显然要引用宿主SDK, 并使用其中定义的类型。 可自由修改宿主应用程序程序集的代码, 由于加载项开发人员不会引用这个宿主应用程序程序集, 所以随时都能退出宿主应用程序程序集的新版本。 不会对加载项开发人员产生任何影响。

使用反射发现类型成员

发现类型的成员

字段、构造器、方法、属性、事件和嵌套类型都可以定义成类型的成员。FCL包含抽象基类System.Reflection.MemberInfo,封装了所有类型成员都通用的一组属性。MemberInfo有许多派生类, 每个都封装了与特定类型成员相关的更多属性.

代码处理的是由调用AppDomain加载的所有程序集定义的所有公共类型。 对每个类型都调用DeclaredMembers属性以返回由MemberInfo派生对象构成的集合;每个对象都引用类型中定义的一个成员。 然后显示每个成员的种类(字段,构造器,方法和属性等)及其字符串值(调用ToString来获取)。

using System;
using System.Reflection;

class Program
{
    static void Main(string[] args)
    {
        //遍历这个appDomain中加载的所有程序集
        Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies();
        foreach (var a in assemblies)
        {
            // 缩进0*3个空格, a代表单个程序集
            // Assembly:mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
            Show(0, "Assembly:{0}", a);
            //查找程序集中的类型
            // a.ExportedTypes 是所有类型的集合
            foreach (var t in a.ExportedTypes)
            {
                //    Assembly:Microsoft.Win32.Registry
                // 缩进1*3个空格 t代表单个导出类型
                Show(1, "Assembly:{0}", t);
                //发现类型的成员
                // t.GetTypeInfo().DeclaredMembers 类型的所有成员
                foreach (var mi in t.GetTypeInfo().DeclaredMembers)
                {
                    string typeName                     = string.Empty;
                    if (mi is Type) typeName            = "(Nested) Type";
                    if (mi is FieldInfo) typeName       = "FieldInfo";
                    if (mi is MethodInfo) typeName      = "MethodInfo";
                    if (mi is ConstructorInfo) typeName = "ConstructorInfo";
                    if (mi is PropertyInfo) typeName    = "PropertyInfo";
                    if (mi is EventInfo) typeName       = "EventInfo";

                    Show(2, "{0}:{1}", typeName, mi);
                }
            }
        }
    }

    private static void Show(int indent, string format, params object[] args)
    {
        Console.WriteLine(new string(' ', 3 * indent) + format, args);
    }
}

在查询DeclaredMembers属性所返回的集合中,每个元素都是对层次结构中的一个具体类型的引用。虽然TypeInfoDeclaredMembers属性能返回类型的所有成员,但还可利用TypeInfo提供的一些方法返回具有指定字符串名称的成员类型。例如,利用TypeInfoGetDeclaredNestedTypeGetDeclaredField等。 而利用GetDeclaredMethods方法能返回由MethodInfo对象构成的集合。

总结了用于遍历反射对象模型的各种类型:

  • 基于AppDoamin,可发现其中加载的所有程序集, 可发现它的所有模块
  • 基于程序集或模块, 可发现它定义的所有类型
  • 基于类型,可以发现它的嵌套类型,字段,构造器,方法,属性和事件
  • 基于一个类型,还可发现它实现的接口。
  • 基于构造器、方法、属性访问器方法或者事件的添加、删除方法,可调用GetParameters方法来获取由ParameterInfo对象构成的数组,从而了解成员的参数的类型。
  • 还可查询只读属性ReturnParameter获得一个ParameterInfo对象,他详细描述了成员的返回类型。
  • 对于泛型类型或方法,可调用GetgenericArguments方法来获得类型参数的集合。

最后,针对上述任何一项,都可查询CustomAttributes属性来获得应用于它们的自定义定制特性的集合。

调用类型的成员

发现类型定义的成员后可调用它们. “调用”(invoke)的确切含义取决于要调用的成员的种类.

  • PropertyInfo类代表与属性有关的元数据信息;

    提供了CanReadCanWritePropertyType只读属性,只读GetMethodSetMethod属性。

  • EventInfo类型代表与事件有关的元数据信息。

    提供了只读EventHandlerType属性只读AddMethodRemoveMethod属性,返回为事件增删委托的方法的MethodInfo对象。

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

许多应用程序都绑定了一组类型(Type对象)或类型成员(MemberInfo派生对象),并将这些对象保存在某种形式的集合中。以后,应用程序搜索这个集合,查找特定对象,然后调用(invoke)这个对象。只是有个小问题:TypeMemberInfo派生对象需要大量内存。如果需要保存/缓存大量Type和MemberInfo派生对象,开发人员可以使用句柄(Runtime Handle)代替对象以减小工作集内存。

FCL定义了三个运行时句柄类型(全部都在System命名空间),包括RuntimeTypeHandleRuntimeFieldHandleRuntimeMethodHandle。三个类型都是值类型,都只包含一个字段,也就是一个IntPtr。

IntPtr字段是一个句柄引用AppDomain的Loader堆中的一个类型、字段或方法。因此,现在需要以一种简单、高效的方式将重量级的Type或MemberInfo对象转换为轻量级的运行时句柄实例,反之亦然。幸好,使用以下转换方法和属性可轻松达到目的。

  • Type对象 转为RuntimeTypeHandle , 调用Type的静态GetTypeHandle方法 并传递那个Type对象引用。

    • 反向转换, 调用Type的静态方法GetTypeFromHandle并传递那个RuntimeTypeHandle对象引用。
  • 要将FieldInfo对象 转为RuntimeTypeHandle , 查询FieldInfo 的实例只读属性FieldHandle

    • 反向转换, 调用FieldInfo静态方法GetFieldFromHandle
  • 要将MethodInfo对象 转换为一个RuntimeMethodHandle, 查询MethodInfo 的实例只读属性MethodHandle

  • 反向转换, 调用MethodInfo静态方法GetMethodFromHandle

以下实例程序获取许多MethodInfo对象,把它们转换为RuntimeMethodHandle实例,并演示了转换前后的工作集的差异。

Licensed under CC BY-NC-SA 4.0
0