程序集
程序集是.net中的概念,程序集可以看作是给一堆相关类打一个包,相当于java中的jar包。在.Net 中,程序集(Assembly)中保存了元数据(MetaData)信息,因此就可以通过分析元数据来获取程序集中的内容,比如类,方法,属性等,这大大方便了在运行时去动态创建实例。
程序集是代码进行编译是的一个逻辑单元,把相关的代码和类型进行组合,然后生成PE文件(例如可执行文件**.exe和类库文件.dll**)。
对于静态程序集可以生成单个或多个文件,而动态程序集是存在于内存中的。在C#中程序集处处可见,因为任何基于.NET的代码在编译时都至少存在一个程序集。
基于.NET框架的.dll库是一个完整的程序集,需要事先引用对应的类库。从代码的结构上看,一个程序集可以包含一个或多个命名空间,而每个命名空间中又可以包含子命名空间或类型列表。由于程序集在编译后可以生成多个模块文件,因此一个物理文件并不代表它就是一个程序集,一个程序集并不一定只有一个文件。在VS开发环境中,一个解决方案可以包含多个项目,而每个项目就是一个程序集。
应用程序结构:包含 应用程序域(AppDomain),程序集(Assembly),模块(Module),类型(Type),成员(EventInfo、FieldInfo、MethodInfo、PropertyInfo) 几个层次。
程序集包含:
- 资源文件
- 类型元数据(描述在代码中定义的每一类型和成员,二进制形式)
- IL代码(这些都被封装在exe或dll中)
使用.NET建立的可执行程序 *.exe,并没有直接承载到进程当中,而是承载到应用程序域(AppDomain)当中。在一个进程中可以包含多个应用程序域,一个应用程序域可以装载一个可执行程序(*.exe)或者多个程序集(*.dll),这样可以使应用程序域之间实现深度隔离,即使进程中的某个应用程序域出现错误,也不会影响其他应用程序域的正常运作。处理asp.net所涉及的类大多数定义在System.Web程序集中。
- 当exe程序集加载完毕,.Net会在当前进程中创建一个默认应用程序域,这个应用程序域的名称与程序集名称相同。默认应用程序域不能被卸载,并且与其所在的进程同生共灭。
- 应用程序域只是允许它所加载的程序集访问由.Net Runtime所提供的服务。这些服务包括托管堆(Managed Heap),垃圾回收器(Garbage collector),JIT 编译器等.Net底层机制.
- 一个应用程序域中如果出现了致命错误导致崩溃,只会影响其本身,而不会影响到其他应用程序域。实现了错误隔离。
应用程序域是进程中承载程序集的逻辑分区,在应用程序域中存在更细粒度的用于承载.NET对象的实体,即.NET上下文Context。
所有的.NET对象都存在于.NET上下文当中,每个AppDomain当中至少存在一个默认上下文(context0)。
exe与dll的区别
exe可以运行,dll不能直接运行,因为exe中有一个main函数(入口函数)。
Untiy中在Assets文件夹中编写脚本,如果没有进行自定义操作,会默认编译到 Assembly-CSharp.dll 中,如果不分多个程序集,就会发现只要改了一行代码就会编译好久的现象。
开发过程中如果进行了编辑器扩展,创建了Editor文件夹,并在该目录下编写了脚本,则默认编译到Assembly-CSharp-Editor.dll文件中。
DLL文件的生成路径:项目路径\Library\ScriptAssemblies\xxxx.dll
使用程序集的好处
- 开发者可以自定义程序集,定义明晰的依赖关系,可以确保脚本更改后,只会重新生成必需的程序集,减少编译时间
- 可以跨项目进行程序的复用,加快开发效率
- 支持跨语言的编程,例如可以在unity中使用C++语言编辑的DLL文件
如何在Unity中使用程序集:请看Unity关于程序集(Assembly )的那些事_unity assembly-CSDN博客
加载
要从程序集的元数据中获取程序集的信息,首先需要加载程序集。
手动加载
System.Reflection.Assembly
Load()方法接收一个String或AssemblyName类型作为参数,这个参数实际上是需要加载的程序集的强名称(名称,版本,语言,公钥标记)。
LoadFrom()方法可以从指定文件中加载程序集,通过查找程序集的AssemblyRef元数据表,得知所有引用和需要的程序集,然后在内部调用Load()方法进行加载。例如:Assembly.LoadFrom(@"C:\ABC\Test.dll");
LoadFrom()首先会打开程序集文件,通过GetAssemblyName方法得到程序集名称,然后关闭文件,最后将得到的AssemblyName对象传入Load()方法中.
随后,Load()方法会再次打开这个文件进行加载。所以,LoadFrom()加载一个程序集时,会多次打开文件,造成了效率低下的现象(与Load相比)
LoadFrom()会直接返回Load()的结果——一个Assembly对象的引用
如果目标程序集已经加载过,LoadFrom()不会重新进行加载
LoadFrom支持从一个URL加载程序集(如"www.abc.com/test.dll"),…
public static Assembly Load(string assemblyString) public static Assembly LoadFrom(string assemblyFile) class Program { static void Main(string[] args) { Assembly assembly = Assembly.Load("System.Data,Version=4.0.0.0,Culture=neutral,PublicKeyToken=b77a5c561934e089"); foreach (Type type in assembly.GetExportedTypes()) { Console.WriteLine(type.FullName); } Console.ReadLine(); Assembly loadedAssembly2 = Assembly.LoadFrom("path_to_your_assembly.dll"); // 需要替换为你的程序集文件路径 Type loadedType2 = loadedAssembly2.GetType("Namespace.TypeName"); // 需要替换为你的类型名称 object instance = Activator.CreateInstance(loadedType2); method = loadedType2.GetMethod("MethodName"); // 需要替换为你的方法名称 method.Invoke(instance, null); // 执行方法 } }
使用Load方法加载程序集必须传递程序集的全名称,程序集的全名称由四个部分组成(程序集的简单名称、程序集的版本、程序集的语言文化,程序集的公钥标记)。程序集的实例属性FullName就是程序集的全名称。
LoadFrom方法允许从本地和远程网络加载程序集,在通过远程网络加载的是时候需要注意,不能通过FTP协议远程加载程序集。
System.AppDomain
在.Net 中,将应用程序域封装为了AppDomain类,这个类提供了应用程序域的各种操作,包含 加载程序集、创建对象、创建应用程序域 等。通常的编程情况下下,我们几乎从不需要对AppDomain进行操作。
除了使用Assembly类来加载程序集之外,还可以利用System.AppDomain类的实例方法public Assembly Load(string assemblyString)来完成。该实例方法有许多重载版本,这里需要注意,由于Assembly不是派生自MarshalByRefObject类,所以使用Load方法得到的程序集是按值封送。
public Assembly Load(string assemblyString); // 获取当前运行的代码所在的应用程序域 AppDomain currentDomain = AppDomain.CurrentDomain; AppDomain currentDomain = Thread.GetDomain(); // 获取应用程序域的名称 string name = AppDomain.CurrentDomain.FriendlyName; // 从当前应用程序域中创建新应用程序域 AppDomain newDomain = AppDomain.CreateDomain("New Domain"); // 配置应用程序域(在创建前) AppDomainSetup domainInfo = new AppDomainSetup(); domainInfo.ApplicationBase = "e:\\AttributeTest"; domainInfo.ConfigurationFile = "e:\\AttributeTest\\1.exe.config"; AppDomain domain = AppDomain.CreateDomain("MyDomain", null, domainInfo); // 判断是否为默认应用程序域 newDomain.IsDefaultAppDomain() // 卸载应用程序集 AppDomain.Unload(newDomain); // 在新的应用程序域中执行程序集, 加载可执行程序 AppDomain newAppDomain = AppDomain.CreateDomain("newAppDomain"); newAppDomain.ExecuteAssembly("Example.exe");//或使用ExecuteAssemblyByName AppDomain.Unload(newAppDomain); /** 如果代表当前AppDomain的域为A,Load方法调用在AppDomain域B中,那么被Load加载的程序集会被域A和域B都加载 这段代码中,MyAssembly程序集将会被加载到两个AppDomain中, 一个是运行代码所在的AppDomain,另一个是新创建的AppDomain。 这是因为Assembly是按值封送的,所以CLR将会在调用线程的AppDomain中重新加载程序集。 */ AppDomain ad = AppDomain.CreateDomain("ChildDomain"); ad.Load("MyAssembly");
两种方式的性能区别
System.AppDomain的Load方法得到的程序集是按值封送的,缺点是程序集将会被加载到两个AppDomain中。
原因是:加载域不一样,一个是运行代码所在的AppDomain; 一个是新创建的AppDomain。所以CLR将会在调用线程的AppDomain中重新加载程序集。
自动加载
BinaryFormart + FileStream 配合
第一步,加载assetBundle资源,assetBundle名称为Test:
//将本地文件转为字节
FileStream fs = new FileStream(Application.dataPath + "/Test", FileMode.Open, FileAccess.Read);
byte[] buffur = new byte[fs.Length];
fs.Read(buffur, 0, (int)fs.Length);
fs.Close();
第二步,加载程序集及获取指定类型
public GameObject _gameObjectRoot;
public TextAsset _script;
public GameObject _gameobject;
StartCoroutine(BeginGetAsset(buffur));
IEnumerator BeginGetAsset(byte[] bytes)
{
//从内存中获取资源
AssetBundleCreateRequest acr = AssetBundle.CreateFromMemory(bytes);
yield return acr;
_assetBundle = acr.assetBundle;
//加载资源中的预设
_gameObjectRoot = (GameObject)_assetBundle.LoadAsset("GameObjectRoot.prefab");
//将预设创建在场景内
yield return _gameobject = Instantiate(_gameObjectRoot);
//获取到assetBundle中的test.DLL
_script = (TextAsset)_assetBundle.LoadAsset("test", typeof(TextAsset));
//C#反射取出程序集
System.Reflection.Assembly assembly = System.Reflection.Assembly.Load(_script.bytes);
//加载程序集中的脚本Mytest.cs
_gameobject.AddComponent(assembly.GetType("Mytest"));
}
反射
优点:
允许在运行时发现并使用编译时还不了解的类型及其成员。
缺点:
一、编译时无法保证类型安全。举个例子: Type,GetType( int)编译时不会报错,运行时会报错。因为CLR只知道“System.int32”。
二、速度慢
1. 依据目标类型的字符串搜索扫描程序集的元数据的过程耗时。
2. 反射调用方法或属性比较耗时。原因在于首先必须将实参打包(pack)成数组: 在内部,反射必须将这些实参解包(unpack)到线程栈上。优化:使用多态避免反射操作。
文章参考:
Unity关于程序集(Assembly )的那些事_unity assembly-CSDN博客
程序集和反射(C#) - 邹琼俊 - 博客园 (cnblogs.com)
应用程序域 System.AppDomain,动态加载程序集 - springsnow - 博客园 (cnblogs.com)