[C#] Assembly Management

入鄉(xiāng)隨俗的配圖

之前在簡(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ì)象,最后利用typeInvokeMember方法去調(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)用指定的方法,只是這里不再顯示地去獲取AssemblyType,而是利用用戶(hù)指定的assembly pathtype 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ì)象之前和之后分別打印我們新建的AppDomainAssembly中的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í)正在加載的AssemblyFullName

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):

  1. Assembly Class
  2. Assembly(c#中簡(jiǎn)單說(shuō)明[轉(zhuǎn)])
  3. C#反射Assembly 詳細(xì)說(shuō)明
  4. 你了解 Assembly.Load 嗎?
  5. C# - Correct Way to Load Assembly, Find Class and Call Run() Method
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容