程序集加载和反射
作者:追风剑情 发布于:2020-12-17 13:46 分类:C#
.NET Framework中的动态编程
.NET中的反射
巴科斯-诺尔范式(BNF)语法
我们知道,JIT编译器将方法的IL代码编译成本机代码时,会查看IL代码中引用了哪些类型。在运行时,JIT编译器利用程序集的 TypeRef 和 AssemblyRef 元数据表来确定哪一个程序集定义了所引用的类型。在 AsembyRef 元数据表的记录项中,包含了构成程序集强名称的各个部分。JIT编译器获取所有这些部分——包括名称(无扩展名和路径)、版本、语言文化和公钥标记(public key token)——并把它们连接成一个字符串。然后,JIT编译器尝试将与该标识匹配的程序集加载到 AppDomain 中(如果还没有加载的话)。如果被加载的程序集是弱命名的,那么标识中就只包含程序集的名称(不包含版本、语言文化及公钥标记信息)。
在内部,CLR 使用 System.Reflection.Assembly 类的静态 Load 方法尝试加载这个程序集。该方法在.NET Framework SDK 文档中是公开的,可调用它显式地将程序集加载到AppDomain 中。该方法是 CLR 的与 Win32 LoadLibrary 函数等价的方法。Assembly的 Load方法实际有几个重载版本。以下是最常用的重载的原型:
public class Assembly { public static Assembly Load(AssemblyName assemblyRef); public static Assembly Load(String assemblyString); //未列出不常用的Load重载 }
在内部,Load 导致 CLR 向程序集应用一个版本绑定重定向策略,并在 GAC(全局程序集缓存)中查找程序集。如果没找到,就接着去应用程序的目录、私有路径子目录和 codebase位置查找。如果调用 Load 时传递的是弱命名程序集,Load 就不会向程序集应用版本绑定重定向策略,CLR 也不会去 GAC 查找程序集。如果 Load 找到指定的程序集,会返回对代表已加载的那个程序集的一个 Assembly 对象的引用。如果 Load 没有找到指定程序集,会抛出一个 System.IO.FileNotFoundException 异常。
"SomeAssembly, Version=2.0.0.0, Culture-neutral, PublickeyToken=01234567890abcde, ProcessorArchitecture=MSIL" CLR目前允许ProcessorArchitecture取5个值之一:MSIL(Microsoft IL),X86、IA64,AMD64 以及 Arm。
在大多数动态可扩展应用程序中,Assembly的Load方法是将程序集加载到AppDomain的首选方式。但它要求事先掌握构成程序集标识的各个部分。开发人员经常需要写一些工具或实用程序(例如ⅡLDasm.exe, PEVerity.exe, CorFlags.exe, GACUtil.exe, SGen.exe, SN.exe和 XSD.exe 等)来操作程序集,它们都要获取引用了程序集文件路径名(包括文件扩展名)的命令行实参。
调用 Assembly 的 LoadFrom 方法加载指定了路径名的程序集:
public class Assembly { public static Assembly LoadFrom(String path); //未列出不常用的 LoadFrom 重载 }
在内部,LoadFrom首先调用System.Reflection.AssemblyName类的静态 GetAssemblyName方法。该方法打开指定的文件,找到AssemblyRef元数据表的记录项,提取程序集标识信息,然后以一个System.Reflection.AssemblyName对象的形式返回这些信息(文件同时会关闭)。随后,LoadFrom 方法在内部调用Assembly的Load方法,将AssemblyName对象传给它。然后,CLR 应用版本绑定重定向策略,并在各个位置查找匹配的程序集。Load 找到匹配程序集会加载它,并返回代表已加载程序集的Assembly对象;LoadFrom 方法将返回这个值。如果Load没有找到匹配的程序集,LoadFrom会加载通过LoadFrom的实参传递的路径中的程序集。当然,如果已加载了具有相同标识的程序集,LoadFrom方法就会直接返回代表已加载程序集的Assembly对象。
顺便说一句,LoadFrom方法允许传递一个URL作为实参,下面是一个例子:
Assembly a = Assembly.LoadFrom(@"http://Wintellect.com/SomeAssembly.dll");
如果传递的是一个Internet位置,CLR会下载文件,把它安装到用户的下载缓存中,再从那儿加载文件。注意,当前必须联网,否则会抛出异常。但如果文件之前已下载过,而且Microsoft Internet Explorer被设为脱机工作(在 Internet Explorer 中单击“文件”|“脱机工作”),就会使用以前下载的文件,不会抛出异常。还可以调用 UnsafeLoadFrom,它能够加载从网上下载的程序集,同时绕过一些安全检查。
Microsoft Visual Studio的UI设计人员和其他工具一般用的是 Assembly 的 LoadFile 方法。这个方法可从任意路径加载程序集,而且可以将具有相同标识的程序集多次加载到一个AppDomain 中。在设计器/工具中对应用程序的 UI 进行了修改,而且用户重新生成了程序集时,便有可能发生这种情况。通过 LoadFile 加载程序集时,CLR 不会自动解析任何依赖性问题;你的代码必须向 AppDomain 的 AssemblyResolve 事件登记,并让事件回调方法显式地加载任何依赖的程序集。
如果你构建的一个工具只想通过反射来分析程序集的元数据,并希望 确保程序集中的任何代码都不会执行,那么加载程序集的最佳方式就是使用 Assembly 的 ReflectionOnlyLoadFrom 方法或者使用 Assembly 的 ReflectionOnlyLoad 方法(后者比较少 见)。下面是这两个方法的原型:
public class Assembly { public static Assembly ReflectionOnlyLoadFrom(String assemblyFile); public static Assembly ReflectionOnlyLoad(String assemblystring); //未列出不常用的 ReflectionOnlyLoad 重载 }
ReflectionOnlyLoadFrom方法加载由路径指定的文件;文件的强名称标识不会获取,也不会在GAC和其他位置搜索文件。ReflectionOnlyLoad 方法会在GAC、应用程序基目录,私有路径和 codebase 指定的位置搜索指定的程序集。但和 Load 方法不同的是,RefectionOnlyLoad 方法不会应用版本控制策略,所以你指定的是哪个版本,获得的就是哪个版本。要自行向程序集标识应用版本控制策略,可将字符串传给AppDomain的ApplyPolicy 方法。
用RenectionOnlyLoadFrom或ReflectionOnlyLoad方法加载程序集时,CLR禁止程序集中的任何代码执行;试图执行由这两个方法加载的程序集中的代码,会导致CLR抛出一个InvalidOperationException异常。这两个方法允许工具加载延迟签名的程序集,这种程序集正常情况下会因为安全权限不够而无法加载。另外,这种程序集也可能是为不同的CPU架构而创建的。
利用反射来分析由这两个方法之一加载的程序集时,代码经常需要向AppDomain的ReflectionOnlyAssemblyResovle事件注册一个回调方法,以便手动加载任何引用的程序集(如有必要,还需要调用AppDomain的ApplyPolicy方法): CLR不会自动帮你做这个事情。回调方法被调用(invoke)时,它必须调用(call) Assembly的ReflectionOnlyLoadFrom或ReflectionOnlyLoad方法来显式加载引用的程序集,并返回对该程序集的引用。
使用 ReflectionOnlyLoadFrom 或 ReflectionOnlyLoad 方法加载的程序集表面上是可以卸载的。毕竟,这些程序集中的代码是不允许执行的。但CLR 一样不允许卸载用这两个方法加载的程序集。因为用这两个方法加载了程序集之后,仍然可以利用反射来创建对象,以便引用这些程序集中定义的元数据。如果卸载程序集,就必须通过某种方式使这些对象失效。无论是实现的复杂性,还是执行速度,跟踪这些对象的状态都是得不偿失的。
许多应用程序都由一个要依赖于众多DLL文件的EXE文件构成。部署应用程序时,所有文件都必须部署。但有一个技术允许只部署一个EXE文件。首先标识出EXE文件要依赖的、不是作为 Microsoft .NET Framework 一部分发布的所有DLL文件。然后将这些DLL添加到 Visual Studio 项目中。对于添加的每个DLL,都显示它的属性,将它的“生成操作”更改为“嵌入的资源”。这会导致C#编译器将DLL文件嵌入EXE文件中,以后就只需部署这个EXE。
在运行时,CLR会找不到依赖的DLL程序集。为了解决这个问题,当应用程序初始化时,向AppDomain的ResolveAssembly事件登记一个回调方法。代码大致如下:
private static Assembly ResolveEventHandler(Object sender, ResolveEventargs args) { String dllName = new AssemblyName(args.Name).Name +",dll"; var assem = Assembly.GetExecutingAssembly(); String resourceName = assem.GetManifestResourceNames().FirstorDefault(rn =>rn.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命名空间中包含的类型,可以写代码来反射(或者说“解析”)这些元数据表。实际上,这个命名空间中的类型为程序集或模块中包含的元数据提供了一个对象模型。
利用对象模型中的类型,可以轻松枚举类型定义元数据表中的所有类型,而针对每个类型都可获取它的基类型、它实现的接口以及与类型关联的标志(flag)。利用System.Reflection命名空间中的其他类型,还可解析对应的元数据表来查询类型的字段、方法、属性和事件。还可发现应用于任何元数据实体的定制特性。甚至有些类允许判断引用的程序集;还有一些方法能返回一个方法的IL字节流。利用所有这些信息,很容易构建出与Microsoft的ILDasm.exe相似的工具。
事实上,只有极少数应用程序才需使用反射类型。如果类库需要理解类型的定义才能提供丰富的功能,就适合使用反射。例如,FCL 的序列化机制就是利用反射来判断类型定义了哪些字段。然后,序列化格式器(serialization formatter)可获取这些字段的值,把它们写入字节流以便通过Internet传送、保存到文件或复制到剪贴板。类似地,在设计期间,Microsoft Visual Studio设计器在Web窗体或Windows窗体上放置控件时,也利用反射来决定要向开发人员显示的属性。
在运行时,当应用程序需要从特定程序集中加载特定类型以执行特定任务时,也要使用反射。例如,应用程序可要求用户提供程序集和类型名。然后,应用程序可显式加载程序集,构造类型的实例,再调用类型中定义的方法。这种用法在概念上类似于调用 Win32 LoadLibrary和GetProcAddress函数。以这种方式绑定到类型并调用方法称为晚期绑定。(对应的,早期绑定是指在编译时就确定应用程序要使用的类型和方法。)
反射是相当强大的机制,允许在运行时发现并使用编译时还不了解的类型及其成员。但是,它也有下面两个缺点:
● 反射造成编译时无法保证类型安全性。由于反射严重依赖字符串,所以会丧失编译时的类型安全性。例如,执行Type.GetType("int");要求通过反射在程序集中查找名为"int”的类型,代码会通过编译,但在运行时会返回mull,因为CLR只知"System.Int32”,不知"int"。
● 反射速度慢。使用反射时,类型及其成员的名称在编译时未知;你要用字符串名称标识每个类型及其成员,然后在运行时发现它们。也就是说,使用System.Reflection命名空间中的类型扫描程序集的元数据时,反射机制会不停地执行字符串搜索。通常,字符串搜索执行的是不区分大小写的比较,这会进一步影响速度。
使用反射调用成员也会影响性能。用反射调用方法时,首先必须将实参打包(pack)成数组;在内部,反射必须将这些实参解包(unpack)到线程栈上。此外,在调用方法前,CLR必须检查实参具有正确的数据类型。最后,CLR必须确保调用者有正确的安全权限来访问被调用的成员。
基于上述所有原因,最好避免利用反射来访问字段或调用方法/属性。应该利用以下两种技术之一开发应用程序来动态发现和构造类型实例:
● 让类型从编译时已知的基类型派生。在运行时构造派生类型的实例,将对它的引用放到基类型的变量中(利用转型),再调用基类型定义的虚方法。
● 让类型实现编译时已知的接口。在运行时构造类型的实例,将对它的引用放到接口类型的变量中(利用转型),再调用接口定义的方法。
在这两种技术中,我个人更喜欢使用接口技术而非基类技术,因为基类技术不允许开发人员选择特定情况下工作得最好的基类。不过,需要版本控制的时候基类技术更合适,因为可随时向基类型添加成员,派生类会直接继承该成员。相反,要向接口添加成员,实现该接口的所有类型都得修改它们的代码并重新编译。
使用这两种技术时,强烈建议接口或基类型在它们自己的程序集中定义,这有助于缓解版本控制问题。
1、发现程序集中定义的类型
反射经常用于判断程序集定义了哪些类型。FCL提供了许多API来获取这方面的信息。目前最常用的API是Assembly的ExportedTypes属性。下例加载一个程序集,并显示其中定义的所有公开导出的类型”(所谓公开导出的类型,就是程序集中定义的 publike类型,它们在程序集的外部可见):
using System; using System.Reflection; public static class Program { public static void Main() { String datanssembly = "System.Data, versio=4.0.0.0," + "culture=neutral, PublickeyToken=b77a5c561934e089"; LoadAssemAndShowPublicTypes(dataAssembly); } private static void LoadAssemAndShowPublicTypes(string assemid){ //显式地将程序集加载到这个AppDomain中 Assembly a = Assembly.Load(assemid); //在一个循环中显示已加载程序集中每个公开导出Type的全名 foreach (Type t in a.ExportedTypes) { //显示类型全名 Console.WriteLine(t.FullName); } } }
2、类型对象的准确含义
注意,上述代码遍历System.Type对象构成的数组。System.Type类型是执行类型和对象 操作的起点。System.Type对象代表一个类型引用(而不是类型定义)。
众所周知,System.Object定义了公共非虚实例方法GetType。调用这个方法时,CLR会判 断指定对象的类型,并返回对该类型的Type对象的引用。由于在一个AppDomain中,每 个类型只有一个Type对象,所以可以使用相等和不等操作符来判断两个对象是不是相同 的类型:
private static Boolean AreObjectsTheSameType(Object o1, Object o2) { return o1.GetType() == o2.GetType(); }
除了调用Object的GetType方法,FCL还提供了获得Type对象的其他几种方式:
● System.Type类型提供了静态GerType方法的几个重载版本。所有版本都接受一个String参数。字符串必须指定类型的全名(包括它的命名空间)。注意不允许使用编译器支持的基元类型(比如C#的int, string, bool等),这些名称对于CLR没有任何意义。如果传递的只是一个类型名称,方法将检查调用程序集,看它是否定义了指定名称的类型如果是,就返回对恰当Type对象的引用。
如果调用程序集没有定义指定的类型,就检查MSCorLib.dll定义的类型。如果还是没有找到,就返回null或抛出System.TypeLoadException(取决于调用的是GetType方法的哪个重载,以及传递的是什么参数)。文档对该方法进行了完整解释。
可向GerType传递限定了程序集的类型字符串,比如"System.Int32,mscorib, Version=4.0.0.0, Culture=neutral, Publickeytoken=b77a5e561934e089”。在本例中,GetType会在指定程序集中查找类型(如有必要会加载程序集)。
● System.Type类型提供了静态ReflectionOnlyGetType方法。该方法与上一条提到的
GetType方法在行为上相似,只是类型会以“仅反射”的方式加载,不能执行。
● System.TypeInfo类型提供了实例成员DeclaredNestedTypes和GetDeclaredNestedType。
● System.Reflection.Assembly类型提供了实例成员GetType,DefinedTypes和ExportedTypes。
许多编程语言都允许使用一个操作符并根据编译时已知的类型名称来获得Type对象。尽 量用这个操作符获得Type引用,而不要使用上述列表中的任何方法,因为操作符生成的 代码通常更快。C#的这个操作符称为typeof,通常用它将晚期绑定的类型信息与早期绑定 (编译时已知)的类型信息进行比较。以下代码演示了一个例子:
private static void SomeMethod(object. o) { //GetType在运行时返回对象的类型(晚期绑定) //typeof返回指定类的类型(早期绑定) if (o.GetType() == typeof(FileInfo)) {...} if (o.GetType() == typeof(DirectoryInfo)) {...} }
如前所述,Type对象是轻量级的对象引用,要更多地了解类型本身,必须获取一个TypeInfo 对象,后者才代表类型定义。可调用System.Refleetion.IntrospectionExtensions的 GerTypeInfo扩展方法将Type对象转换成TypeInfo对象。
Type typeReference = ...;//例如o.GetType()或typeof(Object) TypeInfo typeDefinition = typeReference.GetTypeInfo();
另外,虽然作用不大,但还可调用TypeInfo的AsType方法将TypeInfo对象转换为Type对象。
TypeInfo typeDefinition = ...;
Type TypeReference = typeDefinition.AsType();
获取TypeInfo对象会强迫CLR确保已加载类型的定义程序集,从而对类型进行解析。这个操作可能代价高昂。如果只需要类型引用(Type对象),就应避免这个操作。但一旦获得了TypeInfo对象,就可查询类型的许多属性进一步了解它。大多数属性,比如 IsPublic,IsSealed,IsAbstract,IsClass 和 IsValueType等,都指明了与类型关联的标志。另一些属性,比如 Assembly,AssemblyQualifiedName,FulName 和 Module 等,则返回定义该类型的程序集或模块的名称以及类型的全名。还可查询BaseType属性来获取对类型的基类型的引用。除此之外,还有许多方法能提供关于类型的更多信息。文档描述了TypeInfo公开的所有方法和属性。
3、构建Exception派生类型的层次结构
以下代码使用本章讨论的许多概念将一组程序集加载到AppDomain中,并显示最终从System.Exception派生的所有类:
public static void Go() { //显式加载想要反射的程序集 LoadAssemblies(); //对所有类型进行筛选和排序 var allTypes = (from a in AppDomain.CurrentDomain.GetAssemblies() from t in a.ExportedTypes where typeof(Exception).GetTypeInfo().IsAssignableFrom(t.GetTypeInfo()) orderby t.Name select t).ToArray(); //生成并显示继承层次结构 Console.WriteLine(WalkInheritanceHierarchy(new StringBuilder(), 0, typeof(Exception), allTypes)); private static StringBuilder WalkInheritanceHierarchy( StringBuilder sb, Int32 indent, Type baseType, IEnumerableallTypes) { String spaces = new String(' ', indent * 3); sb.AppendLine(spaces + baseType.FullName); foreach (var t in allTypes) { if (t.GetTypeInfo().BaseType != baseType) continue; WalkInheritanceHierarchy(sb, indent + 1, t, allTypes); } return sb; } private static void LoadAssemblies() { String[] assemblies = { "System, PublicKeyToken={0}", "System.Core, PublicKeyToken={0}", "System.Data, PublicKeyToken={0}", "System.Design, PublicKeyToken={1}", "System.DirectoryServices, PublicKeyToken={1}", "System.Drawing, PublicKeyToken={1}", "System.Drawing.Design, PublicKeyToken={1}", "System.Management, PublicKeyToken={1}", "System.Messaging, PublicKeyToken={1}", "System.Runtime.Remoting, PublicKeyToken={0}", "System.Security, PublicKeyToken={1}", "System.ServiceProcess, PublicKeyToken={1}", "System.Web, PublicKeyToken={1}", "System.Web.RegularExpressions, PublicKeyToken={1}", "System.Web.Services, PublicKeyToken={1}", "System.Xml, PublicKeyToken={1}", }; String EcmaPublicKeyToken = "b77a5c561934e089"; String MSPublicKeyToken = "b03f5f7f11d50a3a"; // 获取包含System.Object的程序集的版本, // 假定其他所有程序集是相同的版本 Version version = typeof(System.Object).Assembly.GetName().Version; // 显示加载想要反射的程序集 foreach (String a in assemblies) { String AssemblyIdentity = String.Format(a, EcmaPublicKeyToken, MSPublicKeyToken) + ", Culture=neutral, Version=" + version; Assembly.Load(AssemblyIdentity); } } }
4、构造类型的实例
获得对Type派生对象的引用之后,就可以构造该类型的实例了。FCL提供了以下几个
机制:
● System.Activator 的 Createlnstance 方法
Activator类提供了静态CreateInstance方法的几个重载版本。调用方法时既可传递一个Type对象引用,也可传递标识了类型的String。直接获取类型对象的几个版本较为简单。你要为类型的构造器传递一组实参,方法返回对新对象的引用。用字符串来指定类型的几个版本则稍微复杂一些。首先必须指定另一个字符串来标识定义了类型的程序集。其次,如果正确配置了远程访问(remoting)选项,这些方法还允许构造远程对象。第三,这些版本返回的不是对新对象的引用,而是一个System.Runtime.Remoting.ObjectHandle 对象(从 System.MarshalByRefObjeet 派生)。ObjectHandle类型允许将一个AppDomain中创建的对象传至其他AppDomain,期间不强迫对象具体化(materialize)。准备好具体化这个对象时,请调用 ObjectHandle的Unwrap方法。在一个AppDomain中调用该方法时,它将定义了要具体化的类型的程序集加载到这个AppDomain中。如果对象按引用封送,会创建代理类型和对象。如果对象按值封送,对象的副本会被反序列化。
● System.Activator 的 CreateInstanceFrom 方法
Activator类还提供了一组静态CreatelnstanceFrom方法。它们与CreateInstance的行为相似,只是必须通过字符串参数来指定类型及其程序集。程序集用Assembly的LoadFrom(而非Load)方法加载到调用AppDomain中。由于都不接受Type参数,所以返回的都是一个ObjectHandle对象引用,必须调用ObjectHandle的Unwrap方法进行具体化。
● System.AppDomain 的方法
AppDomain类型提供了4个用于构造类型实例的实例方法(每个都有几个重载版本),包括 CreateInstance,CreateInstanceAndUnwrap, CreateInstanceFrom 和CreateInstanceFromAndUnwrap。这些方法的行为和Activator类的方法相似,区别在于它们都是实例方法,允许指定在哪个AppDomain中构造对象。另外,带Unwrap后缀的方法还能简化操作,不必执行额外的方法调用。
● System.Reflection.Constructorlnfo 的 Invoke 实例方法
使用一个Type对象引用,可以绑定到一个特定的构造器,并获取对构造器的ConstructorInfo对象的引用。然后,可利用ConstructorInfo对象引用来调用它的Invoke方法。类型总是在调用AppDomain中创建,返回的是对新对象的引用。
利用前面列出的机制,可为除数组(System.Array 派生类型)和委托(System.MulticastDelegate 派生类型)之外的所有类型创建对象。创建数组需要调用Array的静态CreateInstance方法(有几个重载的版本)。所有版本的CreateInstance方法获取的第一个参数都是对数组元素 Type 的引用。CreateInstance的其他参数允许指定数组维数和上下限的各种组合。创建委托则要调用 MethodInfo 的静态 CreateDelegate 方法。所有版本的CreateDelegate方法获取的第一个参数都是对委托Type的引用。CreateDelegate方法的其他参数允许指定在调用实例方法时应将哪个对象作为this参数传递。
构造泛型类型的实例首先要获取对开放类型的引用,然后调用Type的MakeGenericType 方法并向其传递一个数组(其中包含要作为类型实参使用的类型)。然后,获取返回的Type 对象并把它传给上面列出的某个方法。下面是一个例子:
using System; using System.Reflection; internal sealed class Dictionary{} public static class Program { public static void Main() { //获取对泛型类型的对象的引用 Type openType = typeof(Dictionary<,>); //使用TKey=String、TValue=Int32封闭泛型类型 Type closedType = openType.MakeGenericType(typeof(String), typeof(Int32)); //构造封闭类型的实例 Object o = Activator.CreateInstance(closedType); //证实能正常工作 Console.WriteLine(o.GetType()); } }
构建可扩展应用程序时,接口是中心。可用基类代替接口,但接口通常是首选的,因为它允许加载项开发人员选择他们自己的基类。例如,假定要写一个应用程序来无缝地加载和使用别人创建的类型。下面描述了如何设计这种应用程序:
● 创建“宿主 SDK”(Host SDK)程序集,它定义了一个接口,接口的方法作为宿主应用程序与加载项之间的通信机制使用。为接口方法定义参数和返回类型时,请尝试使用MSCorLib.dll 中定义的其他接口或类型。要传递并返回自己的数据类型,也在“宿主SDK”程序集中定义。一旦搞定接口定义,就可为这个程序集赋予强名称,然后把它打包并部署到合作伙伴和用户那里。发布后要避免对该程序集中的类型做出任何重大的改变。例如,不要以任何方式更改接口。但如果定义了任何数据类型,在类型中添加新成员是完全允许的。对程序集进行任何修改之后,可能需要使用一个发布者策略文件来部署它。
● 当然,加载项开发人员会在加载项程序集中定义自己的类型。这些程序集将引用你的“宿主”程序集中的类型。加载项开发人员可按自己的步调推出程序集的新版本,而宿主应用程序能正常使用加载项中的类型,不会出任何纰漏。
● 创建单独的“宿主应用程序”程序集,在其中包含你的应用程序的类型。这个程序集显然要引用“宿主 SDK”程序集,并使用其中定义的类型。可自由修改“宿主应用程序”程序集的代码。由于加载项开发人员不会引用这个“宿主应用程序”程序集,所以随时都能推出“宿主应用程序”程序集的新版本,这不会对加载项开发人员产生任何影响。
本节包含了一些非常重要的信息。跨程序集使用类型时,需要关注程序集的版本控制问题。要花一些时间精心建构,将跨程序集通信的类型隔离到它们自己的程序集中。要避免以后更改这些类型的定义。但是,如果真的要修改类型定义,一定要修改程序集的版本号,并为新版本的程序集创建发布者策略文件。
下面来看一个非常简单的例子,它综合运用了所有这些知识。首先是 HostSDK.dll 程序集的代码:
using System; namespace Wintellect.HostSDK { public interface IAddIn { String DoSomething(Int32 x); } }
其次是AddlnTypes.dll程序集的代码,其中定义了两个公共类型,它们实现了HostSDK.dll的接口。要生成该程序集,必须引用HostSDK.dll程序集:
using System; using Wintellect.HostSDK; public sealed class AddIn_A : IAddIn { public AddIn_A() { } public String DoSomething(Int32 x) { return "AddIn_A:"+ x.ToString(); } } public sealed class AddIn_B : IAddIn { public AddIn_B() { } public String DoSomething(Int32 x) { return "AddIn_B:"+(x*2).ToString(); } }
然后是一个简单的Host.exe程序集(控制台应用程序)的代码。生成该程序集必须引用HostSDK.dll程序集。为了发现有哪些可用的加载项类型,以下宿主代码假定类型是在一个以.dll 文件扩展名结尾的程序集中定义的,而且这些程序集已部署到和宿主的EXE文件相同的目录中。Microsoft的“托管可扩展性框架”(Managed Extensibility Framework,MEF)是在我刚才描述的各种机制的顶部构建的,它还提供了加载项注册和发现机制。构建动态可扩展应用程序时,强烈建议研究一下MEF,它能简化本章描述的一些操作。
using System; using System.IO; using System.Reflection; using System.Collections.Generic; using Wintellect.HostSDK; public static class Program { //查找宿主EXE文件所在的目录 String AddInDir = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location); //假定加载项程序集和宿主EXE文件在同一个目录 var AddInAssemblies = Directory.EnumerateFiles(AddInDir, "*.dll"); //创建可由宿主使用的所有加载Type的一个集合 var AddInTypes = from file in AddInAssemblies let assembly = Assembly.Load(file) from t in assembly.ExportedTypes //公开导出的类型 //如果类型实现了IAddIn接口,该类型就可由宿主使用 where t.IsClass && typeof(IAddIn).GetTypeInfo().IsAssignableFrom(t.GetTypeInfo()) select t; //初始化完成:宿主已发现了所有可用的加载项 //下面示范宿主如何构造加载项对象并使用他们 foreach (Type t in AddInTypes) { IAddIn ai = (IAddIn) Activator.CreateInstance(t); Console.WriteLine(ai.DoSomething(5)); } }
这个简单的宿主/加载项例子没有用到AppDomain。但在实际应用中,每个加载项都可能要在自己的AppDomain中创建,每个AppDomain都有自己的安全性和配置设置。当然,如果希望将加载项从内存中移除,可以卸载相应的AppDomain。为了跨AppDomain边界通信,可告诉加载项开发人员从MashalByRefObjeet派生出他们自己的加载类型。但另一个更常见的办法是让宿主应用程序定义自己的、从MashalByRefObject派生的内部类型。每个AppDomain创建好后,宿主要在新AppDomain中创建它自己的MashalByRefObject派生类型实例。宿主的代码(位于默认AppDomain中)将与它自己的类型(位于其他AppDomain中通信,让后者载入加载项程序集,并创建和使用加载的类型的实例。
到目前为止,本章的重点一直都是构建动态可扩展应用程序所需的反射机制,包括程序集加载、类型发现以及对象构造。要获得好的性能和编译时的类型安全性,应尽量避免使用反射。如果是动态可扩展应用程序,构造好对象后,宿主代码一般要将对象转型为编译时己知的接口类型或者基类。这样访问对象的成员就可以获得较好的性能,而且可以确保编译时的类型安全性。
本章剩余部分将从其他角度探讨反射,目的是发现并调用类型的成员。一般利用这个功能创建开发工具和实用程序,查找特定编程模式或者对特定成员的使用,从而对程序集进行分析。例子包括ILDasm,FxCopCmd.exe以及Visual Situdio的Windows窗体WPFWeb窗体设计器。另外,一些类库也利用这个功能发现和调用类型的成员,为开发人员提供便利和丰富的功能。例子包括执行序列化/反序列化以及简单数据绑定的类库。
1、发现类型的成员
字段、构造器、方法、属性、事件和嵌套类型都可以定义成类型的成员。FCL 包含抽象基
类 System.Reflection.MemberInfo,封装了所有类型成员都通用的一组属性。MemberInfo
有许多派生类,每个都封装了与特定类型成员相关的更多属性。下图是这些类型的层次
结构。
以下程序演示了如何查询类型的成员并显示成员的信息。代码处理的是由调用 AppDomain加载的所有程序集定义的所有公共类型。对每个类型都调用DeclaredMembers属性以返回由MemberInfo派生对象构成的集合;每个对象都引用类型中定义的一个成员。然后,显示每个成员的种类(字段、构造器、方法和属性等)及其字符串值(调用ToString来获取)。
using System; using System.Reflection; namespace ConsoleApp11 { class Program { static void Main(string[] args) { //遍历这个AppDomain中加载的所有程序集 Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies(); foreach (Assembly a in assemblies) { Show(0, "Assembly: {0}", a); //查找程序集中的类型 foreach (Type t in a.ExportedTypes) { Show(1, "Type: {0}", t); //发现类型的成员 foreach (MemberInfo 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); } } } Console.Read(); } private static void Show(Int32 indent, String format, params Object[] args) { Console.WriteLine(new String(' ', 3 * indent) + format, args); } } }
MemberInfo的所有派生类型都通用的属性和方法 | ||
成员名称 | 成员类型 | 说明 |
Name | 一个String属性 | 返回成员名称 |
DeclaringType | 一个Type属性 | 返回声明成员的Type |
Module | 一个Module属性 | 返回声明成员的Module |
CustomAttributes | 该属性返回一个IEnumerable<CustomAttributeData> | 返回一个集合,其中每个元素都标识了应用于该成员的一个定制特性的实例。定制特性可应用于任何成员。虽然Assembly不从MemberInfo派生,但它提供了可用于程序集的相同属性 |
在查询 DeclaredMembers 属性所返回的集合中,每个元素都是对层次结构中的一个具体类型的引用。虽然TypeInfo的DeclaredMembers属性能返回类型的所有成员,但还可利用TypeInfo提供的一些方法返回具有指定字符串名称的成员类型。例如,利用Typelnfo,GetDeclaredNestedType,GetDeclaredField,GetDeclaredMethod,GetDeclaredProperty和GetDeclaredEvent方法,可分别返回一个 TypeInfo、FieldInfo、MethodInfo、PropertyInfo和EventInfo对象引用。而利用GetDeclaredMethods方法能返回由MethodInfo对象构成的集合,这些对象描述了和指定字符串名称匹配的一个(多个)方法。
下图总结了用于遍历反射对象模型的各种类型。基于AppDomain,可发现其中加载的所有程序集。基于程序集,可发现构成它的所有模块。基于程序集或模块,可发现它定义的所有类型。基于类型,可发现它的嵌套类型、字段、构造器、方法、属性和事件。命名空间不是这个层次结构的一部分,因为它们只是从语法角度将相关类型聚集到一起。CLR不知道什么是命名空间。要列出程序集中定义的所有命名空间,需枚举程序集中的所有类型,并查看其Namespace属性。
基于一个类型,还可发现它实现的接口。基于构造器、方法、属性访问器方法或者事件的添加/删除方法,可调用GetParameters方法来获取由ParameterInfo对象构成的数组,从而了解成员的参数的类型。还可查询只读属性 ReturnParameter 获得一个ParameterInfo对象,它详细描述了成员的返回类型。对于泛型类型或方法,可调用GetGenericArguments方法来获得类型参数的集合。最后,针对上述任何一项,都可查询CustomAttributes属性来获得应用于它们的自定义定制特性的集合。
2、调用类型的成员
如何调用成员 | ||
成员类型 | 调用(invoke)成员而需调用(call)的方法 | |
FieldInfo |
调用GetValue获取字段的值 调用SetValue()设置字段的值 |
|
ConstructorInfo | 调用Invoke构造类型的实例并调用构造器 | |
MethodInfo | 调用Invoke来调用类型的方法 | |
PropertyInfo |
调用GetValue来调用属性的get访问器方法 调用SetValue来调用属性的set访问器方法 |
|
EventInfo |
调用AddEventHandler来调用事件的add访问器方法 调用RemoveEventHandler来调用事件的remove访问器方法 |
PropertyInfo类型代表与属性有关的元数据信息;也就是说,PropertyInfo提供了CanRead、CanWrite和PropertyType只读属性,它们指出属性是否可读和可写,以及属性的数据类型是什么。PropertyInfo还提供了只读GetMethod和SetMethod属性,它们返回代表属性get和set访问器方法的MethodInfo对象.PropertyInfo的GetValue和SetValue方法只是为了提供方便;在内部,它们会自己调用合适的MethodInfo对象。为了支持有参属性(C#的索引器),GetValue和SetValue方法提供了一个Object[]类型的index参数。
EventInfo类型代表与事件有关的元数据信息。EventInfo类型提供了只读EventHandlerType属性,返回事件的基础委托的Type。EventInfo类型还提供了只读AddMethod和RemoveMethod属性,返回为事件增删委托的方法的Methodlnfo对象。增删委托可调用这些MethodInfo对象,也可调用EventInfo类型提供的更好用的AddEventHandler和RemoveEventHandler方法。
以下示例应用程序演示了用反射来访问类型成员的各种方式。SomeType类包含多种成员:一个私有字段(m_someField);一个公共构造器(SomelType),它获取一个传引用的Int32实参;一个公共方法(ToString):一个公共属性(SomeProp);以及一个公共事件(SomeEvent)。定义好SomeType类型后,我提供了三个不同的方法,它们利用反射来访问SomeType的成员。三个方法用不同的方式做相同的事情:
● BindToMemberThenInvokeTheMember方法演示了如何绑定到成员并调用它。
● BindToMemberCreateDelegateToMemberThenInvokeTheMember方法演示了如何绑定到一个对象或成员,然后创建一个委托来引用该对象或成员。通过委托来调用的速度很快。如果需要在相同的对象上多次调用相同的成员,这个技术的性能比上一个好。
● UseDynamicToBindAndinvokeTheMember方法演示了如何利用C#的dynamic基元类型简化成员访问语法。此外,在相同类型的不同对象上调用相同成员时,这个技术还能提供不错的性能,因为针对每个类型,绑定都只会发生一次。而且可以缓存起来,以后多次调用的速度会非常快。用这个技术也可以调用不同类型的对象的成员。
using System; using System.Reflection; using Microsoft.CSharp.RuntimeBinder; using System.Linq; namespace ConsoleApp12 { class Program { static void Main(string[] args) { Type t = typeof(SomeType); BindToMemberTheInvokeTheMember(t); Console.WriteLine(); BindToMemberCreateDelegateToMemberTheInvokeTheMember(t); Console.WriteLine(); UseDynamicToBindAndInvokeTheMember(t); Console.WriteLine(); Console.Read(); } private static void BindToMemberTheInvokeTheMember(Type t) { Console.WriteLine("BindToMemberTheInvokeTheMember"); //构造实例 Type ctorArgument = Type.GetType("System.Int32&"); //或者 //Type ctorArgument = typeof(Int32).MakeByRefType(); ConstructorInfo ctor = t.GetTypeInfo().DeclaredConstructors.First( c => c.GetParameters()[0].ParameterType == ctorArgument); Object[] args = new Object[] { 12 }; //构造器的实参 Console.WriteLine("x before constructor called: " + args[0]); Object obj = ctor.Invoke(args); Console.WriteLine("Type: " + obj.GetType()); Console.WriteLine("x after constructor returns: " + args[0]); //读写字段 FieldInfo fi = obj.GetType().GetTypeInfo().GetDeclaredField("m_someField"); fi.SetValue(obj, 33); Console.WriteLine("someField: " + fi.GetValue(obj)); //调用方法 MethodInfo mi = obj.GetType().GetTypeInfo().GetDeclaredMethod("ToString"); String s = (String)mi.Invoke(obj, null); Console.WriteLine("ToString: " + s); //读写属性 PropertyInfo pi = obj.GetType().GetTypeInfo().GetDeclaredProperty("SomeProp"); try { pi.SetValue(obj, 0, null); } catch (TargetInvocationException e) { if (e.InnerException.GetType() != typeof(ArgumentOutOfRangeException)) throw; Console.WriteLine("Property set catch."); } pi.SetValue(obj, 2, null); Console.WriteLine("SomeProp: " + pi.GetValue(obj, null)); //为事件添加和删除委托 EventInfo ei = obj.GetType().GetTypeInfo().GetDeclaredEvent("SomeEvent"); EventHandler eh = new EventHandler(EventCallback);//See ei.EventHandlerType ei.AddEventHandler(obj, eh); ei.RemoveEventHandler(obj, eh); } //添加到事件的回调方法 private static void EventCallback(Object sender, EventArgs e) { } private static void BindToMemberCreateDelegateToMemberTheInvokeTheMember(Type t) { Console.WriteLine("BindToMemberCreateDelegateToMemberTheInvokeTheMember"); //构造实例(不能创建对构造器的委托) Object[] args = new object[] { 12 }; //构造器实参 Console.WriteLine("x before constructor called: " + args[0]); Object obj = Activator.CreateInstance(t, args); Console.WriteLine("Type: " + obj.GetType().ToString()); Console.WriteLine("x after constructor returns: " + args[0]); //注意:不能创建对字段的委托 //调用方法 MethodInfo mi = obj.GetType().GetTypeInfo().GetDeclaredMethod("ToString"); var toString = mi.CreateDelegate<Func<String>>(obj); String s = toString(); Console.WriteLine("ToString: " + s); //读写属性 PropertyInfo pi = obj.GetType().GetTypeInfo().GetDeclaredProperty("SomeProp"); var setSomeProp = pi.SetMethod.CreateDelegate<Action<Int32>>(obj); try { setSomeProp(0); } catch (ArgumentOutOfRangeException) { Console.WriteLine("Property set catch."); } setSomeProp(2); var getSomeProp = pi.GetMethod.CreateDelegate<Func<Int32>>(obj); Console.WriteLine("SomeProp: " + getSomeProp()); //向事件增删委托 EventInfo ei = obj.GetType().GetTypeInfo().GetDeclaredEvent("SomeEvent"); var addSomeEvent = ei.AddMethod.CreateDelegate<Action<EventHandler>>(obj); addSomeEvent(EventCallback); var removeSomeEvent = ei.RemoveMethod.CreateDelegate<Action<EventHandler>>(obj); removeSomeEvent(EventCallback); } private static void UseDynamicToBindAndInvokeTheMember(Type t) { Console.WriteLine("UseDynamicToBindAndInvokeTheMember"); //构造实例(不能创建对构造器的委托) Object[] args = new object[] { 12 }; //构造器实参 Console.WriteLine("x before constructor called: " + args[0]); dynamic obj = Activator.CreateInstance(t, args); Console.WriteLine("Type: " + obj.GetType().ToString()); Console.WriteLine("x after constructor returns: " + args[0]); //读写字段 try { obj.m_someField = 5; Int32 v = (Int32)obj.m_someField; Console.WriteLine("someField: " + v); } catch(RuntimeBinderException e) { //之所以会执行到这里,是因为字段是私有的 Console.WriteLine("Failed to access field: " + e.Message); } //调用访求 String s = (String)obj.ToString(); Console.WriteLine("ToString: " + s); //读写属性 try { obj.SomeProp = 0; } catch(ArgumentOutOfRangeException) { Console.WriteLine("Property set catch."); } obj.SomeProp = 2; Int32 val = (Int32)obj.SomeProp; Console.WriteLine("SomeProp: " + val); //从事件增删委托 obj.SomeEvent += new EventHandler(EventCallback); obj.SomeEvent -= new EventHandler(EventCallback); } } internal static class ReflectionExtensions { //这个辅助扩展方法简化了创建委托的语法 public static TDelegate CreateDelegate<TDelegate>(this MethodInfo mi, Object target = null) { return (TDelegate)(Object)mi.CreateDelegate(typeof(TDelegate), target); } } //该类用于演示反射机制, //其中定义了一个字段、构造器、方法、属性和一个事件 internal sealed class SomeType { private Int32 m_someField; public SomeType(ref Int32 x) { x *= 2; } public override string ToString() { return m_someField.ToString(); } public Int32 SomeProp { get { return m_someField; } set { if (value < 1) throw new ArgumentOutOfRangeException("value"); m_someField = value; } } public event EventHandler SomeEvent; private void NoCompilerWarnings() { SomeEvent.ToString(); } } }
注意,SomeType构造器唯一的参数就是传引用的Int32。上述代码演示了如何调用这个构造器,如何在构造器返回后检查修改过的Int32值。在BindToMemberTheninvokeTheMember方法靠近顶部的地方,我调用Type的GetType方法并传递字符串"System.Int32&"。其中的“&”表明参数是传引用的。这个符号是类型名称的巴克斯-诺尔范式(BNF)语法的一部分,详情请参考文档。在注释中,我还解释了如何用Type的MakeByRefType方法获得相同效果。
3、使用绑定句柄减少进程的内存消耗
许多应用程序都绑定了一组类型(Type对象)或类型成员(MemberInfo派生对象),并将这些对象保存在某种形式的集合中。以后,应用程序搜索这个集合,查找特定对象,然后调用(invoke)这个对象。这个机制很好,只是有个小问题:Type和MemberInfo派生对象需要大量内存。所以,如果应用程序容纳了太多这样的对象,但只是偶尔调用,应用程序消耗的内存就会急剧增加,对应用程序的性能产生负面影响。
CLR内部用更精简的方式表示这种信息。CLR之所以为应用程序创建这些对象,只是为了方便开发人员。CLR不需要这些大对象就能运行。如果需要保存/缓存大量Type和MemberInfo派生对象,开发人员可以使用运行时句柄(runtime handle)代替对象以减小工作集(占用的内存)。FCL定义了三个运行时句柄类型(全部都在System命名空间中),包括RuntimeTypeHandle, RuntimeFieldHandle 和 RuntimeMethodHandle。三个类型都是值类型,都只包含一个字段,也就是一个IntPtr;这使类型的实例显得相当精简(相当省内存)。IntPtr字段是一个句柄,引用了AppDomain的Loader堆中的一个类型、字段或方法。因此,现在需要以一种简单、高效的方式将重量级的Type或MemberInfo对象转换为轻量级的运行时句柄实例,反之亦然。幸好,使用以下转换方法和属性可轻松达到目的。
● 要将Type对象转换为一个RuntimeTypeHandle,调用Type的静态GetTypeHandle方法并传递那个Type对象引用。
● 要将一个RuntimeTypeHandle转换为Type对象,调用Type的静态方法GetTypeFromHandle,并传递那个RuntimeTypeHandle。
● 要将FieldInfo对象转换为一个RuntimeFieldHandle,查询FieldInfo的实例只读属性FieldHandle。
● 要将一个RuntimeFieldHandle转换为FieldInfo对象,调用FieldInfo的静态方法GetFieldFromHandle。
● 要将MethodInfo对象转换为一个RuntimeMethodHandle,查询MethodInfo的实例只读属性MethodHandle。
● 要将一个RuntimeMethodHandle转换为一个MethodInfo对象,调用 MethodInfo的静态方法GetMethodFromHandle。
以下示例程序获取许多MethodInfo对象,把它们转换为RuntimeMethodHandle实例,并演示了转换前后的工作集的差异:
using System; using System.Reflection; using System.Collections.Generic; namespace ConsoleApp13 { 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 objects"); //为所有MethodInfo对象构建RuntimeMethodHandle缓存 List<RuntimeMethodHandle> methodHandles = methodInfos.ConvertAll<RuntimeMethodHandle>(mb=>mb.MethodHandle); Show("Holding MethodInfo and RuntimeMethodHandle cache"); //让methodInfos在GC.KeepAlive()之前不要被垃圾回收 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.Read(); } private static void Show(String s) { Console.WriteLine("Heap size={0, 12:N0} - {1}", GC.GetTotalMemory(true), s); } } }
标签: C#
日历
最新文章
随机文章
热门文章
分类
存档
- 2024年11月(3)
- 2024年10月(5)
- 2024年9月(3)
- 2024年8月(3)
- 2024年7月(11)
- 2024年6月(3)
- 2024年5月(9)
- 2024年4月(10)
- 2024年3月(11)
- 2024年2月(24)
- 2024年1月(12)
- 2023年12月(3)
- 2023年11月(9)
- 2023年10月(7)
- 2023年9月(2)
- 2023年8月(7)
- 2023年7月(9)
- 2023年6月(6)
- 2023年5月(7)
- 2023年4月(11)
- 2023年3月(6)
- 2023年2月(11)
- 2023年1月(8)
- 2022年12月(2)
- 2022年11月(4)
- 2022年10月(10)
- 2022年9月(2)
- 2022年8月(13)
- 2022年7月(7)
- 2022年6月(11)
- 2022年5月(18)
- 2022年4月(29)
- 2022年3月(5)
- 2022年2月(6)
- 2022年1月(8)
- 2021年12月(5)
- 2021年11月(3)
- 2021年10月(4)
- 2021年9月(9)
- 2021年8月(14)
- 2021年7月(8)
- 2021年6月(5)
- 2021年5月(2)
- 2021年4月(3)
- 2021年3月(7)
- 2021年2月(2)
- 2021年1月(8)
- 2020年12月(7)
- 2020年11月(2)
- 2020年10月(6)
- 2020年9月(9)
- 2020年8月(10)
- 2020年7月(9)
- 2020年6月(18)
- 2020年5月(4)
- 2020年4月(25)
- 2020年3月(38)
- 2020年1月(21)
- 2019年12月(13)
- 2019年11月(29)
- 2019年10月(44)
- 2019年9月(17)
- 2019年8月(18)
- 2019年7月(25)
- 2019年6月(25)
- 2019年5月(17)
- 2019年4月(10)
- 2019年3月(36)
- 2019年2月(35)
- 2019年1月(28)
- 2018年12月(30)
- 2018年11月(22)
- 2018年10月(4)
- 2018年9月(7)
- 2018年8月(13)
- 2018年7月(13)
- 2018年6月(6)
- 2018年5月(5)
- 2018年4月(13)
- 2018年3月(5)
- 2018年2月(3)
- 2018年1月(8)
- 2017年12月(35)
- 2017年11月(17)
- 2017年10月(16)
- 2017年9月(17)
- 2017年8月(20)
- 2017年7月(34)
- 2017年6月(17)
- 2017年5月(15)
- 2017年4月(32)
- 2017年3月(8)
- 2017年2月(2)
- 2017年1月(5)
- 2016年12月(14)
- 2016年11月(26)
- 2016年10月(12)
- 2016年9月(25)
- 2016年8月(32)
- 2016年7月(14)
- 2016年6月(21)
- 2016年5月(17)
- 2016年4月(13)
- 2016年3月(8)
- 2016年2月(8)
- 2016年1月(18)
- 2015年12月(13)
- 2015年11月(15)
- 2015年10月(12)
- 2015年9月(18)
- 2015年8月(21)
- 2015年7月(35)
- 2015年6月(13)
- 2015年5月(9)
- 2015年4月(4)
- 2015年3月(5)
- 2015年2月(4)
- 2015年1月(13)
- 2014年12月(7)
- 2014年11月(5)
- 2014年10月(4)
- 2014年9月(8)
- 2014年8月(16)
- 2014年7月(26)
- 2014年6月(22)
- 2014年5月(28)
- 2014年4月(15)
友情链接
- Unity官网
- Unity圣典
- Unity在线手册
- Unity中文手册(圣典)
- Unity官方中文论坛
- Unity游戏蛮牛用户文档
- Unity下载存档
- Unity引擎源码下载
- Unity服务
- Unity Ads
- wiki.unity3d
- Visual Studio Code官网
- SenseAR开发文档
- MSDN
- C# 参考
- C# 编程指南
- .NET Framework类库
- .NET 文档
- .NET 开发
- WPF官方文档
- uLua
- xLua
- SharpZipLib
- Protobuf-net
- Protobuf.js
- OpenSSL
- OPEN CASCADE
- JSON
- MessagePack
- C在线工具
- 游戏蛮牛
- GreenVPN
- 聚合数据
- 热云
- 融云
- 腾讯云
- 腾讯开放平台
- 腾讯游戏服务
- 腾讯游戏开发者平台
- 腾讯课堂
- 微信开放平台
- 腾讯实时音视频
- 腾讯即时通信IM
- 微信公众平台技术文档
- 白鹭引擎官网
- 白鹭引擎开放平台
- 白鹭引擎开发文档
- FairyGUI编辑器
- PureMVC-TypeScript
- 讯飞开放平台
- 亲加通讯云
- Cygwin
- Mono开发者联盟
- Scut游戏服务器引擎
- KBEngine游戏服务器引擎
- Photon游戏服务器引擎
- 码云
- SharpSvn
- 腾讯bugly
- 4399原创平台
- 开源中国
- Firebase
- Firebase-Admob-Unity
- google-services-unity
- Firebase SDK for Unity
- Google-Firebase-SDK
- AppsFlyer SDK
- android-repository
- CQASO
- Facebook开发者平台
- gradle下载
- GradleBuildTool下载
- Android Developers
- Google中国开发者
- AndroidDevTools
- Android社区
- Android开发工具
- Google Play Games Services
- Google商店
- Google APIs for Android
- 金钱豹VPN
- TouchSense SDK
- MakeHuman
- Online RSA Key Converter
- Windows UWP应用
- Visual Studio For Unity
- Open CASCADE Technology
- 慕课网
- 阿里云服务器ECS
- 在线免费文字转语音系统
- AI Studio
- 网云穿
- 百度网盘开放平台
- 迅捷画图
- 菜鸟工具
- [CSDN] 程序员研修院
- 华为人脸识别
- 百度AR导航导览SDK
- 海康威视官网
- 海康开放平台
- 海康SDK下载
- git download
交流QQ群
-
Flash游戏设计: 86184192
Unity游戏设计: 171855449
游戏设计订阅号