[C#] Assembly Management

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

之前在簡介反射的時候提到過侯谁,我們可以在運行時加載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)建指定類型的一個對象,最后利用typeInvokeMember方法去調用該類型中的指定方法笋轨。

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()方法去調用指定的方法秆剪,只是這里不再顯示地去獲取AssemblyType,而是利用用戶指定的assembly pathtype 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)建對象之前和之后分別打印我們新建的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具體是在什么時機被加載進來徽千,我們在Main方法的入口給AppDomain.CurrentDomain.AssemblyLoad綁定一個事件。有了這個事件蛛砰,只要有Assembly加載發(fā)生罐栈,就會打印當時正在加載的AssemblyFullName

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痒芝。

相關文獻:

  1. Assembly Class
  2. Assembly(c#中簡單說明[轉])
  3. C#反射Assembly 詳細說明
  4. 你了解 Assembly.Load 嗎?
  5. C# - Correct Way to Load Assembly, Find Class and Call Run() Method
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末牵素,一起剝皮案震驚了整個濱河市严衬,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌笆呆,老刑警劉巖瞳步,帶你破解...
    沈念sama閱讀 222,729評論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異腰奋,居然都是意外死亡,警方通過查閱死者的電腦和手機抱怔,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,226評論 3 399
  • 文/潘曉璐 我一進店門劣坊,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人屈留,你說我怎么就攤上這事局冰。” “怎么了灌危?”我有些...
    開封第一講書人閱讀 169,461評論 0 362
  • 文/不壞的土叔 我叫張陵康二,是天一觀的道長。 經常有香客問我勇蝙,道長沫勿,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 60,135評論 1 300
  • 正文 為了忘掉前任,我火速辦了婚禮产雹,結果婚禮上诫惭,老公的妹妹穿的比我還像新娘。我一直安慰自己蔓挖,他們只是感情好夕土,可當我...
    茶點故事閱讀 69,130評論 6 398
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著瘟判,像睡著了一般怨绣。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上拷获,一...
    開封第一講書人閱讀 52,736評論 1 312
  • 那天篮撑,我揣著相機與錄音,去河邊找鬼刀诬。 笑死咽扇,一個胖子當著我的面吹牛,可吹牛的內容都是我干的陕壹。 我是一名探鬼主播质欲,決...
    沈念sama閱讀 41,179評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼糠馆!你這毒婦竟也來了嘶伟?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 40,124評論 0 277
  • 序言:老撾萬榮一對情侶失蹤又碌,失蹤者是張志新(化名)和其女友劉穎九昧,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體毕匀,經...
    沈念sama閱讀 46,657評論 1 320
  • 正文 獨居荒郊野嶺守林人離奇死亡铸鹰,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 38,723評論 3 342
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了皂岔。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片蹋笼。...
    茶點故事閱讀 40,872評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖躁垛,靈堂內的尸體忽然破棺而出剖毯,到底是詐尸還是另有隱情,我是刑警寧澤教馆,帶...
    沈念sama閱讀 36,533評論 5 351
  • 正文 年R本政府宣布逊谋,位于F島的核電站,受9級特大地震影響土铺,放射性物質發(fā)生泄漏胶滋。R本人自食惡果不足惜板鬓,卻給世界環(huán)境...
    茶點故事閱讀 42,213評論 3 336
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望镀钓。 院中可真熱鬧穗熬,春花似錦、人聲如沸丁溅。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,700評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽盛险。三九已至拌倍,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間涯穷,已是汗流浹背棍掐。 一陣腳步聲響...
    開封第一講書人閱讀 33,819評論 1 274
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留拷况,地道東北人作煌。 一個月前我還...
    沈念sama閱讀 49,304評論 3 379
  • 正文 我出身青樓,卻偏偏與公主長得像赚瘦,于是被迫代替她去往敵國和親粟誓。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,876評論 2 361

推薦閱讀更多精彩內容