之前在簡(jiǎn)介反射的時(shí)候提到過(guò),我們可以在運(yùn)行時(shí)加載dll并創(chuàng)建其中某種類(lèi)型對(duì)應(yīng)的實(shí)例。而本文呢,則打算講講如果把動(dòng)態(tài)加載程序集和調(diào)用程序集中的方法規(guī)范化,集成到類(lèi)中,從而便于使用和維護(hù)。
Assembly, is a reusable, versionable, and self-describing building block of a common language runtime application. 程序集,是.NET程序的最小組成單位,每個(gè)程序集都有自己的名稱(chēng)、版本等信息,程序集通常表現(xiàn)為一個(gè)或多個(gè)文件(.exe或.dll文件)。
1. 程序集的加載與管理
程序集的使用很多時(shí)候是通過(guò)反射獲取程序集里面的類(lèi)型或者方法,所以這里我們提供了一個(gè)AssemblyManager
的類(lèi)來(lái)管理加載的程序集,并提供獲取類(lèi)型實(shí)例GetInstance
和特定方法GetMethod
的函數(shù)。
簡(jiǎn)要介紹:
(1)loadedAssemblies
成員
我們將已經(jīng)加載過(guò)的Assembly
保存在一個(gè)字典里,key
為對(duì)應(yīng)的程序集的path
,這樣下次使用它的時(shí)候就不必再重新Load
一遍了,而是直接從字典中取出相關(guān)的Assembly
。盡管據(jù)說(shuō)AppDomain
中的Assembly.Load
是有優(yōu)化的,即加載過(guò)的程序集會(huì)被保存在Global Assembly Cache (GAC)
中,以后的Load
會(huì)直接從cache
里面取出。但這些都是對(duì)我們不可見(jiàn)的,我們自己建立一套緩存機(jī)制也無(wú)可厚非,至少我們可以更清晰地看到所有的處理邏輯。
(2)RegisterAssembly
方法
這里我們提供了三種注冊(cè)Assembly
的方法,第一種,是直接提供程序集路徑和程序集,我們直接把他們緩存到字典即可; 第二種,是調(diào)用者還沒(méi)有獲取到相應(yīng)的程序集,所以只提供了一個(gè)路徑,我們會(huì)根據(jù)這個(gè)路徑去加載相應(yīng)的程序集,然后緩存在字典中;第三種,則提供了另外一種加載程序集的機(jī)制,如果調(diào)用者在程序內(nèi)部只有程序集的完成內(nèi)容(byte
數(shù)組)而并沒(méi)有對(duì)應(yīng)的dll文件,則我們可以直接利用Assembly.Load()
對(duì)其進(jìn)行加載,不需要死板地寫(xiě)臨時(shí)文件再?gòu)奈募屑虞d程序集。
(3)GetMethod
方法
用戶(hù)指定程序集名、類(lèi)名和方法名,我們根據(jù)這些參數(shù)返回該方法的MethodInfo。
(4)GetInstance
方法
用戶(hù)指定程序集名和類(lèi)名,我們返回該類(lèi)的一個(gè)實(shí)例。
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í)行動(dòng)態(tài)加載的DLL中的方法
很多時(shí)候我們動(dòng)態(tài)地加載DLL是為了執(zhí)行其中的某個(gè)或者某些方法,這一點(diǎn)上節(jié)的Assembly Manager
中并沒(méi)有涉及。接下來(lái)這里將介紹兩種調(diào)用的方式,并且通過(guò)實(shí)驗(yàn)測(cè)試一下重復(fù)調(diào)用這些方法時(shí)DLL會(huì)不會(huì)重復(fù)加載。
(1)利用Assembly.LoadFrom()
方法從指定的文件路徑加載Assembly
,然后從得到的Assembly
獲取相應(yīng)的類(lèi)型type
(注意這里的參數(shù)一定得是類(lèi)型的FullName
),再用Activator.CreateInstance()
創(chuàng)建指定類(lèi)型的一個(gè)對(duì)象,最后利用type
的InvokeMember
方法去調(diào)用該類(lèi)型中的指定方法。
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)其實(shí)最終還是一樣調(diào)用Type.InvokeMember()
方法去調(diào)用指定的方法,只是這里不再顯示地去獲取Assembly
和Type
,而是利用用戶(hù)指定的assembly path
和type name
直接通過(guò)AppDomain
來(lái)創(chuàng)建一個(gè)對(duì)象。(需要注意的是,使用該方法調(diào)用的方法一定要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);
}
最后,我們來(lái)試試看調(diào)用上述方法兩次時(shí)DLL的加載情況吧。為了確定是否受AppDomain
的影響,我們讓每次調(diào)用上述方法時(shí)創(chuàng)建的Domain
的名字是一個(gè)隨機(jī)的Guid
字符串。我們?cè)趧?chuàng)建對(duì)象之前和之后分別打印我們新建的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具體是在什么時(shí)機(jī)被加載進(jìn)來(lái),我們?cè)贛ain方法的入口給AppDomain.CurrentDomain.AssemblyLoad
綁定一個(gè)事件。有了這個(gè)事件,只要有Assembly
加載發(fā)生,就會(huì)打印當(dāng)時(shí)正在加載的Assembly
的FullName
。
AppDomain.CurrentDomain.AssemblyLoad += (e, arg) =>
{
Console.WriteLine($"AssemblyLoad Called by: {arg.LoadedAssembly.FullName}");
};
下面是調(diào)用兩次時(shí)輸出的信息:
>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
從上述結(jié)果我們可以看到,兩次調(diào)用的時(shí)候在創(chuàng)建對(duì)象之前AppDomain
中都是只有一個(gè)系統(tǒng)Assembly mscorlib
,而創(chuàng)建對(duì)象之后則新增了我們自定義的ClassLibrary1
。
但不同的是在第一個(gè)調(diào)用中創(chuàng)建對(duì)象的時(shí)候,我們看到了AssemblyLoad Called
的輸出信息,即我們自定義的DLL是在那個(gè)時(shí)機(jī)被加載進(jìn)來(lái)的。而第二次調(diào)用該函數(shù)的時(shí)候雖然那個(gè)AppDomain
中也沒(méi)有那個(gè)Assembly
,但是somehow它被緩存在某個(gè)地方所以不需要重新加載。
最后,寫(xiě)完第2節(jié)時(shí),我有那么一瞬間覺(jué)得第一節(jié)并不需要,因?yàn)榧虞dAssembly
本身就已經(jīng)有緩存機(jī)制了。但是再細(xì)想一下,我們自定義的Assembly Manager
至少還有兩個(gè)優(yōu)點(diǎn):1)使用更加靈活。如果我們兩在兩個(gè)不同的方法中分別使用同一個(gè)DLL的兩個(gè)不同版本,直接使用Assembly.LoadFrom
很難做到,即使你試圖把這兩個(gè)DLL分隔在不同的AppDomain
里面。如果我們自己直接從byte[]
中加載DLL,則完全可以在loadedAssemblies
中利用不同的key
來(lái)區(qū)分相同DLL的不同版本。2)可以把Assembly Manager
作為類(lèi)的成員,這樣就可以把assembly
區(qū)分到類(lèi)型的粒度而不是AppDomain
。
相關(guān)文獻(xiàn):