雁过请留痕...
代码改变世界

《CLR via C#》笔记——程序集的加载和反射(1)

2012-07-16 17:25  xiashengwang  阅读(2570)  评论(1编辑  收藏  举报

一,程序集加载

     JIT编译器在将IL代码编译成本地代码时,会查看IL代码中引用了那些类型。在运行时,JIT编译器利用程序集的TypeRef和AssemblyRef元数据表的记录项来确定哪一个程序集定义了引用的类型。在AssemblyRef元数据记录项中记录了程序集强名称的各个部分—包括名称,版本,公钥标记和语言文化。这四个部分组成了一个字符串标识。JIT编译器尝试将与这个标识匹配的程序集加载到当前的AppDomain中。如果程序集是弱命名的,标识中将只包含名称。

1,Assembly的Load方法

    在内部CLR使用Assembly的Load方法来加载这个程序集,这个方法与Win32的LoadLibray等价。在内部,Load导致CLR对程序集应用一个版本重定向策略。并在GAC中查找程序集,如果没有找到,就去应用程序的基目录,私有路径目录和codebase指定的位置查找。如果是一个弱命名程序集,Load不会向程序集应用重定向策略,也不会去GAC中查找程序集。如果找到将返回一个Assembly的引用,如果没有找到则抛出FileNotFoundException异常。注意:Load方法如果已经加载一个相同标识的程序集只会简单的返回这个程序集的引用,而不会去创建一个新的程序集。

    大多数动态可扩展应用程序中,Assembly的Load方法是程序集加载到AppDomain的首选方式。这种方式需要指定程序集的标识字符串。对于弱命名程序集只用指定一个名字。

2,Assembly的LoadFrom方法

    当我们知道程序集的路径的场合,可以使用LoadFrom方法,它允许传入一个Path字符串,在内部,LoadFrom首先调用AssemblyName的静态方法GetAssemblyName。这个方法打开指定的文件,通过AssemblyRef元数据表提取程序集的标识,然后关闭文件。随后,LoadFrom在内部调用Assembly的Load方法查找程序集。到这里,他的行为和Load方法是一致的。唯一不同的是,如果按Load的方式没有找到程序集,LoadFrom会加载Path路径指定的程序集。另外,Path可以是URL。如:

Assembly assembly = Assembly.LoadFrom(@"http://www.test.com/LibA.dll");

 3,Assembly的LoadFile方法

    这个方法初一看和LoadFrom方法很像。但LoadFile方法不会在内部调用Assembly的Load方法。它只会加载指定Path的程序集,并且这个方法可以从任意路径加载程序集,同一程序集如果在不同的路径下,它允许被多次加载,等于多个同名的程序集加载到了AppDomain中,这一点和上面的两个方法完全不一样。但是,LoadFile并不会加载程序集的依赖项,也就是不会加载程序集引用的其他程序集,这会导致运行时找不到其他参照DLL的异常。要解决这个问题,需要向AppDomain的AssemblyResolve事件登记,在回调方法中显示加载引用的程序集。类似于这样:

        AppDomain.CurrentDomain.AssemblyResolve += new ResolveEventHandler(CurrentDomain_AssemblyResolve);
        static Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args)
        {
            if (args.Name != null)
            {
                return Assembly.LoadFrom(string.Format("{0}\\plugin\\{1}.dll", Application.StartupPath, new AssemblyName(args.Name).Name));
            }
            return null;
        }

特别注意:要测试LoadFile有没有加载引用的DLL,切不可将DLL拷贝到应用程序的根目录下测试,因为该目录是CLR加载程序集的默认目录,在这个目录中如果存在引用的DLL,它会被加载,造成LoadFile会加载引用DLL的假象。可以在根目录下新建一个子目录如plugin,把引用的dll拷贝到这里面进行测试。

4,Assembly的ReflectionOnlyLoadForm方法

  用这个方法得到的程序集中的任何代码都不会被执行,一般用于获取程序集的元数据。该方法加载由路径指定的文件,文件的强名称不会获取,也不会在GAC或其他地方搜索文件。

