之前在簡介反射的時候提到過侯谁,我們可以在運行時加載dll并創(chuàng)建其中某種類型對應的實例导帝。而本文呢虚缎,則打算講講如果把動態(tài)加載程序集和調用程序集中的方法規(guī)范化撵彻,集成到類中,從而便于使用和維護实牡。
Assembly, is a reusable, versionable, and self-describing building block of a common language runtime application. 程序集千康,是.NET程序的最小組成單位,每個程序集都有自己的名稱铲掐、版本等信息,程序集通常表現(xiàn)為一個或多個文件(.exe或.dll文件)值桩。
1. 程序集的加載與管理
程序集的使用很多時候是通過反射獲取程序集里面的類型或者方法摆霉,所以這里我們提供了一個AssemblyManager
的類來管理加載的程序集,并提供獲取類型實例GetInstance
和特定方法GetMethod
的函數(shù)奔坟。
簡要介紹:
(1)loadedAssemblies
成員
我們將已經加載過的Assembly
保存在一個字典里携栋,key
為對應的程序集的path
,這樣下次使用它的時候就不必再重新Load
一遍了咳秉,而是直接從字典中取出相關的Assembly
婉支。盡管據(jù)說AppDomain
中的Assembly.Load
是有優(yōu)化的,即加載過的程序集會被保存在Global Assembly Cache (GAC)
中澜建,以后的Load
會直接從cache
里面取出向挖。但這些都是對我們不可見的,我們自己建立一套緩存機制也無可厚非炕舵,至少我們可以更清晰地看到所有的處理邏輯何之。
(2)RegisterAssembly
方法
這里我們提供了三種注冊Assembly
的方法,第一種咽筋,是直接提供程序集路徑和程序集溶推,我們直接把他們緩存到字典即可; 第二種,是調用者還沒有獲取到相應的程序集蒜危,所以只提供了一個路徑虱痕,我們會根據(jù)這個路徑去加載相應的程序集,然后緩存在字典中辐赞;第三種部翘,則提供了另外一種加載程序集的機制,如果調用者在程序內部只有程序集的完成內容(byte
數(shù)組)而并沒有對應的dll文件占拍,則我們可以直接利用Assembly.Load()
對其進行加載略就,不需要死板地寫臨時文件再從文件中加載程序集。
(3)GetMethod
方法
用戶指定程序集名晃酒、類名和方法名表牢,我們根據(jù)這些參數(shù)返回該方法的MethodInfo。
(4)GetInstance
方法
用戶指定程序集名和類名贝次,我們返回該類的一個實例崔兴。
using System;
using System.Reflection;
using System.Collections.Generic;
namespace AssemblyManagerExample
{
public class AssemblyManager
{
private readonly Dictionary<string, Assembly> loadedAssemblies;
public AssemblyManager()
{
this.loadedAssemblies = new Dictionary<string, Assembly>();
}
// Three different method to register assembly
// 1: We already have the assembly
// 2: Directly load assembly from path
// 3: Load assembly from byte array, if we already have the dll content
public void RegisterAssembly(string assemblyPath, Assembly assembly)
{
if (!this.loadedAssemblies.ContainsKey(assemblyPath))
{
this.loadedAssemblies[assemblyPath] = assembly;
}
}
public void RegisterAssembly(string assemblyPath)
{
if (!this.loadedAssemblies.ContainsKey(assemblyPath))
{
Assembly assembly = Assembly.LoadFrom(assemblyPath);
if (assembly == null)
{
throw new ArgumentException($"Unable to load assembly [{assemblyPath}]");
}
this.RegisterAssembly(assemblyPath, assembly);
}
}
public void RegisterAssembly(string assemblyPath, byte[] assemblyContent)
{
if (!this.loadedAssemblies.ContainsKey(assemblyPath))
{
Assembly assembly = Assembly.Load(assemblyContent);
if (assembly == null)
{
throw new ArgumentException($"Unable to load assembly [{assemblyPath}]");
}
this.RegisterAssembly(assemblyPath, assembly);
}
}
public Assembly GetAssembly(string assemblyPath)
{
this.RegisterAssembly(assemblyPath);
return this.loadedAssemblies[assemblyPath];
}
public MethodInfo GetMethod(string assemblyPath, string typeName, string methodName)
{
Assembly assembly = this.GetAssembly(assemblyPath);
Type type = assembly.GetType(typeName, false, true);
if (type == null)
{
throw new ArgumentException($"Assembly [{assemblyPath}] does not contain type [{typeName}]");
}
MethodInfo methodInfo = type.GetMethod(methodName);
if (methodInfo == null)
{
throw new ArgumentException($"Type [{typeName}] in assembly [{assemblyPath}] does not contain method [{methodName}]");
}
return methodInfo;
}
public object GetInstance(string assemblyPath, string typeName)
{
return this.GetAssembly(assemblyPath).CreateInstance(typeName);
}
}
}
2. 執(zhí)行動態(tài)加載的DLL中的方法
很多時候我們動態(tài)地加載DLL是為了執(zhí)行其中的某個或者某些方法,這一點上節(jié)的Assembly Manager
中并沒有涉及蛔翅。接下來這里將介紹兩種調用的方式敲茄,并且通過實驗測試一下重復調用這些方法時DLL會不會重復加載。
(1)利用Assembly.LoadFrom()
方法從指定的文件路徑加載Assembly
山析,然后從得到的Assembly
獲取相應的類型type
(注意這里的參數(shù)一定得是類型的FullName
)堰燎,再用Activator.CreateInstance()
創(chuàng)建指定類型的一個對象,最后利用type
的InvokeMember
方法去調用該類型中的指定方法笋轨。
InvokeMember(String,?BindingFlags,?Binder,?Object,?Object[])
Invokes the specified member, using the specified binding constraints and matching the specified argument list.
public void RunFuncInUserdefinedDll(string dllName, string typeName, string funcName)
{
Assembly assembly = Assembly.LoadFrom(dllName);
if (assembly == null)
{
throw new FileNotFoundException(dllName);
}
Type type = assembly.GetType(typeName);
if (type == null)
{
throw new ArgumentException($"Unable to get [{typeName}] from [{dllName}]");
}
object obj = Activator.CreateInstance(type);
type.InvokeMember(funcName, BindingFlags.InvokeMethod, null, obj, null);
}
(2)其實最終還是一樣調用Type.InvokeMember()
方法去調用指定的方法秆剪,只是這里不再顯示地去獲取Assembly
和Type
,而是利用用戶指定的assembly path
和type name
直接通過AppDomain
來創(chuàng)建一個對象爵政。(需要注意的是仅讽,使用該方法調用的方法一定要Serializable
)
public void RunFuncInUserdefinedDll(string dllName, string typeName, string funcName)
{
AppDomain domain = AppDomain.CreateDomain("NewDomain");
object obj = domain.CreateInstanceFromAndUnwrap(dllName, typeName);
obj.GetType().InvokeMember(funcName, BindingFlags.InvokeMethod, null, obj, null);
}
最后,我們來試試看調用上述方法兩次時DLL的加載情況吧钾挟。為了確定是否受AppDomain
的影響洁灵,我們讓每次調用上述方法時創(chuàng)建的Domain
的名字是一個隨機的Guid
字符串。我們在創(chuàng)建對象之前和之后分別打印我們新建的AppDomain
里Assembly
中的DLL名字掺出。
public void RunFuncInUserdefinedDll(string dllName, string typeName, string funcName)
{
AppDomain domain = AppDomain.CreateDomain(Guid.NewGuid().ToString());
ListAssemblies(domain, "List Assemblies Before Creating Instance");
object obj = domain.CreateInstanceFromAndUnwrap(dllName, typeName);
ListAssemblies(domain, "List Assemblies After Creating Instance");
obj.GetType().InvokeMember(funcName, BindingFlags.InvokeMethod, null, obj, null);
}
private void ListAssemblies(AppDomain domain, string message)
{
Console.WriteLine($"*** {message}");
foreach (Assembly assembly in domain.GetAssemblies())
{
Console.WriteLine(assembly.FullName);
}
Console.WriteLine("***");
}
為了更加有效地捕捉道DLL具體是在什么時機被加載進來徽千,我們在Main方法的入口給AppDomain.CurrentDomain.AssemblyLoad
綁定一個事件。有了這個事件蛛砰,只要有Assembly
加載發(fā)生罐栈,就會打印當時正在加載的Assembly
的FullName
。
AppDomain.CurrentDomain.AssemblyLoad += (e, arg) =>
{
Console.WriteLine($"AssemblyLoad Called by: {arg.LoadedAssembly.FullName}");
};
下面是調用兩次時輸出的信息:
>AssemblyManagement.exe ClassLibrary1.dll ClassLibrary1.Class1 Method1
*** List Assemblies Before Creating Instance
mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
***
AssemblyLoad Called by: ClassLibrary1, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
*** List Assemblies After Creating Instance
mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
ClassLibrary1, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
***
This is Method1 in Class1
*** List Assemblies Before Creating Instance
mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
***
*** List Assemblies After Creating Instance
mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
ClassLibrary1, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
***
This is Method1 in Class1
從上述結果我們可以看到泥畅,兩次調用的時候在創(chuàng)建對象之前AppDomain
中都是只有一個系統(tǒng)Assembly mscorlib
荠诬,而創(chuàng)建對象之后則新增了我們自定義的ClassLibrary1
琅翻。
但不同的是在第一個調用中創(chuàng)建對象的時候,我們看到了AssemblyLoad Called
的輸出信息柑贞,即我們自定義的DLL是在那個時機被加載進來的方椎。而第二次調用該函數(shù)的時候雖然那個AppDomain
中也沒有那個Assembly
,但是somehow它被緩存在某個地方所以不需要重新加載钧嘶。
最后棠众,寫完第2節(jié)時,我有那么一瞬間覺得第一節(jié)并不需要有决,因為加載Assembly
本身就已經有緩存機制了闸拿。但是再細想一下,我們自定義的Assembly Manager
至少還有兩個優(yōu)點:1)使用更加靈活书幕。如果我們兩在兩個不同的方法中分別使用同一個DLL的兩個不同版本新荤,直接使用Assembly.LoadFrom
很難做到,即使你試圖把這兩個DLL分隔在不同的AppDomain
里面台汇。如果我們自己直接從byte[]
中加載DLL苛骨,則完全可以在loadedAssemblies
中利用不同的key
來區(qū)分相同DLL的不同版本。2)可以把Assembly Manager
作為類的成員苟呐,這樣就可以把assembly
區(qū)分到類型的粒度而不是AppDomain
痒芝。
相關文獻: