程序集加载和反射 – Answer.Geng – 博客园

来源: 程序集加载和反射 – Answer.Geng – 博客园

前言


本篇讨论程序集的加载及反射。主要涉及到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);
    }
}
分享到: 更多 (0)