5,Assembly的ReflectionOnlyLoad方法

     用这个方法得到的程序集中的任何代码也不会被执行,一般用于获取程序集的元数据。但是,该方法会在GAC,应用程序基目录,私有目录和codebase中搜索,但不会应用版本控制策略,也就是你指定哪个版本就是那个版本。要自己运行版本策略,可以参考AppDoman的ApplyPolicy方法。

     ReflectionOnlyLoadForm和ReflectionOnlyLoad方法也不会加载引用的程序集,需要显示的在AppDomain的ReflectionOnlyAssemblyResovle事件中手动加载引用的程序集,并且必须调用对应的ReflectionOnlyLoadForm或ReflectionOnlyLoad方法加载引用的程序集。其实这保证了权限的一致,因为这两个方法加载的程序集不能执行,他们引用的程序集当然也不应该比这个权限更高。

6,将DLL嵌入EXE文件

    许多应用程序由要依赖于众多的DLL的exe文件构成。部署这个程序需要部署所有的文件。可以将这些DLL的“生成操作”更改为“嵌入的资源”,这会导致C#编译器将DLL文件嵌入到EXE中,以后只需部署这个EXE文件就可以了。

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

       static Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args)
        {
            string resourceName = "WindowsApplication3.lib." + new AssemblyName(args.Name).Name + ".dll";
            
            using(System.IO.Stream stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(resourceName))
            {
                byte[] buffer = new byte[stream.Length];
                stream.Read(buffer, 0, buffer.Length);
                return Assembly.Load(buffer);
            }           
        }

 现在,一个线程首次调用一个方法时,如果该方法用到了引用DLL的一个类型,就会引发一个AssemblyResolve事件,上面的代码会找到嵌入的DLL资源。注意上面的Load方法的实参是一个Byte[]类型。

二,使用反射构建可扩展的应用程序

    元数据是由一系列的表来存储的,编译器会为程序集生成类型定义表,字段定义表,方法定义表以及其他表。利用System.Reflection命名空间包含的一些类型,可以写代码来反射(或者叫“解析”)这些元数据表。

    我们可以轻松的枚举一个程序集中的所有类型,以及他们的基类型,实现了那些接口等。我们还可以获得一个类型的字段,方法,事件和属性。以及应用于任何元数据实体的定制Attribute。还有一些方法可以返回一个方法的IL字节流。利用这些功能,我们能够构建一个类似于ILDasm.exe的工具。

    事实上,极少的应用程序会使用反射。FCL的序列化使用了反射;VS窗体设计器在放置控件时,利用反射来决定向开发人员显示的属性。在运行时,需要从一个特定的程序集加载一个特定的类型,以执行特定的任务时,也要用到反射。以这种绑定到类型并调用其方法通常称为晚期绑定(late binding)。

三,反射的性能

  反射是一个相当强大的机制,它允许我们在运行时构建并使用一个在编译时还不了解的类型及其成员。但它也有两个缺点。

  • 反射会造成编译时无法保证类型的安全性。由于反射使用字符串,所有会丧失编译时的安全性。如果在运行时找不到字符串标明的类型,就会抛出异常。
  • 反射速度慢。由于类型及其成员在编译时未知,需要用字符串来进行标识。System.Reflection空间下的类型在扫描程序集的元数据时,反射要不断的执行字符串搜索。通常字符串的搜索是不执行大小写比较的,这会进一步影响速度。

  使用反射调用一个成员时,也会对性能产生影响。用反射调用一个方法时,首先要将实参打包成一个数组,在内部,反射将这个数组解包到线程栈上。此外,在调用方法前,CLR要检查实参的数据类型。最后,CLR还要保证调用者拥有正确的安全权限来访问调用的成员。

  基于上面的原因,最好避免利用反射来访问字段或者调用方法/属性。如果需要在运行时动态构造实例,可以使用下面的两种技术。

  • 让类型从一个编译时已知的基类派生。在运行时,利用反射构建派生类的实例,将它的引用放到基类的一个变量中(利用转型)。再调用基类中定义的虚方法。
  • 让类型实现一个编译时已知的接口。在运行时,利用反射构建类型的实例,将它的引用放到接口的一个变量中(利用转型)。再调用接口定义的方法。

  关于接口和基类的选择,一般情况应优先选用接口。不过,在需要版本控制的情形中,基类会更加适合,随时向基类中添加成员,派生类会直接继承它,不用重新编译。而接口中添加一个成员后,所有实现该接口的类型都必须修改并重新编译。另外要注意一点,强烈建议基类或是接口类型在自己的程序集中定义,这有助于缓解版本控制问题。(其实,个人理解这样做的原因在于更符合“依赖倒置”原则,使用接口的程序集才是需求方)。

