最近在工做中牽涉到了.NET下的一個古老的問題:Assembly的加載過程。雖然網上有不少文章介紹這部份內容,不少文章也是好久之前就已經出現了,但閱讀以後發現,並沒能解決個人問題,有些點寫的不是特別詳細,讓人看完以後感受仍是雲裏霧裏。最後,我決定從新複習一下這個經典而古老的問題,並將所得總結於此,而後會有一個實例對這個問題進行演示,但願可以幫助到你們。web
.NET下Assembly的加載,最主要的一步就是肯定Assembly的版本。在.NET下,託管的DLL和EXE都稱之爲Assembly,Assembly由AssemblyName來惟一標識,AssemblyName也就是你們所熟悉的Assembly.FullName,它是由五部分:名稱、版本、語言、公鑰Token、處理器架構組成的,這一點相信你們都知道。有關Assembly Name的詳細描述,請參考:https://docs.microsoft.com/en-us/dotnet/framework/app-domains/assembly-names。那麼版本,就是AssemblyName中的一個重要組成部分。其它四部分相同,版本若是不一樣的話,就不能算做是同一個Assembly。設計這樣一個Assembly的版本策略,微軟自己就是爲了解決最開始的DLL Hell的問題,在維基百科上着關於這段黑歷史的詳細描述,地址是:https://en.wikipedia.org/wiki/DLL_Hell,在此也就很少囉嗦了。架構
.NET下Assembly的加載過程,其實也是Assembly版本的肯定和Assembly文件的定位過程,步驟以下:app
至此,Assembly的最終版本已被肯定,接下來就是搜索Assembly文件並進行加載的過程了。框架
如今,CLR已經開始加載肯定版本的Assembly了,接下來就是搜索Assembly文件的過程。這個過程也叫做Assembly Probing。CLR會作如下事情:dom
在加載Assembly文件失敗的時候,AppDomain會觸發AssemblyResolve的事件,在這個事件的訂閱函數中,容許客戶程序自定義對加載失敗的Assembly的處理方式,好比,能夠經過Assembly.LoadFrom或者Assembly.LoadFile調用「手動地」將Assembly加載到AppDomain。ide
在.NET SDK中帶了一個fuslogvw.exe的應用程序,經過它能夠查看詳細的Assembly加載過程。使用方法很是簡單,使用管理員身份啓動Visual Studio 2017 Developer Command Prompt,而後在命令行輸入fuslogvw.exe,便可啓動日誌查看器。啓動以後,點擊Settings按鈕,以啓用日誌記錄功能:函數
日誌啓動以後,點擊Refresh按鈕,而後啓動你的.NET應用程序,就能夠看到當前應用程序所依賴的Assembly的加載過程日誌了:工具
接下來,我會作一個例子程序,而後使用這個工具來分析Assembly的加載過程。.net
理論結合實際,看看如何經過實際代碼來詮釋以上所述Assembly的加載過程。一個比較好的例子就是設計一個簡單的插件系統,並經過觀察系統加載插件的過程,來了解Assembly加載的前因後果。爲了簡單直觀,我把這個插件系統稱爲PluginDemo。這個插件很簡單,主體程序是一個控制檯應用程序,而後咱們實現兩個插件:Earth和Mars,在不一樣的插件的Initialize方法中,會輸出不一樣的字符串。插件
整個應用程序的項目結構以下:
該插件系統包含4個C#的項目:
注意:除了PluginDemo.Common以外的其它三個項目,都對PluginDemo.Common有引用關係。而PluginDemo.App項目僅僅在項目自己依賴於PluginDemo.Plugins.Earth和PluginDemo.Plugins.Mars,它不會去引用這兩個項目。目的就是爲了當PluginDemo.App被編譯時,其他兩個插件項目也會同時被編譯並輸出到指定位置。
在Earth插件的CustomAddIn類中,咱們實現了Initialize方法,並在此輸出一個字符串:
public class CustomAddIn : AddIn { public override string Name => "Earth AddIn"; public override void Initialize() { Console.WriteLine("Earth Plugin initialized."); } }
在Mars插件的CustomAddIn類中,咱們也實現了Initialize方法,並在此輸出一個字符串:
public class CustomAddIn : AddIn { public override string Name => "Mars AddIn"; public override void Initialize() { Console.WriteLine("Mars AddIn initialized."); } }
那麼,在插件系統主程序中,就會掃描Modules子目錄下的module.xml文件,而後解析每一個module.xml文件得到每一個插件類的Assembly Qualified Name,而後經過Type.GetType方法得到插件類,進而建立實例、調用Initialize方法。代碼以下:
static void Main() { var directory = new DirectoryInfo("Modules"); foreach(var file in directory.EnumerateFiles("module.xml", SearchOption.AllDirectories)) { var addinDefinition = AddInDefinition.ReadFromFile(file.FullName); var addInType = Type.GetType(addinDefinition.FullName); var addIn = (AddIn)Activator.CreateInstance(addInType); Console.WriteLine($"{addIn.Id} - {addIn.Name}"); addIn.Initialize(); } }
接下來,修改App.config文件,修改成:
<?xml version="1.0" encoding="utf-8" ?> <configuration> <runtime> <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1"> <probing privatePath="Modules\Earth;Modules\Mars;" /> </assemblyBinding> </runtime> </configuration>
此時,運行程序,能夠獲得:
目前沒有什麼問題。接下來,對兩個AddIn分別作一些修改。讓這兩個AddIn依賴於不一樣版本的Newtonsoft.Json,好比,Earth依賴於7.0.0.0的版本,Mars依賴於6.0.0.0的版本,而後分別修改兩個CustomAddIn的Initialize方法,在方法中各自調用一次JsonConvert.SerializeObject方法,以觸發Newtonsoft.Json這個Assembly的加載。此時再次運行程序,你將看到下面的異常:
如今,刷新fuslogvw.exe,找到Newtonsoft.Json的日誌:
雙擊打開日誌,能夠看到以下信息:
從整個過程能夠看出:
那麼接下來,改一改App.config文件,將privatePath下的兩個值換個位置呢?
再試試:
此時,Earth AddIn又出錯了。那麼,咱們加上版本重定向的配置,指定當程序須要加載7.0.0.0版本的Newtonsoft.Json時,讓它重定向到6.0.0.0的版本:
再次執行,成功了:
看看日誌:
版本已經被重定向到6.0.0.0,而且在Mars目錄下找到了6.0.0.0的Newtonsoft.Json,加載成功了。
這個案例的源代碼能夠點擊此處下載。
本文詳細介紹了.NET下Assembly的版本肯定和加載過程,最後給出了一個實例,對這個過程進行了演示。