1,发现程序集中定义的类型

  最常用的方法是Assembly的GetExportedType方法。例子:

        private void button1_Click(object sender, EventArgs e)
        {
            string assemblyId = "System.Data, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089";

            DispalyAssemblyTypes(assemblyId);
        }

        private void DispalyAssemblyTypes(string assemblyId)
        {
            Type[] findTypes = Assembly.Load(assemblyId).GetExportedTypes();
            
            foreach (var type in findTypes)
            {
                Console.WriteLine(type.ToString());
            }
        }

  关于上面的assemblyId字符串,这个字符串构造不用可以去记忆,也不可能记得住。特别是PublicKeyToken。可以借助其他工具,如Reflector就可以轻松得到这个字符串。

2,类型对象的准确含义

  上述代码遍历了Sytem.Type构成的数组,Sytem.Type类型是执行类型和对象操作的起点。Sytem.Type是一个抽象类,它派生自System.Reflection.MemberInfo(因为Type可能是另一个类型的成员)。FCL提供了几个从Sytem.Type派生的类型。包括:System.RuntimeType,Sytem.ReflectionOnlyType,Sytem.Reflection.TypeDelegator,以及System.Reflection.Emit空间下的EnumBuilder,GenericTypeParameterBuilder和TypeBuilder。

  RuntimeType是FCL内部使用的一个类型,一个类型在AppDomain中首次被创建时就会创建一个RuntimeType的实例。在同一个AppDomain中,每个类型只能有一个RuntimeType,所以可以用相等和不等符号来判断两个对象是不是同一类型。如:o1.GetType() == o2.GetType()。获取Type对象的方式可以有一些几种:

  • 对象的GetType方法。如:o1.GetType()。
  • Sytem.Type类型提供的静态方法GetType。这个方法只能指定一个类型的完全限定名的字符串。如:
Type t = Type.GetType("System.Int32");

     并且可以传递程序集的类型字符串,如:

Type t2 = Type.GetType("System.Int32,mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089");
  • Sytem.Type类型提供的静态方法ReflectionOnlyGetType。这个方法得到的类型不能执行。
  • Sytem.Type类型提供的实例方法GetNestedType和GetNestedTypes。
  • System.Reflection.Assembly类型提供的实例方法GetType,GetTypes和GetExportedTypes。
  • System.Reflection.Module类型提供的实例方法GetType,GetTypes和FindTypes。
  • C#特有的操作符typeof,但参数只能是已知的类型,既编译时已知的类型。通常可用该操作符来将晚期绑定的类型信息和早期绑定的类型信息进行比较。如:
        private void SomeMethod(object o)
        {
            if (o.GetType() == typeof(System.Int32))
            {
                //...
            }
            if (o.GetType() == typeof(System.String))
            {
                //...
            }
        }

  上述代码的测试是精确匹配,而非兼容匹配(使用转型或C#的is/as操作符是,使用的就是兼容匹配)

得到Type对象的引用后,可以查阅它的属性,如:IsPublic,IsSealed,IsAbstract,IsClass,IsValueType。还有一些属性,如:Assembly,AssemblyQualifiedName(Type的程序集限定名称,如:"System.Int32,mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"),FullName。

3,利用反射构建Exception派生类型的一个层次结构

 private void DispalyExcepionHierachy()
        {
            LoadAssemblies();
            //递归生成类层次结构
            Func<Type, string> classNameAddBase = null;
            classNameAddBase = (t) =>
            {
                string temp = "-" + t.FullName +
                    (t.BaseType != typeof(object) ? classNameAddBase(t.BaseType) : string.Empty);
                return temp;
            };
            //在AppDomain加载的所有程序集中,查找派生自Exception的public类型
            var exceptionTree = (from a in AppDomain.CurrentDomain.GetAssemblies()
                                 from t in a.GetExportedTypes()
                                 where t.IsClass && t.IsPublic && typeof(Exception).IsAssignableFrom(t)
                                 let typeHierarchyTemp = classNameAddBase(t).Split('-').Reverse().ToArray()
                                 let typeHierarchy = string.Join("-", typeHierarchyTemp, 0, typeHierarchyTemp.Length - 1)
                                 orderby typeHierarchy
                                 select typeHierarchy).ToArray();
            Console.WriteLine("{0} Exception types found. ,exceptionTree.Length);
            foreach (var s in exceptionTree)
            {
                //分离Excepton的基类部分
                string[] x = s.Split('-');
                //根据基类型的数量来缩进,显示最远的派生类型
                Console.WriteLine(new string(' ', 3*(x.Length - 1)) + x[x.Length - 1]);
            }
        }

        private void LoadAssemblies()
        {
            string[] assemblies = {
                "System,                      PublicKeyToken={0}",
                "System.Data,                 PublicKeyToken={0}",
                "System.Windows.Forms,        PublicKeyToken={0}",
                "System.Xml,          PublicKeyToken={0}"
            };

            string publicKeyToken = "b77a5c561934e089";

            //获取包含object的程序集的版本,假设其他的程序集和它一样
            Version version = typeof(object).Assembly.GetName().Version;

            foreach (var a in assemblies)
            {
                string assemblyIdentity = string.Format(a, publicKeyToken) + ",Culture=neutral,Version=" + version.ToString();
                Assembly.Load(assemblyIdentity);
            }
        }

 运行结果如下,内容太多,摘取一部分。

187 Exception types found. 
System.Exception
   Microsoft.CSharp.RuntimeBinder.RuntimeBinderException
   Microsoft.CSharp.RuntimeBinder.RuntimeBinderInternalCompilerException
   System.AggregateException
   System.ApplicationException
      Microsoft.VisualStudio.Debugger.Runtime.CrossThreadMessagingException
      System.Reflection.InvalidFilterCriteriaException
      System.Reflection.TargetException
      System.Reflection.TargetInvocationException
      System.Reflection.TargetParameterCountException
      System.Threading.WaitHandleCannotBeOpenedException
   System.Configuration.SettingsPropertyIsReadOnlyException
   System.Configuration.SettingsPropertyNotFoundException
   System.Configuration.SettingsPropertyWrongTypeException
   。。。略

4,构造类型的实例

    拥有一个对Type派生类对象的引用后,就可以构造该类型的实例了。为此,FCL提供了以下几个机制。·System.Activator的CreateInstance方法。这个方法有几个重载,最简单的方式就是传递一个Type的引用。也可以传递类型的一个字符串,但这时还得传递一个程序集的名称,因为CLR并不敢保证要创建的类型就在当前的程序集中。参数还允许指定AppDomain,这个会稍微复杂些,指定AppDomain后返回的对象都是一个System.Runtime.Remoting.ObjectHandle对象(派生自System.MarshalByRefObject),ObjectHandle允许将一个AppDomain中创建的对象传至其他的AppDomain中。要具体化ObjectHandle包装的对象时,可以调用ObjectHandle的Unwrap方法。

跨AppDomain的场景中,如果是按引用封送,会创建代理类型和对象。如果是按值封送,对象的副本会被序列化。但要注意:如果指定的AppDomain就是当前的AppDomain,不会封送,也就是不会创建代理类型和对象,只会进行简单的包装。示例代码:

Cat cat = (Cat)Activator.CreateInstance(t);
  • System.Activator的CreateInstanceFrom方法。这个方法和CreateInstance类似,只是它只能接受字符串形式的参数,并且程序集的参数是一个Path,在内部用Assembly的LoadFrom加载程序集。这个方法也可以指定AppDomain,和CreateInstance用法相同。示例代码:
        AppDomain ad2 = AppDomain.CreateDomain("AD #2");
            ObjectHandle oh = Activator.CreateInstanceFrom(ad2, Assembly.GetEntryAssembly().Location, "AssemblyTest.Cat");
            bool isProxy = RemotingServices.IsTransparentProxy(oh);
            Console.WriteLine(isProxy); //返回true,因为是在“AD #2” AppDomain中创建的
            Cat cat = (Cat)oh.Unwrap();
            cat.Action();
  • System.Type的InvokeMember实例方法。只能在调用AppDomain中创建。示例代码:
Cat cat = (Cat) t.InvokeMember("Cat", BindingFlags.CreateInstance, null, null, null);
  •  System.AppDomain提供的四个实例方法。他们是CreateInstance,CreateInstanceAndUnwrap,CreateInstanceFrom,CreateInstanceFromAndUnwarp。这些方法和Activator的方法相似,允许指定在哪一个AppDomain中构建对象。带Unwarp后缀的方法可以简化一个具体化对象的操作。
  • System.Reflection.ConstructorInfo的Invoke实例方法。只能在调用AppDomain中创建。
             ConstructorInfo ctorInfo = t.GetConstructor(BindingFlags.Instance| BindingFlags.Public, null, System.Type.EmptyTypes , null);
            Cat cat = (Cat)ctorInfo.Invoke(null);
             cat.Action();

注意:对于值类型(struct)的创建,Jeffrey在这里有笔误,原文中说“CLR允许值类型可以不定义构造器。这时只能用Activator的CreateInstance的Type参数版本或Type和Boolean型的那个版本创建实例,因为上述其他的创建方法都要求调用构造器来创建对象。”经本人实验,除了ConstructorInfo外,其他的都能创建没有构造器的值类型。

我定义的值类型如下:

    [Serializable]
    public struct Cat
    {
        public void Action()
        {
            Console.WriteLine("cat!");
        }
    }

前面列出的机制可以创建除了数组(Array)和代理(Delegate)以外的所有类型。

对于数组的创建,可以调用Array类的静态方法CreateInstance,如:

var arrayData =  Array.CreateInstance(typeof(int), 10);

对于代理的创建,可以调用Delegate类的静态方法CreateDelegate方法。如:

        private delegate void CatActionEventHandle();
        private void CreateInstanceOfType(Type t)
        {
            Cat cat = new Cat();
            Delegate action = Delegate.CreateDelegate(typeof(CatActionEventHandle), cat, "Action");
            Delegate sleep = Delegate.CreateDelegate(typeof(CatActionEventHandle), cat, "Sleep");
            Delegate summary = Delegate.Combine(action, sleep);
            foreach (var d in summary.GetInvocationList())
            {
                d.Method.Invoke(d.Target, null);
            }
        }

  对于泛型类型的实例,需要调用Type的实例方法MakeGenericType方法。向它传递一个数组(其中包含作为类型实参使用的类型)。平常工作中一般不会用到,看看这个例子。

          //获取对泛型类型类型对象的一个引用
            Type openType = typeof(Dictionary<,>);
           //用TKey=int,TValue=string封闭泛型类型
            Type closeType = openType.MakeGenericType(typeof(int), typeof(string));
           //构造泛型类型的一个实例
            object instance = Activator.CreateInstance(closeType);
           //验证对象
            Console.WriteLine(instance.ToString());

运行结果:

System.Collections.Generic.Dictionary`2[System.Int32,System.String]

 

未完,下接 《CLR via C#》笔记——程序集的加载和反射(2)